Просмотр исходного кода

Merge pull request #2549 from deepseek-harness/worktree/llm-deepseek-low-effort

feat(llm-deepseek): support low reasoning effort
Yichen Jiang 1 месяц назад
Родитель
Сommit
39ada2e763

+ 2 - 2
.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;
 # 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:
 # 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
 #   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

+ 1 - 1
.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.
 `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
 ## Alternatives considered
 
 

+ 1 - 1
.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。
 `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`。
 
 
 ## 备选方案
 ## 备选方案
 
 

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
 #   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

+ 1 - 1
docs/config-catalog.md

@@ -859,7 +859,7 @@ export interface Config {
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   thinking?: 'enabled' | 'disabled'
   thinking?: 'enabled' | 'disabled'
   /** Default thinking effort (default `high`); `off` disables thinking per request. */
   /** 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. */
   /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
   maxTokens?: number
   maxTokens?: number
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */

+ 1 - 1
docs/config-catalog.zh.md

@@ -861,7 +861,7 @@ export interface Config {
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   thinking?: 'enabled' | 'disabled'
   thinking?: 'enabled' | 'disabled'
   /** Default thinking effort (default `high`); `off` disables thinking per request. */
   /** 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. */
   /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
   maxTokens?: number
   maxTokens?: number
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */

+ 1 - 1
examples/acp-agent/tests/fs-diff-bound.cordis.snapshot.yml

@@ -41,6 +41,6 @@
                     - id: deepseek-v4-flash
                     - id: deepseek-v4-flash
                       contextWindow: 1000000
                       contextWindow: 1000000
                       defaultMaxTokens: 256000
                       defaultMaxTokens: 256000
-                      reasoningEfforts: ['off', 'high', 'max']
+                      reasoningEfforts: ['off', 'low', 'high', 'max']
                       defaultReasoningEffort: max
                       defaultReasoningEffort: max
                     - id: deepseek-v4-pro
                     - id: deepseek-v4-pro

+ 2 - 1
examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml

@@ -6,7 +6,8 @@
       - id: llm-deepseek
       - id: llm-deepseek
         config:
         config:
           baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
           baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
-          thinking: disabled
+          thinking: enabled
+          reasoningEffort: low
           streamIdleTimeoutMs: 150
           streamIdleTimeoutMs: 150
       - id: agent-spine
       - id: agent-spine
         config:
         config:

+ 2 - 1
examples/headless-agent/tests/headless.snapshot.ts

@@ -541,6 +541,7 @@ describe('headless stream-json snapshots', () => {
       expect(result.stderr).toBe('')
       expect(result.stderr).toBe('')
       expect(server.requests).toHaveLength(1)
       expect(server.requests).toHaveLength(1)
       expect(server.requests[0]?.max_tokens).toBe(256_000)
       expect(server.requests[0]?.max_tokens).toBe(256_000)
+      expect(server.requests[0]?.reasoning_effort).toBe('low')
       const header = (parseJsonl(result.stdout)
       const header = (parseJsonl(result.stdout)
         .map(record => record.event)
         .map(record => record.event)
         .find((event): event is JsonObject => (
         .find((event): event is JsonObject => (
@@ -555,7 +556,7 @@ describe('headless stream-json snapshots', () => {
           "maxTokens": 256000,
           "maxTokens": 256000,
           "model": "deepseek-v4-flash",
           "model": "deepseek-v4-flash",
           "provider": "deepseek-official",
           "provider": "deepseek-official",
-          "reasoningEffort": "off",
+          "reasoningEffort": "low",
         }
         }
       `)
       `)
       expect(header?.adapterDefaults).toEqual({
       expect(header?.adapterDefaults).toEqual({

+ 1 - 1
packages/client/ui-settings-models/tests/components.client.spec.tsx

@@ -48,7 +48,7 @@ const PiAiConfig = Schema.object({
 const DeepSeekConfig = Schema.object({
 const DeepSeekConfig = Schema.object({
   apiKeyEnv: Schema.string().role('credential-ref'),
   apiKeyEnv: Schema.string().role('credential-ref'),
   baseURL: Schema.string().pattern(/^https:\/\//),
   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),
   defaultContextWindow: Schema.number().step(1).min(1),
   models: Schema.array(Schema.object({
   models: Schema.array(Schema.object({
     id: Schema.string().required(),
     id: Schema.string().required(),

+ 1 - 1
packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx

@@ -29,7 +29,7 @@ function fail<T>(message: string): RpcResponse<T> {
 const DeepSeekConfig = Schema.object({
 const DeepSeekConfig = Schema.object({
   apiKeyEnv: Schema.string().role('credential-ref'),
   apiKeyEnv: Schema.string().role('credential-ref'),
   baseURL: Schema.string().pattern(/^https:\/\//),
   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),
   defaultContextWindow: Schema.number().step(1).min(1),
   models: Schema.array(Schema.object({
   models: Schema.array(Schema.object({
     id: Schema.string().required(),
     id: Schema.string().required(),

+ 2 - 2
packages/llm/llm-deepseek/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
 #   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

+ 3 - 3
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
     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
     baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
     thinking: enabled        # optional; provider default is enabled
     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
     maxTokens: 256000        # optional positive per-request output cap; this is the default
     streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
     streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
     retryPolicy:             # optional; omission uses bounded normal defaults
     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`.
 `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.
 `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.
 
 

