From 226600147e4a14e61bacfe3804d51b2125292df4 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 14 Aug 2026 11:47:59 +0800 Subject: [PATCH] feat(llm-deepseek): support low reasoning effort --- ...r-owned-reasoning-effort-capabilities.i18n.yaml | 4 ++-- ...-adapter-owned-reasoning-effort-capabilities.md | 2 +- ...apter-owned-reasoning-effort-capabilities.zh.md | 2 +- docs/config-catalog.i18n.yaml | 4 ++-- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../tests/fs-diff-bound.cordis.snapshot.yml | 2 +- .../tests/fixtures/deepseek-defaults.cordis.yml | 3 ++- examples/headless-agent/tests/headless.snapshot.ts | 3 ++- .../tests/components.client.spec.tsx | 2 +- .../tests/onboarding-dialog.client.spec.tsx | 2 +- packages/llm/llm-deepseek/README.i18n.yaml | 4 ++-- packages/llm/llm-deepseek/README.md | 6 +++--- packages/llm/llm-deepseek/README.zh.md | 6 +++--- packages/llm/llm-deepseek/src/adapter.ts | 10 +++++++--- packages/llm/llm-deepseek/src/index.ts | 4 ++-- packages/llm/llm-deepseek/src/serialize.ts | 12 ++++++------ packages/llm/llm-deepseek/src/types.ts | 4 ++-- packages/llm/llm-deepseek/tests/adapter.e2e.ts | 6 +++--- packages/llm/llm-deepseek/tests/adapter.spec.ts | 14 ++++++++------ packages/llm/llm-deepseek/tests/serialize.spec.ts | 6 +++--- 21 files changed, 54 insertions(+), 46 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.i18n.yaml index 7e8eb5c17d..48967a6faa 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.i18n.yaml @@ -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-24-adapter-owned-reasoning-effort-capabilities.md -2026-07-24-adapter-owned-reasoning-effort-capabilities.md: f71a15626b97090a89f071e552a12b1f89173620 -2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md: 6e8c99f98107e2855aa7cb7ea759544013d0ea8a +2026-07-24-adapter-owned-reasoning-effort-capabilities.md: f59e5d351970e785e6ade32797f1c1a5dd9e2591 +2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md: 6e657a90e8ef3cd400a3e252f728bb1aadd9f4f2 diff --git a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.md b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.md index f71a15626b..f59e5d3519 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.md +++ b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.md @@ -14,7 +14,7 @@ Reasoning strength was adapter configuration only, so a conversation could not d `LlmCallConfig` and `GenerateOptions` carry the optional effort. The agent loop prepares the post-`agent/request` config under the active turn signal before writing `request/header`, so defaults and dynamic changes are model-visible only after becoming durable facts. The prepared call retains the exact adapter registration across asynchronous exact-model resolution, durable header logging, and dispatch; direct `LlmRuntime.stream()` calls likewise capture their final registration before awaiting resolution. A route with no registered adapter retains its proposed config so an `llm/stream` middleware can own and short-circuit it; terminal dispatch still rejects an unhandled route. A resumed loop retains the logged effort only when its initial provider/model route is unchanged; a route change discards the previous model's opaque id. -The native DeepSeek adapter advertises `off`, `high`, and `max` when deployment policy permits thinking, and defaults to the configured effort or `high`. Its adapter-owned `off` maps to `thinking.type: disabled` with no `reasoning_effort`; `high` and `max` enable thinking and carry their official wire effort. A `thinking: disabled` deployment publishes only `off` and rejects attempts to enable thinking before provider I/O. The pi-ai adapter publishes each exact model's `getSupportedThinkingLevels()` result unchanged, including `off`, preserves an absent profile default as a provider default, and leaves provider wire-value mapping inside pi-ai. Its common stream options represent `off` by omitting `reasoning`, as required by pi-ai's own API. +The native DeepSeek adapter advertises `off`, `low`, `high`, and `max` when deployment policy permits thinking, and defaults to the configured effort or `high`. Its adapter-owned `off` maps to `thinking.type: disabled` with no `reasoning_effort`; `low`, `high`, and `max` enable thinking and carry their same-named official wire effort. A `thinking: disabled` deployment publishes only `off` and rejects attempts to enable thinking before provider I/O. The pi-ai adapter publishes each exact model's `getSupportedThinkingLevels()` result unchanged, including `off`, preserves an absent profile default as a provider default, and leaves provider wire-value mapping inside pi-ai. Its common stream options represent `off` by omitting `reasoning`, as required by pi-ai's own API. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md index 6e8c99f981..6e657a90e8 100644 --- a/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md @@ -14,7 +14,7 @@ Status: implemented `LlmCallConfig` 和 `GenerateOptions` 携带可选的推理强度。agent loop 在活跃轮次信号的控制下准备 `agent/request` 处理完成后的配置,再写入 `request/header`,因此默认值和动态变更只有成为持久化事实后才对模型可见。准备完成的调用在异步确切模型解析、请求头持久记录和分派全程保留同一项确切的适配器注册;直接调用 `LlmRuntime.stream()` 时,也会在等待解析前捕获最终的适配器注册。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 中间件可以接管并短路该请求;若仍未得到处理,最终分发会拒绝该路由。恢复后的 agent loop 仅在初始提供方/模型路由未变时保留日志中记录的推理强度;如果路由发生变化,则丢弃上一模型的不透明 ID。 -当部署策略允许思考时,原生 DeepSeek 适配器声明 `off`、`high` 和 `max`,默认使用配置指定的推理强度,若未配置则使用 `high`。由适配器持有的 `off` 映射为 `thinking.type: disabled`,且不带 `reasoning_effort`;`high` 和 `max` 会启用思考并携带各自的官方协议强度值。配置为 `thinking: disabled` 的部署仅声明 `off`,并会在提供方 I/O 前拒绝启用思考的尝试。pi-ai 适配器原样发布每个确切模型的 `getSupportedThinkingLevels()` 结果,其中包括 `off`;profile 未指定默认值时保留提供方默认行为,并将提供方协议值的映射留在 pi-ai 内部。按照 pi-ai 自身 API 的要求,其通用流选项通过省略 `reasoning` 来表示 `off`。 +当部署策略允许思考时,原生 DeepSeek 适配器声明 `off`、`low`、`high` 和 `max`,默认使用配置指定的推理强度,若未配置则使用 `high`。由适配器持有的 `off` 映射为 `thinking.type: disabled`,且不带 `reasoning_effort`;`low`、`high` 和 `max` 会启用思考并携带各自的同名官方协议强度值。配置为 `thinking: disabled` 的部署仅声明 `off`,并会在提供方 I/O 前拒绝启用思考的尝试。pi-ai 适配器原样发布每个确切模型的 `getSupportedThinkingLevels()` 结果,其中包括 `off`;profile 未指定默认值时保留提供方默认行为,并将提供方协议值的映射留在 pi-ai 内部。按照 pi-ai 自身 API 的要求,其通用流选项通过省略 `reasoning` 来表示 `off`。 ## 备选方案 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index f5a845ad5f..ef4931f765 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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: 20919b3fdc5ab26255465949d72bdce8d356a529 -config-catalog.zh.md: 8dfb49df5e3f5906af859a4de83ebc44f315a0bd +config-catalog.md: 82f6d26c79d32c6952f3bc11c96fa1c2ddceecdc +config-catalog.zh.md: 958d3115447db37de248bbf30b0744308ff8dbb8 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 20919b3fdc..82f6d26c79 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -859,7 +859,7 @@ export interface Config { /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */ thinking?: 'enabled' | 'disabled' /** Default thinking effort (default `high`); `off` disables thinking per request. */ - reasoningEffort?: 'off' | 'high' | 'max' + reasoningEffort?: 'off' | 'low' | 'high' | 'max' /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */ maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 8dfb49df5e..958d311544 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -861,7 +861,7 @@ export interface Config { /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */ thinking?: 'enabled' | 'disabled' /** Default thinking effort (default `high`); `off` disables thinking per request. */ - reasoningEffort?: 'off' | 'high' | 'max' + reasoningEffort?: 'off' | 'low' | 'high' | 'max' /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */ maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ diff --git a/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml b/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml index 4cc6ba1f3e..ac89b962eb 100644 --- a/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml +++ b/examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml @@ -41,6 +41,6 @@ - id: deepseek-v4-flash contextWindow: 1000000 defaultMaxTokens: 256000 - reasoningEfforts: ['off', 'high', 'max'] + reasoningEfforts: ['off', 'low', 'high', 'max'] defaultReasoningEffort: max - id: deepseek-v4-pro diff --git a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml index fab8ae1b7b..7f1c0b3e9e 100644 --- a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml +++ b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml @@ -6,7 +6,8 @@ - id: llm-deepseek config: baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL - thinking: disabled + thinking: enabled + reasoningEffort: low streamIdleTimeoutMs: 150 - id: agent-spine config: diff --git a/examples/headless-agent/tests/headless.snapshot.ts b/examples/headless-agent/tests/headless.snapshot.ts index 4f2eb28d40..d22a48a7ab 100644 --- a/examples/headless-agent/tests/headless.snapshot.ts +++ b/examples/headless-agent/tests/headless.snapshot.ts @@ -541,6 +541,7 @@ describe('headless stream-json snapshots', () => { expect(result.stderr).toBe('') expect(server.requests).toHaveLength(1) expect(server.requests[0]?.max_tokens).toBe(256_000) + expect(server.requests[0]?.reasoning_effort).toBe('low') const header = (parseJsonl(result.stdout) .map(record => record.event) .find((event): event is JsonObject => ( @@ -555,7 +556,7 @@ describe('headless stream-json snapshots', () => { "maxTokens": 256000, "model": "deepseek-v4-flash", "provider": "deepseek-official", - "reasoningEffort": "off", + "reasoningEffort": "low", } `) expect(header?.adapterDefaults).toEqual({ diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index d7643522dd..66ba8d33a4 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -48,7 +48,7 @@ const PiAiConfig = Schema.object({ const DeepSeekConfig = Schema.object({ apiKeyEnv: Schema.string().role('credential-ref'), baseURL: Schema.string().pattern(/^https:\/\//), - reasoningEffort: Schema.union(['off', 'high', 'max']), + reasoningEffort: Schema.union(['off', 'low', 'high', 'max']), defaultContextWindow: Schema.number().step(1).min(1), models: Schema.array(Schema.object({ id: Schema.string().required(), diff --git a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx index 192b57bf50..ebb5992cfe 100644 --- a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx @@ -29,7 +29,7 @@ function fail(message: string): RpcResponse { const DeepSeekConfig = Schema.object({ apiKeyEnv: Schema.string().role('credential-ref'), baseURL: Schema.string().pattern(/^https:\/\//), - reasoningEffort: Schema.union(['off', 'high', 'max']), + reasoningEffort: Schema.union(['off', 'low', 'high', 'max']), defaultContextWindow: Schema.number().step(1).min(1), models: Schema.array(Schema.object({ id: Schema.string().required(), diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 9f1f57ef90..4ac4cf4ed0 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -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: 1923be694c20149fe5387bd7bb4dac5da32a63a4 -README.zh.md: bdce7f2b2fdff0e1542deb3e7fae5c5e7139d13b +README.md: af89e7ee3bab6ec209349d047f81308eb6e87cef +README.zh.md: e9ce3206027ffeee9bc49eb7a2ed76ddcbe7bfc8 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 1923be694c..af89e7ee3b 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -17,7 +17,7 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire apiKeyEnv: DEEPSEEK_API_KEY # default; resolved per request via ctx.credentials, then the environment baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted thinking: enabled # optional; provider default is enabled - reasoningEffort: high # optional; off | high | max — omitted ⇒ high + reasoningEffort: high # optional; off | low | high | max — omitted ⇒ high maxTokens: 256000 # optional positive per-request output cap; this is the default streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default retryPolicy: # optional; omission uses bounded normal defaults @@ -41,9 +41,9 @@ The plugin registers the single provider route `deepseek-official` together with `maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmRuntime` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`. -The same exact-model result exposes ordered `off`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `high` and `max` enable thinking and serialize as the official top-level `reasoning_effort`; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O. +The same exact-model result exposes ordered `off`, `low`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `low`, `high`, and `max` enable thinking and serialize as the same official top-level `reasoning_effort` value; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O. -`thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `high` or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults. +`thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `low`, `high`, or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults. `streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. DeepSeek SSE comments rearm an outstanding read as transport activity but never become `StreamChunk` values or session-log events. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers the configured policy as provider metadata, and `dsh-llm-retry` separately executes it at durable agent-step boundaries. diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index bdce7f2b2f..e9ce320602 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -17,7 +17,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: apiKeyEnv: DEEPSEEK_API_KEY # default; resolved per request via ctx.credentials, then the environment baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted thinking: enabled # optional; provider default is enabled - reasoningEffort: high # optional; off | high | max — omitted ⇒ high + reasoningEffort: high # optional; off | low | high | max — omitted ⇒ high maxTokens: 256000 # optional positive per-request output cap; this is the default streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default retryPolicy: # optional; omission uses bounded normal defaults @@ -41,9 +41,9 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: `maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmRuntime` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。 -同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`high` 和 `max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。 +同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`low`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`low`、`high` 和 `max` 会启用思考,并以同名值序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。 -`thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `high` 或 `max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。 +`thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `low`、`high` 或 `max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。 `streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它把已配置策略注册为提供方元数据,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。 diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 5fa62d30ff..bc30d46c3b 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -93,10 +93,12 @@ export const DEFAULT_CONTEXT_WINDOW = 1_000_000 export const DEFAULT_MAX_TOKENS = 256_000 const STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT' const OFF_REASONING_EFFORT = ReasoningEffortId('off') +const LOW_REASONING_EFFORT = ReasoningEffortId('low') const HIGH_REASONING_EFFORT = ReasoningEffortId('high') const MAX_REASONING_EFFORT = ReasoningEffortId('max') const REASONING_EFFORTS = [ { id: OFF_REASONING_EFFORT, name: 'Off' }, + { id: LOW_REASONING_EFFORT, name: 'Low' }, { id: HIGH_REASONING_EFFORT, name: 'High' }, { id: MAX_REASONING_EFFORT, name: 'Max' }, ] as const @@ -203,9 +205,11 @@ export class DeepSeekAdapter extends LlmAdapter { efforts: REASONING_EFFORTS, defaultEffort: connection.defaults.reasoningEffort === 'off' ? OFF_REASONING_EFFORT - : connection.defaults.reasoningEffort === 'max' - ? MAX_REASONING_EFFORT - : HIGH_REASONING_EFFORT, + : connection.defaults.reasoningEffort === 'low' + ? LOW_REASONING_EFFORT + : connection.defaults.reasoningEffort === 'max' + ? MAX_REASONING_EFFORT + : HIGH_REASONING_EFFORT, }, }, }) diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 8d01d9d6bc..a7b22ddd88 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -67,7 +67,7 @@ export interface Config { /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */ thinking?: 'enabled' | 'disabled' /** Default thinking effort (default `high`); `off` disables thinking per request. */ - reasoningEffort?: 'off' | 'high' | 'max' + reasoningEffort?: 'off' | 'low' | 'high' | 'max' /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */ maxTokens?: number /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */ @@ -92,7 +92,7 @@ export const Config: z = z.object({ apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV), baseURL: z.string(), thinking: z.union(['enabled', 'disabled']), - reasoningEffort: z.union(['off', 'high', 'max']), + reasoningEffort: z.union(['off', 'low', 'high', 'max']), maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS), defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW), models: z.array(catalogModel).default(DEFAULT_MODELS), diff --git a/packages/llm/llm-deepseek/src/serialize.ts b/packages/llm/llm-deepseek/src/serialize.ts index 34fa214bb9..f51ef23031 100644 --- a/packages/llm/llm-deepseek/src/serialize.ts +++ b/packages/llm/llm-deepseek/src/serialize.ts @@ -14,18 +14,18 @@ import type { WireMessage, WireRequest, WireTool } from './types.ts' /** Adapter-level request defaults (from plugin config). */ export interface RequestDefaults { thinking?: 'enabled' | 'disabled' | undefined - reasoningEffort?: 'off' | 'high' | 'max' | undefined + reasoningEffort?: 'off' | 'low' | 'high' | 'max' | undefined } interface ResolvedThinking { thinking?: 'enabled' | 'disabled' - reasoningEffort?: 'high' | 'max' + reasoningEffort?: 'low' | 'high' | 'max' } /** Validate the adapter-owned effort before resolving its DeepSeek wire fields. */ -function reasoningEffort(effort: NonNullable): 'off' | 'high' | 'max' { - if (effort === 'off' || effort === 'high' || effort === 'max') { - return effort as 'off' | 'high' | 'max' +function reasoningEffort(effort: NonNullable): 'off' | 'low' | 'high' | 'max' { + if (effort === 'off' || effort === 'low' || effort === 'high' || effort === 'max') { + return effort as 'off' | 'low' | 'high' | 'max' } throw new LlmError( `DeepSeek does not support reasoning effort "${effort}"`, @@ -46,7 +46,7 @@ function resolveThinking(options: GenerateOptions, defaults: RequestDefaults): R ) } if (effort === 'off') return { thinking: 'disabled' } - if (effort === 'high' || effort === 'max') { + if (effort === 'low' || effort === 'high' || effort === 'max') { return { thinking: 'enabled', reasoningEffort: effort } } return defaults.thinking === undefined ? {} : { thinking: defaults.thinking } diff --git a/packages/llm/llm-deepseek/src/types.ts b/packages/llm/llm-deepseek/src/types.ts index a9d7403a35..ec3f28cbf4 100644 --- a/packages/llm/llm-deepseek/src/types.ts +++ b/packages/llm/llm-deepseek/src/types.ts @@ -17,8 +17,8 @@ export interface WireRequest { stream_options: { include_usage: true } /** Thinking-mode toggle (top level, NOT inside extra_body on the wire). */ thinking?: { type: 'enabled' | 'disabled' } - /** Thinking effort (official levels; low/medium map to high server-side). */ - reasoning_effort?: 'high' | 'max' + /** Thinking effort (official levels). */ + reasoning_effort?: 'low' | 'high' | 'max' tools?: WireTool[] temperature?: number max_tokens?: number diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index 269169c73a..19ce411406 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -12,7 +12,7 @@ import { assemble, type AssembledResult } from './assemble.ts' /** * Real-API e2e for the direct-fetch adapter: V4 Flash + V4 Pro across - * thinking modes and both official effort levels. Key-gated — skips + * thinking modes and all official effort levels. Key-gated — skips * entirely without $DEEPSEEK_API_KEY (see vitest.e2e.config.ts). */ @@ -95,7 +95,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () } }) - it('flash dynamically switches from off to high', async () => { + it('flash dynamically switches from off to low', async () => { const ctx = await harness(FLASH, { reasoningEffort: 'off' }) const withoutThinking = await assemble(ctx,{ model: FLASH, @@ -110,7 +110,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () const withThinking = await assemble(ctx,{ model: FLASH, - reasoningEffort: ReasoningEffortId('high'), + reasoningEffort: ReasoningEffortId('low'), messages: ask('Which is larger, 9.11 or 9.8? Answer with just the number.'), maxTokens: 2000, }) diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 387abf583f..ce56635ee7 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -141,13 +141,13 @@ describe('DeepSeekAdapter against a mock server', () => { expect(server.headers[0]?.['x-deepseek-harness-compact']).toBe('1') }) - it('switches dynamically from the configured high default through off to max', async () => { + it('switches dynamically from the configured low default through off to max', async () => { const server = await mockServer([ { kind: 'sse', events: textEvents }, { kind: 'sse', events: textEvents }, { kind: 'sse', events: textEvents }, ]) - const ctx = await harness(server.url, { thinking: 'enabled', reasoningEffort: 'high' }) + const ctx = await harness(server.url, { thinking: 'enabled', reasoningEffort: 'low' }) await assemble(ctx,{ model: 'deepseek-v4-flash', @@ -174,7 +174,7 @@ describe('DeepSeekAdapter against a mock server', () => { }) expect(server.requests[0]).toMatchObject({ thinking: { type: 'enabled' }, - reasoning_effort: 'high', + reasoning_effort: 'low', }) expect(server.requests[1]).toMatchObject({ thinking: { type: 'disabled' }, @@ -671,6 +671,7 @@ describe('plugin registration and config', () => { reasoning: { efforts: [ { id: ReasoningEffortId('off'), name: 'Off' }, + { id: ReasoningEffortId('low'), name: 'Low' }, { id: ReasoningEffortId('high'), name: 'High' }, { id: ReasoningEffortId('max'), name: 'Max' }, ], @@ -679,7 +680,7 @@ describe('plugin registration and config', () => { }) }) - it.each(['off', 'max'] as const)('uses the configured %s reasoning default', async (effort) => { + it.each(['off', 'low', 'max'] as const)('uses the configured %s reasoning default', async (effort) => { const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(LlmDeepSeek, { @@ -691,6 +692,7 @@ describe('plugin registration and config', () => { reasoning: { efforts: [ { id: ReasoningEffortId('off'), name: 'Off' }, + { id: ReasoningEffortId('low'), name: 'Low' }, { id: ReasoningEffortId('high'), name: 'High' }, { id: ReasoningEffortId('max'), name: 'Max' }, ], @@ -716,7 +718,7 @@ describe('plugin registration and config', () => { }) }) - it.each(['high', 'max'] as const)( + it.each(['low', 'high', 'max'] as const)( 'rejects configured reasoning effort %s when thinking is disabled', async (reasoningEffort) => { const ctx = new Context() @@ -730,7 +732,7 @@ describe('plugin registration and config', () => { }, ) - it.each(['high', 'max'] as const)( + it.each(['low', 'high', 'max'] as const)( 'rejects disabled-thinking effort %s at the resolver boundary', (reasoningEffort) => { expect(() => resolveAdapterOptions({ thinking: 'disabled', reasoningEffort })) diff --git a/packages/llm/llm-deepseek/tests/serialize.spec.ts b/packages/llm/llm-deepseek/tests/serialize.spec.ts index e7e95703fb..714cfc5d27 100644 --- a/packages/llm/llm-deepseek/tests/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/serialize.spec.ts @@ -203,13 +203,13 @@ describe('serializeRequest', () => { expect(wire.tools).toBeUndefined() }) - it('maps adapter-default thinking and the request reasoning effort', () => { + it.each(['low', 'high', 'max'] as const)('maps adapter-default thinking and request effort %s', (effort) => { const wire = serializeRequest( - request({ messages: history, reasoningEffort: ReasoningEffortId('max') }), + request({ messages: history, reasoningEffort: ReasoningEffortId(effort) }), { thinking: 'enabled', reasoningEffort: 'high' }, ) expect(wire.thinking).toEqual({ type: 'enabled' }) - expect(wire.reasoning_effort).toBe('max') + expect(wire.reasoning_effort).toBe(effort) }) it('maps off to disabled thinking without a wire reasoning effort', () => {