mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #2726 from deepseek-harness/worktree/deepseek-vision-model-catalog
feat(llm-deepseek): publish the vision model
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.md: 3dddbe7e9fae74e4ce1ec1c8e93a40c3352b5a54
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: b93b333c6f25a779b1d307b6141bea9f5eb1e407
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.md: 8a7b882c3c6b6e6153a2d3b26d5c56440cb09658
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 77c27bf1f2148ecae7ab8857572bf58fbc3086a9
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ ACP selection must also preserve the provider dimension. The same model id may a
|
||||
|
||||
Catalog membership is advisory. It drives selectors and diagnostics but never changes `stream()` routing and never rejects an otherwise valid request. Provider ownership remains exclusive and lifecycle-bound; model ids remain request-time adapter input.
|
||||
|
||||
`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` named `DeepSeek-V4-Flash` and `deepseek-v4-pro` named `DeepSeek-V4-Pro`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
|
||||
`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` named `DeepSeek-V4-Flash`, `deepseek-v4-pro` named `DeepSeek-V4-Pro`, and image-capable `deepseek-v4-flash-vision-exp` named `DeepSeek-V4-Flash-Vision-Exp`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
|
||||
|
||||
### Per-session selection in the front end
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ ACP 选择还必须保留提供方维度。同一个模型 ID 可能存在于多
|
||||
|
||||
目录成员关系仅提供建议。它驱动选择器与诊断,但不会改变 `stream()` 路由,也不会拒绝原本有效的请求。提供方所有权仍然具有排他性并绑定生命周期;模型 ID 仍是请求时传给适配器的输入。
|
||||
|
||||
`dsh-llm-pi-ai` 将已配置提供方的 `getModels(provider)` 返回的已安装条目映射为提供方无关的目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受包含展示条目的可选 `models` 配置,默认包含名为 `DeepSeek-V4-Flash` 的 `deepseek-v4-flash` 和名为 `DeepSeek-V4-Pro` 的 `deepseek-v4-pro`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
|
||||
`dsh-llm-pi-ai` 将已配置提供方的 `getModels(provider)` 返回的已安装条目映射为提供方无关的目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受包含展示条目的可选 `models` 配置,默认包含名为 `DeepSeek-V4-Flash` 的 `deepseek-v4-flash`、名为 `DeepSeek-V4-Pro` 的 `deepseek-v4-pro`,以及名为 `DeepSeek-V4-Flash-Vision-Exp`、支持图片输入的 `deepseek-v4-flash-vision-exp`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
|
||||
|
||||
### 前端内的会话级选择
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 83293fbe9bd4e8a7b4422dbbe8495cf9f48bcdb5
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: fc9dee1d767d2de4cbb5170499a10cf3a231c333
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 3f77ab8d55f8eca821cd12a4591c6239c2ea10f5
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: c95c5abe664635f3cde3a1fc2d569c9474c69665
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ Model catalog entries gain optional merge-extensible input modality declarations
|
||||
|
||||
The host is the authoritative preflight boundary. It resolves the session's latest routed provider/model, falling back through agent options to host defaults; if that model explicitly excludes image input, it rejects the prompt before writing any attachment or event, and the client restores the draft. Image-bearing prompt admission and model selection share one per-agent serial boundary, and a dequeued prompt remains pending until its durable message event publishes ([ordering decision](../bug-fix/2026-07-29-atomic-web-image-admission.md)); a steering carrier gates from its enqueue until its `steering/message` event publishes, closing the outbox hop that never enters the queued mirror. Selection rejects a text-only target while an image is pending publication or remains in the session's current derived history. Compaction can remove old images and make a later text-only selection valid; idle without publication releases a claimed queued carrier, while steering retained in the outbox stays gated until publication or discard. `session.updateQueue` edits accept text content only, so a queue edit cannot inject an image past this admission boundary. Unknown capability proceeds to the adapter guard so uncatalogued model identifiers remain usable. The browser rejects unsupported declared image media types before allocating preview URLs, but it does not snapshot deployment limits or model capability: a handshake snapshot cannot represent a session's current target after `session.selectModel`, and deployment policy may change independently. The host validates the complete batch against current byte, count, aggregate, media, dimension, pixel, and routed-model policy before writing any attachment or event; its rejection announces through the composer's transient toast.
|
||||
|
||||
Pi-AI and the direct DeepSeek adapter resolve `ctx.attachments` at request time, recursively convert each durable image reference including references nested inside tool results, and emit native image content only for models that declare image input. The direct route accepts configured image-capable catalog entries but does not advertise a vision model until its endpoint is ready; its Flash, Pro, custom models without an image declaration, and unlisted pass-through ids remain text-only. Request-time service resolution keeps Cordis load order from freezing optional attachment availability. No adapter may flatten or skip a retained image; unsupported roles and models fail with typed `UNSUPPORTED_CONTENT`.
|
||||
Pi-AI and the direct DeepSeek adapter resolve `ctx.attachments` at request time, recursively convert each durable image reference including references nested inside tool results, and emit native image content only for models that declare image input. The direct route advertises `deepseek-v4-flash-vision-exp` as image-capable and accepts configured image-capable catalog entries; its Flash, Pro, custom models without an image declaration, and unlisted pass-through ids remain text-only. Request-time service resolution keeps Cordis load order from freezing optional attachment availability. No adapter may flatten or skip a retained image; unsupported roles and models fail with typed `UNSUPPORTED_CONTENT`.
|
||||
|
||||
Core supports structured assistant image blocks, but no current production provider route is certified for image output. Any future output-capable adapter must retrieve provider bytes under bounded size and time policy, validate them through the same attachment service, persist them, and only then publish the atomic `ImageBlock`. A URL in assistant Markdown remains text and is never downloaded automatically.
|
||||
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ Base64 只跨越一次协议边界,并在持久化后丢弃。每个入口都
|
||||
|
||||
宿主是权威的前置检查边界。它会解析会话最新路由到的提供方和模型,并在缺失时依次回退到 agent 选项和宿主默认值;如果该模型明确排除图片输入,宿主会在写入任何附件或事件前拒绝提示词,客户端则恢复草稿。包含图片的提示词准入与模型选择共用一个逐 agent 的串行边界,而且已经出队的提示词在其持久消息事件发布前仍保持待发布状态([顺序决策](../bug-fix/2026-07-29-atomic-web-image-admission.zh.md));steering 载体则从入队起就参与门槛,直到其 `steering/message` 事件发布为止,堵住了从不进入排队镜像的 outbox 窗口。当图片正等待发布或仍存在于会话当前的派生历史中时,模型选择会拒绝纯文本目标。压缩(compaction)可以移除旧图片,使之后选择纯文本目标变得有效;未发布任何事件即转入空闲时,已认领的 queued 载体会被释放,而保留在 outbox 中的 steering 在发布或丢弃前始终受门槛约束。`session.updateQueue` 的编辑只接受文本内容,因此队列编辑无法绕过该准入边界注入图片。能力未知时继续进入适配器强制检查,使未收录的模型标识符仍然可用。浏览器会在分配预览 URL 前拒绝声明不支持的图片媒体类型,但不会为部署限制或模型能力保留快照:握手快照无法表达 `session.selectModel` 之后会话的当前目标,部署策略也可能独立变化。宿主会根据当前的单张字节数、图片数量、总字节数、媒体类型、尺寸、像素数和路由模型策略校验整个批次,再写入任何附件或事件;其拒绝通过 composer 的短时 toast 播报。
|
||||
|
||||
Pi-AI 与直接 DeepSeek 适配器都会在请求时解析 `ctx.attachments`,递归转换每个持久图片引用,包括嵌套在工具结果中的引用,并且仅为声明支持图片输入的模型生成提供方原生图片内容。直接路由接受已配置且支持图片的 catalog 配置项,但在视觉模型端点就绪前不会公布视觉模型;其 Flash、Pro、未声明图片能力的自定义模型和未列出原样传递 id 仍仅支持文本。在请求时解析服务,可避免 Cordis 加载顺序将可选附件服务的可用性固化。任何适配器都不得将保留的图片展平或跳过;不支持的角色与模型会以类型化的 `UNSUPPORTED_CONTENT` 失败。
|
||||
Pi-AI 与直接 DeepSeek 适配器都会在请求时解析 `ctx.attachments`,递归转换每个持久图片引用,包括嵌套在工具结果中的引用,并且仅为声明支持图片输入的模型生成提供方原生图片内容。直接路由会将 `deepseek-v4-flash-vision-exp` 公布为支持图片,并接受已配置且支持图片的 catalog 配置项;其 Flash、Pro、未声明图片能力的自定义模型和未列出原样传递 id 仍仅支持文本。在请求时解析服务,可避免 Cordis 加载顺序将可选附件服务的可用性固化。任何适配器都不得将保留的图片展平或跳过;不支持的角色与模型会以类型化的 `UNSUPPORTED_CONTENT` 失败。
|
||||
|
||||
核心层支持结构化助手图片块,但当前没有任何生产提供方路径通过图片输出认证。未来任何支持输出的适配器都必须在有界的大小和时间策略下获取提供方字节,通过同一个附件服务校验并持久化字节,之后才能以原子方式发布 `ImageBlock`。助手 Markdown 中的 URL 仍是文本,绝不自动下载。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-19-direct-deepseek-vision-input.md
|
||||
2026-08-19-direct-deepseek-vision-input.md: 5c2f2ea3ff87b58906ce76caf9f611e07f3e2d04
|
||||
2026-08-19-direct-deepseek-vision-input.zh.md: 3e0fc26ee8ba8d0bed1478d54add5be1bfef01df
|
||||
2026-08-19-direct-deepseek-vision-input.md: 76d3244e67a73c1cdf4419a6537ada38e0a75bd5
|
||||
2026-08-19-direct-deepseek-vision-input.zh.md: a77231104156371fe698f8a8ad386cfa03251990
|
||||
|
||||
@@ -10,7 +10,7 @@ DeepSeek vision deployments use the chat-completions image protocol, but the dir
|
||||
|
||||
## Decision
|
||||
|
||||
The direct adapter lets a configured model opt in with `inputModalities: [text, image]`; validation rejects empty, unknown, or duplicate modalities. Flash, Pro, unlisted ids, and configured models that omit `inputModalities` remain explicitly text-only. The shipped catalog does not advertise `deepseek-v4-flash-vision-exp` until its model endpoint is ready, so the model selector cannot offer an unavailable route; deployment and snapshot catalogs can enable their exact vision model independently.
|
||||
The shipped catalog declares `deepseek-v4-flash-vision-exp` with `inputModalities: [text, image]`; configured catalogs use the same declaration to opt another exact model into image input, and validation rejects empty, unknown, or duplicate modalities. Flash, Pro, unlisted ids, and configured models that omit `inputModalities` remain explicitly text-only.
|
||||
|
||||
The adapter resolves `ctx.attachments` per image request, reads each retained durable reference with the request signal, and serializes verified bytes as ordered OpenAI-compatible `image_url` data URLs. Text-only user messages retain string content. Tool results retain string-only `tool` messages; image-only results use `(see attached image)`, and consecutive retained tool-result images follow in one `user` message beginning `Attached image(s) from tool result:`. System and assistant history images fail with `UNSUPPORTED_CONTENT` before attachment or network I/O.
|
||||
|
||||
@@ -27,8 +27,8 @@ Canonical messages continue to store only `ImageAttachmentRef`. Data URLs exist
|
||||
|
||||
## Verification
|
||||
|
||||
Package tests pin model discovery and fallback capabilities, configuration validation and live settings updates, user and tool-result wire messages, all admitted MIME types, cancellation, attachment failures, 413 classification, exact image-bound behavior, and pi-ai equivalence. A keyless assembled ACP request records the native adapter's tool-result data URL and oldest-image placeholder.
|
||||
Package tests pin model discovery and fallback capabilities, configuration validation and live settings updates, user and tool-result wire messages, all admitted MIME types, cancellation, attachment failures, 413 classification, exact image-bound behavior, and pi-ai equivalence. A keyless assembled ACP request records the native adapter's tool-result data URL and oldest-image placeholder. A real-API smoke test with an explicit image-capable catalog entry sends a deterministic image only when `DEEPSEEK_VISION_E2E=1` is set in addition to the provider key.
|
||||
|
||||
## Consequences
|
||||
|
||||
Configured DeepSeek vision routes can consume durable user and tool-result images without changing session durability or response streaming. Repeated history still expands request bodies, but deterministic oldest-first offload bounds the dominant payload and leaves headroom below the official 30 MiB request-body limit. Image token pricing remains provider-owned because the official image token formula is not available.
|
||||
The official DeepSeek vision route and configured vision routes can consume durable user and tool-result images without changing session durability or response streaming. Repeated history still expands request bodies, but deterministic oldest-first offload bounds the dominant payload and leaves headroom below the official 30 MiB request-body limit. Image token pricing remains provider-owned because the official image token formula is not available.
|
||||
|
||||
@@ -10,7 +10,7 @@ DeepSeek 视觉部署使用 chat-completions 图片协议,但直接 `deepseek-
|
||||
|
||||
## Decision
|
||||
|
||||
直接适配器允许已配置模型通过 `inputModalities: [text, image]` 选择加入;校验会拒绝空列表、未知模态或重复模态。Flash、Pro、未列出 id,以及省略 `inputModalities` 的已配置模型仍明确仅支持文本。在模型端点就绪前,随附目录不会公布 `deepseek-v4-flash-vision-exp`,因此模型选择器不会提供不可用路由;部署与 snapshot 目录可以独立启用其确切视觉模型。
|
||||
随附目录为 `deepseek-v4-flash-vision-exp` 声明 `inputModalities: [text, image]`;已配置目录可以用同一声明让另一个确切模型支持图片输入,校验会拒绝空列表、未知模态或重复模态。Flash、Pro、未列出 id,以及省略 `inputModalities` 的已配置模型仍明确仅支持文本。
|
||||
|
||||
适配器会对每个图片请求解析 `ctx.attachments`,用请求 signal 读取每个保留的持久引用,并将校验后的字节按顺序序列化为 OpenAI 兼容的 `image_url` data URL。纯文本 user 消息保留字符串内容。工具结果保留仅字符串的 `tool` 消息;仅含图片的结果使用 `(see attached image)`,连续工具结果中保留的图片随后合并进一条以 `Attached image(s) from tool result:` 开头的 `user` 消息。System 与 assistant 历史图片会在附件或网络 I/O 前以 `UNSUPPORTED_CONTENT` 失败。
|
||||
|
||||
@@ -27,8 +27,8 @@ DeepSeek 视觉部署使用 chat-completions 图片协议,但直接 `deepseek-
|
||||
|
||||
## Verification
|
||||
|
||||
包测试固定模型发现与回退能力、配置校验与存活 settings 更新、user 和工具结果协议消息、所有已准入 MIME 类型、取消、附件失败、413 分类、确切图片上限行为和 pi-ai 等价性。无需密钥的组装 ACP 请求会记录原生适配器的工具结果 data URL 与最旧图片占位文本。
|
||||
包测试固定模型发现与回退能力、配置校验与存活 settings 更新、user 和工具结果协议消息、所有已准入 MIME 类型、取消、附件失败、413 分类、确切图片上限行为和 pi-ai 等价性。无需密钥的组装 ACP 请求会记录原生适配器的工具结果 data URL 与最旧图片占位文本。真实 API 冒烟测试会配置明确支持图片的目录项,并且仅在提供方密钥之外还设置 `DEEPSEEK_VISION_E2E=1` 时发送确定性图片。
|
||||
|
||||
## Consequences
|
||||
|
||||
已配置的 DeepSeek 视觉路由可以消费持久 user 与工具结果图片,而无需改变会话持久性或响应流。重复历史仍会扩张请求正文,但确定性的最旧优先 offload 会限制主导 payload,并在官方 30 MiB 请求正文上限下保留余量。由于官方图片 token 公式尚不可用,图片 token 定价仍由提供方掌握。
|
||||
官方 DeepSeek 视觉路由与已配置视觉路由可以消费持久 user 与工具结果图片,而无需改变会话持久性或响应流。重复历史仍会扩张请求正文,但确定性的最旧优先 offload 会限制主导 payload,并在官方 30 MiB 请求正文上限下保留余量。由于官方图片 token 公式尚不可用,图片 token 定价仍由提供方掌握。
|
||||
|
||||
@@ -205,13 +205,13 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await settings.getByText('自定义设置').click()
|
||||
await settings.getByRole('button', { name: /删除模型/ }).first().click()
|
||||
await settings.getByRole('button', { name: '添加模型' }).click()
|
||||
const customModelId = settings.getByLabel('模型 ID 2')
|
||||
const customModelId = settings.getByLabel('模型 ID 3')
|
||||
await customModelId.fill('private-preview')
|
||||
await settings.getByLabel('显示名称 2').fill('Private Preview')
|
||||
await settings.getByLabel('显示名称 3').fill('Private Preview')
|
||||
// Capacities live behind the row's own disclosure, as in the pi-ai form.
|
||||
await settings.getByRole('button', { name: '容量 2' }).click()
|
||||
await settings.getByLabel('上下文窗口 2').fill('131072')
|
||||
await settings.getByLabel('最大输出 token 数 2').fill('64K')
|
||||
await settings.getByRole('button', { name: '容量 3' }).click()
|
||||
await settings.getByLabel('上下文窗口 3').fill('131072')
|
||||
await settings.getByLabel('最大输出 token 数 3').fill('64K')
|
||||
|
||||
const modelEditor = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MODELS_EXPECTED, modelEditor, MODE)
|
||||
@@ -220,6 +220,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(document).toContain('id: deepseek-v4-pro')
|
||||
expect(document).toContain('id: deepseek-v4-flash-vision-exp')
|
||||
expect(document).toContain('inputModalities:')
|
||||
expect(document).toContain('- image')
|
||||
expect(document).toContain('id: private-preview')
|
||||
expect(document).toContain('name: Private Preview')
|
||||
expect(document).toContain('contextWindow: 131072')
|
||||
@@ -236,6 +239,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await modelTrigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
expect(await page.getByText('deepseek-v4-flash', { exact: true }).count()).toBe(0)
|
||||
await page.getByRole('menuitemradio', { name: 'DeepSeek-V4-Flash-Vision-Exp' }).waitFor({ timeout: 10_000 })
|
||||
await page.getByRole('menuitemradio', { name: 'Private Preview' }).waitFor({ timeout: 10_000 })
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
|
||||
@@ -46,20 +46,30 @@
|
||||
- img
|
||||
- textbox "模型 ID 2":
|
||||
- /placeholder: 模型 ID
|
||||
- text: private-preview
|
||||
- text: deepseek-v4-flash-vision-exp
|
||||
- textbox "显示名称 2":
|
||||
- /placeholder: 显示名称
|
||||
- text: Private Preview
|
||||
- button "容量 2" [expanded]:
|
||||
- text: DeepSeek-V4-Flash-Vision-Exp
|
||||
- button "容量 2":
|
||||
- img
|
||||
- button "删除模型 2":
|
||||
- img
|
||||
- textbox "模型 ID 3":
|
||||
- /placeholder: 模型 ID
|
||||
- text: private-preview
|
||||
- textbox "显示名称 3":
|
||||
- /placeholder: 显示名称
|
||||
- text: Private Preview
|
||||
- button "容量 3" [expanded]:
|
||||
- img
|
||||
- button "删除模型 3":
|
||||
- img
|
||||
- text: 上下文窗口
|
||||
- textbox "上下文窗口 2":
|
||||
- textbox "上下文窗口 3":
|
||||
- /placeholder: 1M
|
||||
- text: "131072"
|
||||
- text: 最大输出 token 数
|
||||
- textbox "最大输出 token 数 2":
|
||||
- textbox "最大输出 token 数 3":
|
||||
- /placeholder: 256K
|
||||
- text: 64K
|
||||
- button "添加模型":
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 36f3e96e69207076b1a1bdae58032f7b3d0c1b8f
|
||||
config-catalog.zh.md: f3eaa4326b73bfe8f7a78ccce4f6026ef90f9f12
|
||||
config-catalog.md: 02ea9c182f0ca1a5c31dedab41aec57b8b79dec7
|
||||
config-catalog.zh.md: 571b1d4b997ea49f1efc23e3761cbb2d04e43c4e
|
||||
|
||||
@@ -928,7 +928,7 @@ export interface Config {
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
@@ -957,7 +957,7 @@ export interface DeepSeekCatalogModel {
|
||||
|
||||
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
|
||||
|
||||
Source: [`packages/llm/llm-deepseek/src/index.ts:66`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
Source: [`packages/llm/llm-deepseek/src/index.ts:72`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-pi-ai"></a>
|
||||
|
||||
|
||||
@@ -930,7 +930,7 @@ export interface Config {
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
@@ -959,7 +959,7 @@ export interface DeepSeekCatalogModel {
|
||||
|
||||
依赖:[`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
|
||||
|
||||
来源:[`packages/llm/llm-deepseek/src/index.ts:66`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
来源:[`packages/llm/llm-deepseek/src/index.ts:72`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-pi-ai"></a>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Code Mode image overlay: mounts the worker runtime and durable attachment
|
||||
# store so a nested read_image result can cross the generic rich-result bridge.
|
||||
# The authored snapshot is replay-only; the live config retains the ordinary
|
||||
# exact provider route for manual use.
|
||||
# The live config selects the shipped vision route for manual use.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
@@ -11,7 +10,7 @@
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-pro
|
||||
model: deepseek-v4-flash-vision-exp
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
|
||||
workspaceContext:
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
- id: deepseek-v4-pro
|
||||
- id: deepseek-v4-flash-vision-exp
|
||||
inputModalities: [text, image]
|
||||
|
||||
# The default composition confines bash AND the filesystem tools to the
|
||||
# workspace and asks before a wider retry. Snapshot runs select
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# Image-scenario overlay: adds the durable attachment store the read_image tool
|
||||
# commits through. The store resolves its root from $DSH_HOME, which the
|
||||
# snapshot harness scopes per run, so the overlay itself carries no paths. The
|
||||
# app config is restated to re-pin `deepseek-v4-flash` (base ships pro; the
|
||||
# authored fixture and the pinned header class are flash), because a config
|
||||
# patch replaces the whole app config. Keyless replay supplies its own exact
|
||||
# image-capable catalog while the production endpoint remains unavailable.
|
||||
# app config is restated to select the shipped vision model because a config
|
||||
# patch replaces the whole app config.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
@@ -14,7 +12,7 @@
|
||||
name: '@deepseek-ai/dsh-acp-demo'
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
model: deepseek-v4-flash-vision-exp
|
||||
persistenceRoot: !!js process.env.DSH_SNAPSHOT_SESSIONS_ROOT ?? './.sessions'
|
||||
persistenceCompression: none
|
||||
workspaceContext:
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: 1c007d34f329177603305da3858c63960e30a898
|
||||
README.zh.md: 5add3972037cc82fbacf8207476b63e40c7e631e
|
||||
README.md: bae9011135a9cbc14467086e4b6ebc6f052ed230
|
||||
README.zh.md: 0a5f0224dbebd62766775822260585825579f4a7
|
||||
|
||||
@@ -28,18 +28,18 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
|
||||
maxDelayMs: 10000
|
||||
jitterRatio: 0.1
|
||||
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
||||
models: # optional; defaults to V4 Flash and V4 Pro
|
||||
models: # optional; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp
|
||||
- id: deepseek-v4-flash
|
||||
name: DeepSeek-V4-Flash
|
||||
- id: private-vision
|
||||
name: Private Vision
|
||||
- id: deepseek-v4-flash-vision-exp
|
||||
name: DeepSeek-V4-Flash-Vision-Exp
|
||||
inputModalities: [text, image]
|
||||
- id: private-reasoner
|
||||
description: Company-hosted reasoning model
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`; omission resolves to normal mode with five retries. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` and `deepseek-v4-pro`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Vision models are not advertised by default until their endpoint rollout is complete, but a deployment can add one with `inputModalities: [text, image]`. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id, and omitted `inputModalities` means `text` only.
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`; omission resolves to normal mode with five retries. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash`, `deepseek-v4-pro`, and the image-capable `deepseek-v4-flash-vision-exp`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id, and omitted `inputModalities` means `text` only.
|
||||
|
||||
An image-capable catalog entry may declare `inputModalities: [text, image]`. The adapter resolves user and tool-result `ImageBlock` references through `ctx.attachments`, verifies the stored bytes, and sends transient `data:<media-type>;base64,...` `image_url` parts without changing the durable session message. Text-only and unlisted models reject image input before credential, attachment, or network I/O. System and assistant history remain image-free; tool-result images follow their string-only `tool` messages in a separate `user` message.
|
||||
|
||||
|
||||
@@ -28,18 +28,18 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
maxDelayMs: 10000
|
||||
jitterRatio: 0.1
|
||||
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
||||
models: # optional; defaults to V4 Flash and V4 Pro
|
||||
models: # optional; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp
|
||||
- id: deepseek-v4-flash
|
||||
name: DeepSeek-V4-Flash
|
||||
- id: private-vision
|
||||
name: Private Vision
|
||||
- id: deepseek-v4-flash-vision-exp
|
||||
name: DeepSeek-V4-Flash-Vision-Exp
|
||||
inputModalities: [text, image]
|
||||
- id: private-reasoner
|
||||
description: Company-hosted reasoning model
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
该插件注册唯一提供方路由 `deepseek-official`,并一同注册解析后的 `retryPolicy`;省略时会解析为 normal 模式并重试五次。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash` 和 `deepseek-v4-pro`,两者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。在视觉模型端点完成发布前,默认目录不会公布视觉模型,但部署方可以通过 `inputModalities: [text, image]` 主动添加。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id,省略 `inputModalities` 则表示仅支持 `text`。
|
||||
该插件注册唯一提供方路由 `deepseek-official`,并一同注册解析后的 `retryPolicy`;省略时会解析为 normal 模式并重试五次。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`、`deepseek-v4-pro` 与支持图片输入的 `deepseek-v4-flash-vision-exp`,三者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id,省略 `inputModalities` 则表示仅支持 `text`。
|
||||
|
||||
支持图片的 catalog 配置项可以声明 `inputModalities: [text, image]`。适配器通过 `ctx.attachments` 解析 user 和工具结果中的 `ImageBlock` 引用,校验已存储字节,再发送瞬态 `data:<media-type>;base64,...` `image_url` 部分,不改变持久会话消息。纯文本模型与未列出模型会在凭据、附件或网络 I/O 前拒绝图片输入。System 和 assistant 历史仍不能包含图片;工具结果图片会在仅含字符串的 `tool` 消息后,通过单独的 `user` 消息发送。
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@ const PROVIDER = 'deepseek-official'
|
||||
const DEFAULT_MODELS: DeepSeekCatalogModel[] = [
|
||||
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', contextWindow: DEFAULT_CONTEXT_WINDOW },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: DEFAULT_CONTEXT_WINDOW },
|
||||
{
|
||||
id: 'deepseek-v4-flash-vision-exp',
|
||||
name: 'DeepSeek-V4-Flash-Vision-Exp',
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
inputModalities: ['text', 'image'],
|
||||
},
|
||||
]
|
||||
|
||||
const MODEL_MODALITIES = ['text', 'image'] as const satisfies readonly ModelModality[]
|
||||
@@ -76,7 +82,7 @@ export interface Config {
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash, V4 Pro, and V4 Flash Vision Exp. */
|
||||
models?: DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs?: number
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
import type {
|
||||
ImageAttachmentLimits,
|
||||
ImageAttachmentRef,
|
||||
SaveImageAttachment,
|
||||
StoredImageAttachment,
|
||||
} from '@deepseek-ai/dsh-attachment'
|
||||
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
|
||||
@@ -12,12 +20,49 @@ import { assemble, type AssembledResult } from './assemble.ts'
|
||||
|
||||
/**
|
||||
* Real-API e2e for the direct-fetch adapter: V4 Flash + V4 Pro across
|
||||
* thinking modes and all official effort levels. Key-gated — skips
|
||||
* entirely without $DEEPSEEK_API_KEY (see vitest.e2e.config.ts).
|
||||
* thinking modes and all official effort levels. The suite skips entirely
|
||||
* without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally
|
||||
* requires $DEEPSEEK_VISION_E2E=1 (see vitest.e2e.config.ts).
|
||||
*/
|
||||
|
||||
const FLASH = 'deepseek-v4-flash'
|
||||
const PRO = 'deepseek-v4-pro'
|
||||
const VISION = 'deepseek-v4-flash-vision-exp'
|
||||
const VISION_E2E_ENABLED = process.env.DEEPSEEK_VISION_E2E === '1'
|
||||
const RED_IMAGE = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC',
|
||||
'base64',
|
||||
)
|
||||
const RED_IMAGE_REF: ImageAttachmentRef = {
|
||||
attachmentId: AttachmentId(`sha256:${createHash('sha256').update(RED_IMAGE).digest('hex')}`),
|
||||
mediaType: 'image/png',
|
||||
bytes: RED_IMAGE.byteLength,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
|
||||
class E2eAttachmentStore extends AttachmentStore {
|
||||
readonly imageLimits: ImageAttachmentLimits = {
|
||||
maxImageBytes: 1024,
|
||||
maxImagesPerMessage: 1,
|
||||
maxMessageImageBytes: 1024,
|
||||
maxImagePixels: 1,
|
||||
maxImageDimension: 1,
|
||||
mediaTypes: ['image/png'],
|
||||
}
|
||||
|
||||
validateImage(_input: SaveImageAttachment): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
saveImage(_input: SaveImageAttachment): Promise<ImageAttachmentRef> {
|
||||
return Promise.resolve(RED_IMAGE_REF)
|
||||
}
|
||||
|
||||
readImage(_ref: ImageAttachmentRef, _signal?: AbortSignal): Promise<StoredImageAttachment> {
|
||||
return Promise.resolve({ ref: RED_IMAGE_REF, data: RED_IMAGE })
|
||||
}
|
||||
}
|
||||
const contexts: Context[] = []
|
||||
let identityHome: string
|
||||
|
||||
@@ -30,6 +75,7 @@ async function harness(_model: string, config: Partial<Config> = {}) {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(E2eAttachmentStore)
|
||||
await ctx.plugin(LlmDeepSeek, config)
|
||||
return ctx
|
||||
}
|
||||
@@ -65,6 +111,25 @@ const weatherTool: ToolSchema = {
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () => {
|
||||
it.skipIf(!VISION_E2E_ENABLED)('recognizes a deterministic image with the official vision model', async () => {
|
||||
const ctx = await harness(VISION, {
|
||||
thinking: 'disabled',
|
||||
})
|
||||
const result = await assemble(ctx, {
|
||||
model: VISION,
|
||||
messages: [createUserMessage({
|
||||
content: [
|
||||
{ type: 'text', text: 'This image is one solid color. Reply with only its English color name.' },
|
||||
{ type: 'image', attachment: RED_IMAGE_REF },
|
||||
],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
})],
|
||||
maxTokens: 50,
|
||||
})
|
||||
expect(result.finish.kind).toBe('stop')
|
||||
expect(textOf(result).toLowerCase()).toContain('red')
|
||||
})
|
||||
|
||||
it('serves a real request with the key held only by a credentials-local document', async () => {
|
||||
const key = process.env.DEEPSEEK_API_KEY
|
||||
if (key === undefined) throw new Error('e2e ran without DEEPSEEK_API_KEY')
|
||||
|
||||
@@ -117,10 +117,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
return Promise.resolve({ ref, data: Uint8Array.of(1, 2, 3) })
|
||||
}),
|
||||
} as unknown as AttachmentStore
|
||||
const adapter = adapterOf({
|
||||
baseURL: server.url,
|
||||
models: [{ id: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }],
|
||||
}, attachments)
|
||||
const adapter = adapterOf({ baseURL: server.url }, attachments)
|
||||
|
||||
await drain(adapter.stream({
|
||||
provider: 'deepseek-official',
|
||||
@@ -768,6 +765,7 @@ describe('plugin registration and config', () => {
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] },
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', inputModalities: ['text'] },
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] },
|
||||
])
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash'))
|
||||
.resolves.toMatchObject({
|
||||
@@ -786,6 +784,15 @@ describe('plugin registration and config', () => {
|
||||
defaultEffort: ReasoningEffortId('high'),
|
||||
},
|
||||
})
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash-vision-exp'))
|
||||
.resolves.toMatchObject({
|
||||
provider: 'deepseek-official',
|
||||
id: 'deepseek-v4-flash-vision-exp',
|
||||
name: 'DeepSeek-V4-Flash-Vision-Exp',
|
||||
inputModalities: ['text', 'image'],
|
||||
context: { contextWindow: 1_000_000 },
|
||||
defaultMaxTokens: 256_000,
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['off', 'low', 'max'] as const)('uses the configured %s reasoning default', async (effort) => {
|
||||
@@ -865,6 +872,7 @@ describe('plugin registration and config', () => {
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: ['text'] },
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', inputModalities: ['text'] },
|
||||
{ provider: 'deepseek-official', id: 'deepseek-v4-flash-vision-exp', name: 'DeepSeek-V4-Flash-Vision-Exp', inputModalities: ['text', 'image'] },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1081,7 +1089,7 @@ describe('plugin registration and config', () => {
|
||||
// First-boot onboarding: the route registers so models stay discoverable;
|
||||
// only the request itself needs a key.
|
||||
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3)
|
||||
const first = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
expect(first.finish).toMatchObject({ kind: 'error', failure: { code: 'MISSING_CREDENTIAL' } })
|
||||
// The guidance leads with the managed credential store.
|
||||
@@ -1169,7 +1177,7 @@ describe('plugin registration and config', () => {
|
||||
expect(adapter).toBeInstanceOf(DeepSeekAdapter)
|
||||
// Direct embedding shares the plugin's one resolve step, so it advertises
|
||||
// the same default catalog instead of a divergent empty one.
|
||||
await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(2)
|
||||
await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(3)
|
||||
})
|
||||
|
||||
it('resolves connection facts and the credential exactly once per stream call', async () => {
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('request-level dynamic configuration', () => {
|
||||
const dir = await home()
|
||||
const { ctx } = await boot(dir, { baseURL: 'http://127.0.0.1:1' })
|
||||
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3)
|
||||
await ctx.settings.update(NS, {
|
||||
models: [{ id: 'settings-model', name: 'From Settings', inputModalities: ['text', 'image'] }],
|
||||
})
|
||||
@@ -171,10 +171,7 @@ describe('request-level dynamic configuration', () => {
|
||||
{ kind: 'sse', events: textEvents },
|
||||
{ kind: 'sse', events: textEvents },
|
||||
])
|
||||
const { ctx } = await boot(dir, {
|
||||
baseURL: server.url,
|
||||
models: [{ id: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }],
|
||||
})
|
||||
const { ctx } = await boot(dir, { baseURL: server.url })
|
||||
const messages = [createUserMessage({
|
||||
content: [
|
||||
{ type: 'image', attachment: IMAGE_REF },
|
||||
@@ -226,7 +223,7 @@ describe('request-level dynamic configuration', () => {
|
||||
// Schema-valid but resolver-invalid: duplicate catalog ids pass the array
|
||||
// schema and fail the explicit resolve step.
|
||||
await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] })
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(3)
|
||||
await ctx.settings.update(NS, { models: [{ id: 'recovered' }] })
|
||||
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
|
||||
{ provider: 'deepseek-official', id: 'recovered', name: 'recovered', inputModalities: ['text'] },
|
||||
|
||||
Reference in New Issue
Block a user