+ 3 - 3
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
     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
     baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
     thinking: enabled        # optional; provider default is enabled
     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
     maxTokens: 256000        # optional positive per-request output cap; this is the default
     streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
     streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
     retryPolicy:             # optional; omission uses bounded normal defaults
     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`。
 `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(智能体)步骤边界单独执行该策略。
 `streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它把已配置策略注册为提供方元数据,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。
 
 

+ 7 - 3
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
 export const DEFAULT_MAX_TOKENS = 256_000
 const STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT'
 const STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT'
 const OFF_REASONING_EFFORT = ReasoningEffortId('off')
 const OFF_REASONING_EFFORT = ReasoningEffortId('off')
+const LOW_REASONING_EFFORT = ReasoningEffortId('low')
 const HIGH_REASONING_EFFORT = ReasoningEffortId('high')
 const HIGH_REASONING_EFFORT = ReasoningEffortId('high')
 const MAX_REASONING_EFFORT = ReasoningEffortId('max')
 const MAX_REASONING_EFFORT = ReasoningEffortId('max')
 const REASONING_EFFORTS = [
 const REASONING_EFFORTS = [
   { id: OFF_REASONING_EFFORT, name: 'Off' },
   { id: OFF_REASONING_EFFORT, name: 'Off' },
+  { id: LOW_REASONING_EFFORT, name: 'Low' },
   { id: HIGH_REASONING_EFFORT, name: 'High' },
   { id: HIGH_REASONING_EFFORT, name: 'High' },
   { id: MAX_REASONING_EFFORT, name: 'Max' },
   { id: MAX_REASONING_EFFORT, name: 'Max' },
 ] as const
 ] as const
@@ -203,9 +205,11 @@ export class DeepSeekAdapter extends LlmAdapter {
             efforts: REASONING_EFFORTS,
             efforts: REASONING_EFFORTS,
             defaultEffort: connection.defaults.reasoningEffort === 'off'
             defaultEffort: connection.defaults.reasoningEffort === 'off'
               ? OFF_REASONING_EFFORT
               ? 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,
           },
           },
         },
         },
     })
     })

+ 2 - 2
packages/llm/llm-deepseek/src/index.ts

@@ -67,7 +67,7 @@ export interface Config {
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   /** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
   thinking?: 'enabled' | 'disabled'
   thinking?: 'enabled' | 'disabled'
   /** Default thinking effort (default `high`); `off` disables thinking per request. */
   /** 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. */
   /** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
   maxTokens?: number
   maxTokens?: number
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
   /** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
@@ -92,7 +92,7 @@ export const Config: z<Config> = z.object({
   apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
   apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV),
   baseURL: z.string(),
   baseURL: z.string(),
   thinking: z.union(['enabled', 'disabled']),
   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),
   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),
   defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
   models: z.array(catalogModel).default(DEFAULT_MODELS),
   models: z.array(catalogModel).default(DEFAULT_MODELS),

+ 6 - 6
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). */
 /** Adapter-level request defaults (from plugin config). */
 export interface RequestDefaults {
 export interface RequestDefaults {
   thinking?: 'enabled' | 'disabled' | undefined
   thinking?: 'enabled' | 'disabled' | undefined
-  reasoningEffort?: 'off' | 'high' | 'max' | undefined
+  reasoningEffort?: 'off' | 'low' | 'high' | 'max' | undefined
 }
 }
 
 
 interface ResolvedThinking {
 interface ResolvedThinking {
   thinking?: 'enabled' | 'disabled'
   thinking?: 'enabled' | 'disabled'
-  reasoningEffort?: 'high' | 'max'
+  reasoningEffort?: 'low' | 'high' | 'max'
 }
 }
 
 
 /** Validate the adapter-owned effort before resolving its DeepSeek wire fields. */
 /** Validate the adapter-owned effort before resolving its DeepSeek wire fields. */
-function reasoningEffort(effort: NonNullable<GenerateOptions['reasoningEffort']>): 'off' | 'high' | 'max' {
-  if (effort === 'off' || effort === 'high' || effort === 'max') {
-    return effort as 'off' | 'high' | 'max'
+function reasoningEffort(effort: NonNullable<GenerateOptions['reasoningEffort']>): 'off' | 'low' | 'high' | 'max' {
+  if (effort === 'off' || effort === 'low' || effort === 'high' || effort === 'max') {
+    return effort as 'off' | 'low' | 'high' | 'max'
   }
   }
   throw new LlmError(
   throw new LlmError(
     `DeepSeek does not support reasoning effort "${effort}"`,
     `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 === 'off') return { thinking: 'disabled' }
