Files
deepseek-harness/.agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.zh.md
T
Yichen Jiang c48f8523f2 feat(llm): follow the official /models listing on the DeepSeek API-key route (#6944)
* refactor(llm-deepseek): share the /models listing machinery

Move the account route's `/models` request and decoding, its
credential-scoped listing cache, and listed-catalog resolution with the
version gate into the shared DeepSeek Messages library, so a second
route can follow the same listing. Names drop the account prefix, the
cache takes each access's endpoint settings with the call, and it gains
`invalidate()` for a forced reload. `isBuiltInCatalog()` recognizes the
catalog an uncustomized `models` setting resolves to.

The account route keeps its behavior; only its imports and the
override diagnostic wording ("model listing") change.

* feat(llm-deepseek-api-key): follow the official /models listing

While `models` equals the built-in catalog and the Messages origin is
https://api.deepseek.com, `deepseek-official` serves the official
`GET /models` listing, authenticated with the API key in `x-api-key`.
Loading, refresh, fallback, merging, and the client-version gate match
the account route: the first access waits up to `modelsTimeoutMs`, a
listing older than `modelsMaxAgeMs` refreshes in the background on the
next access, a failure keeps the last good listing or the built-in
catalog, and listed models this build cannot serve are refused before
network I/O.

A customized `models` list replaces the catalog and requests nothing.
Switching between a customized catalog and the listing discards the
cached listing, so Restore defaults reloads it on the next access.

Suites that boot the route on the official endpoint now answer the
listing themselves instead of reaching the network.

* docs: describe the official route model listing

Document when `deepseek-official` follows the official `/models`
listing, its timing fields, customization precedence, and the forced
reload after Restore defaults in the API-key README, the shared
transport README, and the provider guide. Record the decision in a new
Agent Note, keep the account note's statement of where the shared
listing code lives current, and regenerate the configuration catalog.

* test(llm-deepseek-api-key): pin the once-per-listing hidden-entry diagnostic

* docs: regenerate module and event graphs for the shared listing

* fix(llm-deepseek-api-key): report credential failures on the model request

On the official endpoint, `connect` waited for the listing view, whose
credential lookup rethrew a malformed key or a credential-store failure
before the agent loop recorded the prompt. A failed lookup now resolves
the connection from the current listing view, so `resolveAuth` reports
the failure on the model request and the turn keeps the prompt and the
failed attempt. A caller abort still rejects.

* docs(llm-deepseek-api-key): name which hidden listing entries are logged

* test: keep unit suites off the official model listing

Suites that boot the DeepSeek routes with a dummy or inherited key now
request `GET https://api.deepseek.com/models` on the official endpoint.
A unit-lane setup file rejects that request at once, so such a suite
serves the built-in catalog instead of reaching production or waiting
out the listing deadline on a network that never answers; suites that
exercise the listing answer `fetch` themselves. Other requests pass
through unchanged.

* feat(llm-deepseek): read tool_update from the model listing

Both DeepSeek routes now take a listed model's Messages tool update mode
from `api_capabilities.anthropic_messages.tool_update`, alongside the
system prompt mode. `in-history` and `addition-only` replace the
bundled mode; any other value reads as `leading-only` and removes it.
An absent or `null` mode keeps the bundled one, so a listing that does
not declare the field leaves `deepseek-flash` on `addition-only`.

`system_prompt_update: null` likewise keeps the bundled mode now; it
previously read as `leading-only`, which a serializer writing empty
values as null would have turned into a cache-unfriendly downgrade.

* fix(llm-deepseek): sync the built-in context window with the official listing

The official `/models` listing declares a 1,048,576-token context window
for `deepseek-flash` and `deepseek-v4-pro`; the built-in catalog said
1,000,000, a decimal-versus-binary mismatch. Both built-in entries now
carry the listed value, so offline starts and listing failures size the
context the same way the listing does. The Models page shows the exact
count for these rows, since 1,048,576 has no decimal K/M spelling.

`defaultContextWindow`, the fallback for model ids the catalog does not
describe, stays 1,000,000.

* test(llm-deepseek-api-key): reject the stubbed listing request with an Error

* test(cli): keep the built SDK smoke off the official model listing

The SDK profile's initialize now resolves the deepseek-official catalog,
which requests GET https://api.deepseek.com/models on the official
endpoint. With the smoke's fixture key that request answers 401 and the
route warns on stderr, so the test's empty-stderr assertion failed in the
e2e and snapshots lanes. Point the run at a loopback mock and assert it
received no model call.

* test(llm-deepseek): compose the malformed-key turn without the native executor

release/dsh-0.2.1 predates @deepseek-ai/dsh-tool-executor-native; its tool
runtime executes tools itself. Compose the official-route turn the way
account-routing.spec.ts does on this branch.

* fix(llm-deepseek-api-key): gate nothing when the credential lookup fails

The listing cache switches scope only after it reads a credential, so a
failed lookup left `connect` serving the previous credential's listing.
After a key whose listing refuses a model was replaced by a malformed one,
`admit` refused that model with CLIENT_UPGRADE_REQUIRED before the turn
recorded the prompt, instead of `resolveAuth` reporting the credential
error on the request. Serve the built-in catalog without listing gates
when the lookup fails.

* test(llm-deepseek-api-key): pin the once-per-listing diagnostic across request preparation

The hidden-entry diagnostic test repeated the same discovery assertion to
prove a second pass reports nothing new, which read as a duplicate. Assert
the single warning after the request preparations instead; they resolve
the same listing, so dropping the per-listing deduplication still fails
the test.

* docs(llm-deepseek-api-key): note that a list matching an earlier built-in catalog counts as customized

Customization is judged against the running version's built-in catalog,
so a saved `models` list equal to an earlier version's catalog stops
following the listing once the built-in catalog changes, until Restore
defaults. Record the consequence in the Agent Note and the README's known
limitations.
2026-10-22 20:42:19 +08:00

3.6 KiB

Agent Note: DeepSeek 账号模型来自账号列表,并按客户端版本门控

Status: implemented

English | 中文

问题

账号路由的目录原本是部署配置:每个构建只提供它发布时自带的模型,新模型只能随发版到达用户,也没有办法阻止旧构建请求它驱动不了的模型。账号服务在 GET /models 上按账号发布模型元数据,这份列表可能包含旧构建不认识的模型。

决策

  • llm-deepseek-account 按凭据懒加载列表,首次访问最多等待 modelsTimeoutMs,超过 modelsMaxAgeMs 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。
  • 列出的模型在其 client_requirements["deepseek-harness"].min_version 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 CLIENT_UPGRADE_REQUIRED 或 MODEL_UNSUPPORTED 失败。
  • 列表中的能力逐字段覆盖内置能力;内置条目保留列表没有声明的客户端知识,例如图片预算,以及列表没有给出或设为 null 的协议模式;modelOverrides 最后叠加,复用 models 条目的字段与校验。max_output_tokens 只限制请求默认值,不直接成为默认值。
  • 共享传输层只新增 connect 与 admit 两个提供方钩子。列表、门控与合并位于共享传输包中,官方路由也使用它们。

考虑过的替代方案

只在服务端按客户端版本过滤。 Codex 发送 client_version,由后端去掉模型。这样旧构建无法提示新版本支持某个模型,而且列表漏掉声明时会暴露未知模型。

未知模型套用默认参数。 Reasonix 和 Pi 用通用默认值驱动未知 id,MiniMax 把这种做法当作缺陷。需要客户端改动的模型会以错误的方式运行。

整份列表严格校验。 Codex、ZCode 和 Kimi 会因为一个坏字段拒绝整份列表。一条格式错误的记录就会让所有账号退回内置目录。

持久化上一次成功的列表。 持久缓存要新增持久化格式并承担迁移责任;内置目录已经覆盖离线启动。

保留 models 并改为按 id 合并。 API key 路由的 models 会替换整个目录,同名字段在兄弟路由上改成合并语义会让含义相反。

扩展共享传输层。 按模型的推理档位、输出能力字段和目录变更通知,会为一个使用方扩大 Messages 包的范围。列表中的推理档位与内置默认值一致;需要不同档位的模型需要发版适配,门控正是用来保证这一点。

影响

  • 旧构建永远不会提供它不认识的模型,除非服务端声明了该构建满足的最低版本;服务端必须声明 min_version,将来的构建才能提示升级后可用。
  • 改过的客户端仍然可以请求任意 id;只有服务端自己拒绝这类请求时,版本下限才会被强制执行。
  • 登录后的第一次账号访问最多等待 modelsTimeoutMs;离线启动在加载到列表之前使用内置能力;进程运行中途的列表变化要等 Web 选择器下一次刷新目录时才会显示。