Fix Messages-only consumer validation and document recovery

This commit is contained in:
Tianyi Cui committed 2026-09-20 14:37:58 +08:00
1 parent 2b29f31dc6
commit 385a540d7e
11 files changed
+37 -13

No files matched your search

@@ -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-09-07-deepseek-messages-adapter.md
2026-09-07-deepseek-messages-adapter.md: e8c84066e2ede5bdaf9e490b4ac0c9562cc8a557
2026-09-07-deepseek-messages-adapter.zh.md: bde7fea769fbc5b4c2e622067b89ff8f29e91fb4
2026-09-07-deepseek-messages-adapter.md: 29f712e57c8048ddb0c05b5f56fd4793c52bec5f
2026-09-07-deepseek-messages-adapter.zh.md: 3cf60fc2101fb835423b74a6532e5edcc5bee911
@@ -6,7 +6,7 @@ English | [中文](2026-09-07-deepseek-messages-adapter.zh.md)
## Problem
Deployments expose DeepSeek through Anthropic Messages gateways as well as chat-completions. Messages represents thinking, signatures, tool calls, tool results, and cumulative usage differently. Translating only the endpoint or flattening assistant history loses information needed by subsequent tool turns.
Messages represents thinking, signatures, tool calls, tool results, and cumulative usage as native protocol fields. Translating only the endpoint or flattening assistant history loses information needed by subsequent tool turns.
## Decision
@@ -24,7 +24,7 @@ System updates use the existing [route capability](2026-09-02-in-history-system-
Web displays DeepSeek with an endpoint and credential reference. Without an endpoint override, resolution uses `https://api.deepseek.com/anthropic`. Overrides must support Messages. The adapter follows the exact `/v1` root rule rather than inferring compatibility from other version-like suffixes. One model catalog advertises the capabilities declared by its entries.
The adapter uses the existing [request-extension registry](../architecture/2026-08-21-deepseek-llm-api-request-extensions.md) after native serialization and accept captured contributions after HTTP 2xx, before reading the stream. Session-log delivery and plugin inventory retain their owners and remain outside model input. The auxiliary [web-search provider](../../../../packages/web/web-search-deepseek/README.md) retains its separate endpoint, request, and settings.
The adapter uses the existing [request-extension registry](../architecture/2026-08-21-deepseek-llm-api-request-extensions.md) after native serialization and accepts captured contributions after HTTP 2xx, before reading the stream. Session-log delivery and plugin inventory retain their owners and remain outside model input. The auxiliary [web-search provider](../../../../packages/web/web-search-deepseek/README.md) retains its separate endpoint, request, and settings.
## Alternatives considered
@@ -6,7 +6,7 @@ Status: implemented
## 问题
部署可以通过 Anthropic Messages 网关或 chat-completions 调用 DeepSeek。Messages 对思考、签名、工具调用、工具结果和累计用量的表示不同。仅替换端点或压平助手历史会丢失后续工具轮次需要的信息。
Messages 以原生协议字段表示思考、签名、工具调用、工具结果和累计用量。仅替换端点或压平助手历史会丢失后续工具轮次需要的信息。
## 决策
+2 -2
View File
@@ -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: 696db921f266ea0a5187aa9b98cf316343b6c74c
README.zh.md: 8e152a93de50dea7fc62cd3638957e6b1a879d23
README.md: e03b1024bf83fa39603c3c2413c3270df70b7bda
README.zh.md: 9f14e175d95ff0cac3e77c972a9bbc940c9d9e9f
+3 -1
View File
@@ -104,6 +104,8 @@ When `ctx.deepseekLlmApiExtensions` is present, the adapter prepares its registe
### Failures and recovery
Configuration accepts Messages only and has no `protocol` field. If resolution reports `protocol is not configurable`, remove `protocol` from the `llm-deepseek` section in `<harness home>/settings.yaml` and from any matching Cordis entry or overlay. Keep the intended `baseURL`, `apiKeyEnv`, and `models` fields. An invalid live section keeps the complete last good configuration; saving other fields in the Models card does not remove an unknown property. Edit the configuration file, then let settings reload or restart the profile; composition edits require a profile restart.
Successful Files responses must contain valid JSON. JSON decoding failures from upload, list, retrieve, and delete throw `INVALID_RESPONSE` with the operation and HTTP status in the message, the status in `LlmError.failure`, and the original parser error as `cause`. Body-read transport and cancellation errors retain their identity.
Non-2xx responses fail with stable codes: `AUTH` (401/403), `QUOTA`, `RATE_LIMIT`, `CONTEXT_WINDOW_EXCEEDED`, `INVALID_REQUEST`, `SERVER`, and `HTTP_<status>` otherwise; pre-response transport failures throw `TRANSPORT`, caller aborts throw `ABORTED`, and stream-idle expiry throws `TIMEOUT`. Request-extension preparation, field collision, or post-2xx acceptance fails with `REQUEST_EXTENSION`. A normalized-image rejection names every plausible attachment and its durable position when the provider does not identify a file id. Stale-file rejection invalidates the named mappings (or every mapping used by the attempt) and permits one replacement model request. Protocol violations throw `STREAM_CLOSED` or `MALFORMED_RESPONSE`, and a terminal `stop` with no content blocks becomes `EMPTY_RESPONSE`, which the default retry policy retries. A request with no key anywhere fails with `MISSING_CREDENTIAL`, and a malformed credential fails with `INVALID_CREDENTIAL` naming the reference to fix — never any part of the key.
@@ -196,7 +198,7 @@ These limits define where the adapter stops and future work begins. They are cur
- **Messages in-history system updates require a retained user or tool-result turn** — if all user input after an update is omitted and the preceding wire turn is assistant, serialization fails with `UNSUPPORTED_CONTENT` before the next assistant or at the end of the request. Text or an empty tool result can retain that turn. Moving the update to an earlier turn is not supported; the [input-history decision](../../../.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md) records the ordering constraint.
- **Images are input-only durable attachments** — direct external URLs and assistant image output are not supported; DeepSeek input normally uses the Files API and uses inline base64 only for per-request recovery.
- The default catalog advertises `deepseek-flash` and its text/image and in-history capabilities without probing gateway availability. Requests can fail with `INVALID_REQUEST` until the gateway enables the id.
- The [Messages system-update e2e checks](tests/adapter.e2e.ts) require `DEEPSEEK_IN_HISTORY_MODEL` to name a supported model, such as `deepseek-flash`, and run with `high` effort. They skip when that variable is unset or empty; ordinary `off` text checks remain enabled with credentials. Known instruction-following instability with thinking disabled makes these system-update checks unsuitable for `off`.
- Real API checks in [adapter.e2e.ts](tests/adapter.e2e.ts) and [runtime.e2e.ts](tests/runtime.e2e.ts) require `DEEPSEEK_API_KEY`. Set `DEEPSEEK_IN_HISTORY_MODEL` to a supported nonempty model id to run system-update checks: the adapter suite uses `high` effort, while the runtime suite compares cache reuse with thinking disabled and can be sensitive to instruction-following instability. The runtime image cases additionally require `DEEPSEEK_FLASH_E2E=1` or `DEEPSEEK_VISION_E2E=1`.
<a id="dev-note"></a>
### Dev Note
+3 -1
View File
@@ -104,6 +104,8 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留
### 失败与恢复
配置仅接受 Messages,不提供 `protocol` 字段。若解析报告 `protocol is not configurable`,请从 `<harness home>/settings.yaml` 的 `llm-deepseek` 分节以及对应 Cordis 条目或 overlay 中删除 `protocol`,保留需要的 `baseURL`、`apiKeyEnv` 和 `models` 字段。无效的实时分节会继续使用完整的最后有效配置;在模型设置卡中保存其他字段不会移除未知属性。请编辑配置文件,等待设置重新加载或重启 profile;组装配置变更需要重启 profile。
成功的 Files 响应必须包含有效 JSON。上传、列举、获取和删除操作的 JSON 解码失败抛出 `INVALID_RESPONSE`,消息包含操作名称与 HTTP 状态,`LlmError.failure` 保留该状态,`cause` 保留原始解析错误。读取响应体时的传输和取消错误保留其原有身份。
非 2xx 响应以稳定 code 失败:`AUTH`(401/403)、`QUOTA`、`RATE_LIMIT`、`CONTEXT_WINDOW_EXCEEDED`、`INVALID_REQUEST`、`SERVER` 以及其他情况的 `HTTP_<status>`;响应前传输失败抛出 `TRANSPORT`,调用方中止抛出 `ABORTED`,流空闲超时抛出 `TIMEOUT`。请求扩展准备、字段冲突或 2xx 后接受失败使用 `REQUEST_EXTENSION`。当提供方未指出 file id 时,规范化图片拒绝会列出所有可能附件及其持久位置。陈旧文件拒绝会使点名映射(或该次尝试使用的全部映射)失效,并允许一次替换模型请求。协议违规抛出 `STREAM_CLOSED` 或 `MALFORMED_RESPONSE`;不带内容块的终止 `stop` 变成 `EMPTY_RESPONSE`,默认重试策略会重试它。任何位置都没有密钥的请求以 `MISSING_CREDENTIAL` 失败;格式错误的凭据以 `INVALID_CREDENTIAL` 失败,并点名需要修复的引用——绝不包含密钥的任何部分。
@@ -196,7 +198,7 @@ loop 保留的响应块会追加到下一个请求,并保留其更早的可复
- **Messages 历史内 system 更新需要保留用户或工具结果轮次**——若更新后的全部用户输入都被省略,且前一个协议轮次是 assistant,序列化会在下一个 assistant 之前或请求结束处以 `UNSUPPORTED_CONTENT` 失败。文本或空工具结果可以保留该轮次。不支持将更新移到更早的轮次;[输入历史决策](../../../.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.zh.md)记录了排序约束。
- **图片是仅用于输入的持久附件**——不支持直接外部 URL 与 assistant 图片输出;DeepSeek 输入通常使用 Files API,仅在单次请求恢复时使用内联 base64。
- 默认目录公布 `deepseek-flash` 及其文本、图片和历史内更新能力,不探测网关可用性。网关开放该 ID 前,请求可能以 `INVALID_REQUEST` 失败。
- [Messages system 更新 e2e](tests/adapter.e2e.ts) 要求通过 `DEEPSEEK_IN_HISTORY_MODEL` 指定支持该能力的模型,例如 `deepseek-flash`,并使用 `high` 思考强度。该变量未设置或为空时跳过;普通 `off` 文本检查仍在有凭据时运行。关闭思考时已知的指令遵循不稳定,使这些 system 更新检查不适合使用 `off`。
- [adapter.e2e.ts](tests/adapter.e2e.ts) 与 [runtime.e2e.ts](tests/runtime.e2e.ts) 中的真实 API 检查需要 `DEEPSEEK_API_KEY`。将 `DEEPSEEK_IN_HISTORY_MODEL` 设为受支持的非空模型 ID 可运行 system 更新检查:适配器套件使用 `high` 思考强度,运行时套件则在关闭思考时比较缓存复用,可能受到指令遵循不稳定的影响。运行时图片用例还需要 `DEEPSEEK_FLASH_E2E=1` 或 `DEEPSEEK_VISION_E2E=1`。
<a id="dev-note"></a>
### 开发备注
-1
View File
@@ -47,7 +47,6 @@ export type { DeepSeekFileId as DeepSeekFileIdType } from './file-id.ts'
export { DeepSeekUploadIndex, deepSeekFileScope } from './upload-index.ts'
export type { DeepSeekUploadRecord } from './upload-index.ts'
export type { RequestDefaults } from './types.ts'
export type * from './wire-types.ts'
export const name = 'llm-deepseek'
export const inject = ['llm']
@@ -312,6 +312,28 @@ describe('request-level dynamic configuration', () => {
expect(good.headers[0]?.['x-api-key']).toBe('good-key')
})
it.each(['messages', 'chat-completions'])('rejects a persisted protocol=%s section and recovers after removing the field', async (protocol) => {
const dir = await home()
const composition = await mockServer([{ kind: 'sse', events: textEvents }])
const repaired = await mockServer([{ kind: 'sse', events: textEvents }])
vi.stubEnv('DEEPSEEK_API_KEY', 'composition-key')
vi.stubEnv('REPAIRED_DEEPSEEK_KEY', 'repaired-key')
const section = { baseURL: repaired.url, apiKeyEnv: 'REPAIRED_DEEPSEEK_KEY' }
await writeFile(join(dir, 'settings.yaml'), JSON.stringify({ [NS]: { ...section, protocol } }))
const { ctx } = await boot(dir, { baseURL: composition.url })
await prompt(ctx)
expect(composition.requests).toHaveLength(1)
expect(composition.headers[0]?.['x-api-key']).toBe('composition-key')
expect(repaired.requests).toHaveLength(0)
await ctx.settings.replace(NS, section)
await prompt(ctx)
expect(repaired.requests).toHaveLength(1)
expect(repaired.headers[0]?.['x-api-key']).toBe('repaired-key')
expect(ctx.settings.get(NS)).not.toHaveProperty('protocol')
})
it('falls back to the composition entry when settings detach', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', '')
const dir = await home()
@@ -34,7 +34,7 @@ import { assemble, type AssembledResult } from './assemble.ts'
* and a max-effort tool round trip with reasoning passback. The suite skips
* entirely without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally
* requires $DEEPSEEK_VISION_E2E=1, and the Flash image/system-update smoke
* requires $DEEPSEEK_FLASH_E2E=1 (see vitest.e2e.config.ts).
* requires $DEEPSEEK_FLASH_E2E=1.
*/
const FLASH = 'deepseek-v4-flash'
@@ -24,7 +24,6 @@ async function harness(_model: string, config: Partial<PiAiProviderProfile> = {}
providers: {
deepseek: {
...process.env.DEEPSEEK_API_KEY === undefined ? {} : { apiKey: process.env.DEEPSEEK_API_KEY },
baseURL: LlmDeepSeek.PUBLIC_BASE_URL,
...config,
},
},
@@ -628,7 +628,7 @@ function chooseRandomBehavior(
/**
* Start a local Messages server that consumes one configured behavior
* per accepted request. Only a `POST` path ending in `/v1/messages` consumes the script;
* invalid routes, methods, authorization, and JSON receive ordinary 4xx
* invalid routes, methods, API keys, and JSON receive ordinary 4xx
* responses. Closing the handle terminates stalled connections.
*
* @param options - listener, script, response content, timing, and telemetry options.