-  if (effort === 'high' || effort === 'max') {
+  if (effort === 'low' || effort === 'high' || effort === 'max') {
     return { thinking: 'enabled', reasoningEffort: effort }
     return { thinking: 'enabled', reasoningEffort: effort }
   }
   }
   return defaults.thinking === undefined ? {} : { thinking: defaults.thinking }
   return defaults.thinking === undefined ? {} : { thinking: defaults.thinking }

+ 2 - 2
packages/llm/llm-deepseek/src/types.ts

@@ -17,8 +17,8 @@ export interface WireRequest {
   stream_options: { include_usage: true }
   stream_options: { include_usage: true }
   /** Thinking-mode toggle (top level, NOT inside extra_body on the wire). */
   /** Thinking-mode toggle (top level, NOT inside extra_body on the wire). */
   thinking?: { type: 'enabled' | 'disabled' }
   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[]
   tools?: WireTool[]
   temperature?: number
   temperature?: number
   max_tokens?: number
   max_tokens?: number

+ 3 - 3
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
  * 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).
  * 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 ctx = await harness(FLASH, { reasoningEffort: 'off' })
     const withoutThinking = await assemble(ctx,{
     const withoutThinking = await assemble(ctx,{
       model: FLASH,
       model: FLASH,
@@ -110,7 +110,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
 
 
     const withThinking = await assemble(ctx,{
     const withThinking = await assemble(ctx,{
       model: FLASH,
       model: FLASH,
-      reasoningEffort: ReasoningEffortId('high'),
+      reasoningEffort: ReasoningEffortId('low'),
       messages: ask('Which is larger, 9.11 or 9.8? Answer with just the number.'),
       messages: ask('Which is larger, 9.11 or 9.8? Answer with just the number.'),
       maxTokens: 2000,
       maxTokens: 2000,
     })
     })

+ 8 - 6
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')
     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([
     const server = await mockServer([
       { kind: 'sse', events: textEvents },
       { kind: 'sse', events: textEvents },
       { kind: 'sse', events: textEvents },
       { 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,{
     await assemble(ctx,{
       model: 'deepseek-v4-flash',
       model: 'deepseek-v4-flash',
@@ -174,7 +174,7 @@ describe('DeepSeekAdapter against a mock server', () => {
     })
     })
     expect(server.requests[0]).toMatchObject({
     expect(server.requests[0]).toMatchObject({
       thinking: { type: 'enabled' },
       thinking: { type: 'enabled' },
-      reasoning_effort: 'high',
+      reasoning_effort: 'low',
     })
     })
     expect(server.requests[1]).toMatchObject({
     expect(server.requests[1]).toMatchObject({
       thinking: { type: 'disabled' },
       thinking: { type: 'disabled' },
@@ -671,6 +671,7 @@ describe('plugin registration and config', () => {
         reasoning: {
         reasoning: {
           efforts: [
           efforts: [
             { id: ReasoningEffortId('off'), name: 'Off' },
             { id: ReasoningEffortId('off'), name: 'Off' },
+            { id: ReasoningEffortId('low'), name: 'Low' },
             { id: ReasoningEffortId('high'), name: 'High' },
             { id: ReasoningEffortId('high'), name: 'High' },
             { id: ReasoningEffortId('max'), name: 'Max' },
             { 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()
     const ctx = new Context()
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(LlmRuntime)
     await ctx.plugin(LlmDeepSeek, {
     await ctx.plugin(LlmDeepSeek, {
@@ -691,6 +692,7 @@ describe('plugin registration and config', () => {
         reasoning: {
         reasoning: {
           efforts: [
           efforts: [
             { id: ReasoningEffortId('off'), name: 'Off' },
             { id: ReasoningEffortId('off'), name: 'Off' },
+            { id: ReasoningEffortId('low'), name: 'Low' },
             { id: ReasoningEffortId('high'), name: 'High' },
             { id: ReasoningEffortId('high'), name: 'High' },
             { id: ReasoningEffortId('max'), name: 'Max' },
             { 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',
     'rejects configured reasoning effort %s when thinking is disabled',
     async (reasoningEffort) => {
     async (reasoningEffort) => {
       const ctx = new Context()
       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',
     'rejects disabled-thinking effort %s at the resolver boundary',
     (reasoningEffort) => {
     (reasoningEffort) => {
       expect(() => resolveAdapterOptions({ thinking: 'disabled', reasoningEffort }))
       expect(() => resolveAdapterOptions({ thinking: 'disabled', reasoningEffort }))

+ 3 - 3
packages/llm/llm-deepseek/tests/serialize.spec.ts

@@ -203,13 +203,13 @@ describe('serializeRequest', () => {
     expect(wire.tools).toBeUndefined()
     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(
     const wire = serializeRequest(
-      request({ messages: history, reasoningEffort: ReasoningEffortId('max') }),
+      request({ messages: history, reasoningEffort: ReasoningEffortId(effort) }),
       { thinking: 'enabled', reasoningEffort: 'high' },
       { thinking: 'enabled', reasoningEffort: 'high' },
     )
     )
     expect(wire.thinking).toEqual({ type: 'enabled' })
     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', () => {
   it('maps off to disabled thinking without a wire reasoning effort', () => {