diff --git a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml index 7db9691a6c..3778da03de 100644 --- a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml @@ -24,6 +24,9 @@ - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official - id: session-log-deepseek config: diff --git a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml index 498c600d7b..2b4b636ecb 100644 --- a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml +++ b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml @@ -24,6 +24,9 @@ - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official # Hold the parent's second step until the manager notice enters its inbox. - id: settlement-fence diff --git a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl index ea54cd60af..904ee65726 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl @@ -12,7 +12,7 @@ {"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"cli-mock","model":"cli-mock"}} {"type":"session/title","data":{"title":"Prove the product headless profile","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"cli-mock","model":"cli-mock"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Prove the product headless profile path with one real tool round trip.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}} +{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"cli-mock","model":"cli-mock"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Prove the product headless profile path with one real tool round trip.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Inspecting the task before the tool call."},{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}],"source":{"kind":"model","provider":"cli-mock","model":"cli-mock"},"id":"{{sessionId}}"},"usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[],"texts":["Inspecting the task before the tool call."]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Inspecting the task before the tool call."}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"cli-smoke-call","name":"bash","args":["{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":11,"outputTokens":3,"cacheReadTokens":2}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"cli-smoke-call","name":"bash","arguments":"{\"command\":\"printf CLI_TOOL_ROUND_TRIP\",\"description\":\"Prove the CLI tool round trip.\"}"}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"cli-smoke-call"},"toolCallId":"cli-smoke-call","content":[{"type":"text","text":"CLI_TOOL_ROUND_TRIP"}],"isError":false,"id":"{{sessionId}}"},"meta":{"cwd":"{{cwd}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} diff --git a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl index 54bd0bb907..6ea0d43a17 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl @@ -8,7 +8,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: the API key resolved from DEEPSEEK_API_KEY contains characters no HTTP header can carry; set DEEPSEEK_API_KEY to the raw key alone (the web Models page writes it)","code":"INVALID_CREDENTIAL"}}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl index bc8fb7453e..f24d221b96 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl @@ -8,7 +8,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":13,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"say pong\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":4096,"reasoningEffort":"off"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/attempt","seq":14,"time":0,"data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":15,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":16,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), or export DEEPSEEK_API_KEY in the launching environment","code":"MISSING_CREDENTIAL"}}}}} diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index b3901f3dec..7a46b1dc89 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -269,6 +269,8 @@ describe('headless stream-json snapshots', () => { if (actual === undefined) throw new Error('the headless profile did not persist its session') const context = contextFromLogs([actual.content]) const session = normalizeSessionSnapshot(actual.content, context) + const titleRequest = parseJsonl(session).find(event => event.type === 'session/title-llm-request') + expect(titleRequest?.data).toMatchObject({ reasoningEffort: 'off', maxTokens: TITLE_MAX_TOKENS }) if (refreshing) await writeFile(headlessSessionExpected, session) await expectSessionSnapshot(session, context, headlessSessionExpected) expect(session).toContain(task) @@ -716,9 +718,7 @@ describe('headless stream-json snapshots', () => { const agentRequest = server.requests.find(request => !isTitleRequest(request)) const titleRequest = server.requests.find(isTitleRequest) expect(agentRequest).not.toHaveProperty('max_completion_tokens') - // The route override caps this session's requests at 1024, so the title - // request dispatches below the base bundle's 4096 ceiling. - expect(titleRequest?.max_tokens).toBe(1_024) + expect(titleRequest?.max_tokens).toBe(TITLE_MAX_TOKENS) const header = (parseJsonl(result.stdout) .map(record => record.event) .find((event): event is JsonObject => ( diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 848a575480..e6df532f2b 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1943,7 +1943,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable + @@ -4501,7 +4501,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff` +SHA-256: `045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19` Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts) @@ -4510,6 +4510,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/sess | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | | `messages` | required | [`Message[]`](#persistence-type-sha256-4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c) | +| `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | | `titleProvider` | required | `string` | @@ -8344,17 +8345,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd` +SHA-256: `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index f4ea1a6a4c..d165df79e3 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -58,7 +58,7 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd) | +| `event:session/title-llm-request` | event | `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -845,7 +845,7 @@ export type SessionEvent = { 类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md) -来源:[`packages/session/session-title-llm/src/index.ts:52`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:54`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -3838,7 +3838,7 @@ SHA-256: `67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0` SHA-256: `326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693` -来源:[`packages/llm/llm/src/call-config.ts:36`](../packages/llm/llm/src/call-config.ts) +来源:[`packages/llm/llm/src/call-config.ts:50`](../packages/llm/llm/src/call-config.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4495,7 +4495,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4503,7 +4503,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff` +SHA-256: `045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19` 来源:[`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts) @@ -4512,6 +4512,7 @@ SHA-256: `681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff` | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | | `messages` | 必需 | [`Message[]`](#persistence-type-sha256-4be8fd28fefa5534675ea9aabd0a24efc2838b989bbc74da20a1efbd0daede8c) | +| `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | | `titleProvider` | 必需 | `string` | @@ -8346,17 +8347,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd` +SHA-256: `b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml b/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml new file mode 100644 index 0000000000..3ac60a790d --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-07-title-reasoning-effort.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-07-title-reasoning-effort.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-07-title-reasoning-effort: + en: 62238f237d762372 + zh: 67c868935122faba +/2026-10-07-title-reasoning-effort/summary: + en: a23a870e04888548 + zh: a794ea9022adf492 +/2026-10-07-title-reasoning-effort/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-07-title-reasoning-effort/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-07-title-reasoning-effort/compatibility: + en: 2aef689aa3af8d1c + zh: dc468f9d2c98d70c +/2026-10-07-title-reasoning-effort/verification: + en: 48ef9b977eaf074b + zh: b1183bda3a6a38be +/2026-10-07-title-reasoning-effort/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.md b/docs/persistence-changes/2026-10-07-title-reasoning-effort.md new file mode 100644 index 0000000000..658dac171d --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-07-title-reasoning-effort + +English | [中文](2026-10-07-title-reasoning-effort.zh.md) + +## Summary + +Records the concrete reasoning effort selected for auxiliary session-title requests. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-07-title-reasoning-effort +baseline: false +changes: + - root: "event:session/title-llm-request" + previous: "2026-10-05-working-directory-attribution" + after: "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6" + decision: same-version +``` + + +## Compatibility + +The new reasoningEffort field is optional. Existing V4 records remain valid and retain their original absence of a recorded effort. Prepared title requests write the resolved effort when the route exposes selectable reasoning; no Session format bump is required. + + +## Verification + +Focused LLM, adapter, title-provider, and replay tests passed: 50 files and 1960 tests. The session-title-after-turn SDK snapshot passed with a recorded low effort selected from a route whose default is high. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json b/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json new file mode 100644 index 0000000000..06cad34dff --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.schema.json @@ -0,0 +1,2990 @@ +{ + "formatVersion": 2, + "roots": [ + { + "key": "event:session/title-llm-request", + "kind": "event", + "event": "session/title-llm-request", + "surface": false, + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", + "schema": { + "root": 0, + "nodes": [ + { + "kind": "object", + "properties": [ + { + "name": "data", + "type": 1, + "optional": false + }, + { + "name": "ignorable", + "type": 29, + "optional": true + }, + { + "name": "seq", + "type": 2, + "optional": false + }, + { + "name": "time", + "type": 2, + "optional": false + }, + { + "name": "type", + "type": 210, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "maxTokens", + "type": 2, + "optional": false + }, + { + "name": "messageSeqs", + "type": 3, + "optional": false + }, + { + "name": "messages", + "type": 4, + "optional": false + }, + { + "name": "reasoningEffort", + "type": 10, + "optional": true + }, + { + "name": "route", + "type": 209, + "optional": false + }, + { + "name": "system", + "type": 10, + "optional": false + }, + { + "name": "titleProvider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "number" + }, + { + "kind": "array", + "element": 2 + }, + { + "kind": "array", + "element": 5 + }, + { + "kind": "union", + "types": [ + 6, + 37, + 204, + 205, + 207 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "isError", + "type": 34, + "optional": true + }, + { + "name": "role", + "type": 35, + "optional": false + }, + { + "name": "source", + "type": 36, + "optional": false + }, + { + "name": "toolCallId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 8 + }, + { + "kind": "union", + "types": [ + 9, + 12, + 14, + 17, + 19, + 21, + 31 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 11, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "string" + }, + { + "kind": "literal", + "value": "reasoning" + }, + { + "kind": "object", + "properties": [ + { + "name": "text", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 13, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "text" + }, + { + "kind": "object", + "properties": [ + { + "name": "tool", + "type": 15, + "optional": true + }, + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 16, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "primitive", + "type": "never" + }, + { + "kind": "literal", + "value": "tool-addition" + }, + { + "kind": "object", + "properties": [ + { + "name": "toolName", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 18, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-removal" + }, + { + "kind": "object", + "properties": [ + { + "name": "arguments", + "type": 10, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "type", + "type": 20, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-call" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 22, + "optional": false + }, + { + "name": "offloaded", + "type": 29, + "optional": true + }, + { + "name": "type", + "type": 30, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "mediaType", + "type": 23, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": true + }, + { + "name": "originalDimensions", + "type": 28, + "optional": true + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 24, + 25, + 26, + 27 + ] + }, + { + "kind": "literal", + "value": "image/gif" + }, + { + "kind": "literal", + "value": "image/jpeg" + }, + { + "kind": "literal", + "value": "image/png" + }, + { + "kind": "literal", + "value": "image/webp" + }, + { + "kind": "object", + "properties": [ + { + "name": "height", + "type": 2, + "optional": false + }, + { + "name": "width", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "literal", + "value": "image" + }, + { + "kind": "object", + "properties": [ + { + "name": "attachment", + "type": 32, + "optional": false + }, + { + "name": "type", + "type": 33, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "attachmentId", + "type": 10, + "optional": false + }, + { + "name": "bytes", + "type": 2, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "file" + }, + { + "kind": "primitive", + "type": "boolean" + }, + { + "kind": "literal", + "value": "tool" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 35, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 38, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.developer-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "developer" + }, + { + "kind": "union", + "types": [ + 36, + 40, + 45, + 47, + 49, + 51, + 53, + 55, + 56, + 58, + 60, + 62, + 63, + 65, + 67, + 69, + 71, + 72, + 73, + 76, + 79, + 81, + 83, + 92, + 95, + 97, + 98, + 100, + 102, + 104, + 109, + 111, + 119, + 120, + 121, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 191, + 192, + 194, + 197, + 198, + 199, + 200, + 201, + 203 + ] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "snapshot" + }, + { + "kind": "literal", + "value": "tool-goal" + }, + { + "kind": "array", + "element": 44 + }, + { + "kind": "object", + "properties": [ + { + "name": "name", + "type": 10, + "optional": false + }, + { + "name": "text", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "runtime-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "schedule" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 50, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system-prompt" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "time-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tmux-context" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-jobs" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 59, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "tool-registry" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 61, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-approval" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 66, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "working-directory" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 68, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "cordis-host-runner" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "dsh-session-title-llm" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 75, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "notice" + }, + { + "kind": "literal", + "value": "subagent-settled" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 78, + "optional": false + }, + { + "name": "senderSessionId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "relay" + }, + { + "kind": "literal", + "value": "agent-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-claude-code" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "hooks-codex" + }, + { + "kind": "object", + "properties": [ + { + "name": "baseline", + "type": 29, + "optional": true + }, + { + "name": "baselineIdentity", + "type": 10, + "optional": true + }, + { + "name": "changes", + "type": 84, + "optional": false + }, + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 91, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 85 + }, + { + "kind": "object", + "properties": [ + { + "name": "action", + "type": 86, + "optional": false + }, + { + "name": "digest", + "type": 10, + "optional": true + }, + { + "name": "path", + "type": 10, + "optional": false + }, + { + "name": "scope", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 87, + 88, + 89 + ] + }, + { + "kind": "literal", + "value": "remove" + }, + { + "kind": "literal", + "value": "replace" + }, + { + "kind": "literal", + "value": "set" + }, + { + "kind": "literal", + "value": "instructions" + }, + { + "kind": "literal", + "value": "agent-instructions" + }, + { + "kind": "object", + "properties": [ + { + "name": "callId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 93, + "optional": false + }, + { + "name": "outcome", + "type": 94, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "user-question-reply" + }, + { + "kind": "literal", + "value": "answered" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model-selection" + }, + { + "kind": "object", + "properties": [ + { + "name": "clientTimeZone", + "type": 10, + "optional": true + }, + { + "name": "kind", + "type": 61, + "optional": false + }, + { + "name": "rpcId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "compactionId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 99, + "optional": false + }, + { + "name": "sourceCommandId", + "type": 10, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "compact-checkpoint" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "plan-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "deliveryId", + "type": 10, + "optional": false + }, + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 103, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "ruleId", + "type": 10, + "optional": false + }, + { + "name": "source", + "type": 10, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "webhook" + }, + { + "kind": "object", + "properties": [ + { + "name": "entries", + "type": 105, + "optional": false + }, + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 108, + "optional": false + }, + { + "name": "update", + "type": 29, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "array", + "element": 106 + }, + { + "kind": "object", + "properties": [ + { + "name": "description", + "type": 10, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "catalog" + }, + { + "kind": "literal", + "value": "skill-catalog" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 110, + "optional": false + }, + { + "name": "name", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "skill-invocation" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 113, + "optional": false + }, + { + "name": "references", + "type": 114, + "optional": false + }, + { + "name": "version", + "type": 118, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "recall" + }, + { + "kind": "literal", + "value": "session-reference" + }, + { + "kind": "array", + "element": 115 + }, + { + "kind": "object", + "properties": [ + { + "name": "capturedFormatVersion", + "type": 2, + "optional": true + }, + { + "name": "capturedThroughSeq", + "type": 116, + "optional": false + }, + { + "name": "compacted", + "type": 34, + "optional": false + }, + { + "name": "inputIndex", + "type": 2, + "optional": false + }, + { + "name": "label", + "type": 10, + "optional": false + }, + { + "name": "omittedBytes", + "type": 2, + "optional": false + }, + { + "name": "omittedMessages", + "type": 2, + "optional": false + }, + { + "name": "originalMessages", + "type": 2, + "optional": false + }, + { + "name": "retainedMessages", + "type": 2, + "optional": false + }, + { + "name": "sessionId", + "type": 10, + "optional": false + }, + { + "name": "truncated", + "type": 34, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "union", + "types": [ + 2, + 117 + ] + }, + { + "kind": "primitive", + "type": "null" + }, + { + "kind": "literal", + "value": 1 + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "repeat-tool-reminder" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 74, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + }, + { + "name": "summary", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 90, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 112, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 77, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 46, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 48, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 52, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 42, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 57, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 64, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 107, + "optional": false + }, + { + "name": "kind", + "type": 70, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "goalId", + "type": 10, + "optional": false + }, + { + "name": "kind", + "type": 190, + "optional": false + }, + { + "name": "revision", + "type": 2, + "optional": false + }, + { + "name": "round", + "type": 2, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "goal" + }, + { + "kind": "object", + "properties": [ + { + "name": "form", + "type": 41, + "optional": false + }, + { + "name": "kind", + "type": 54, + "optional": false + }, + { + "name": "sections", + "type": 43, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 193, + "optional": false + }, + { + "name": "messageId", + "type": 10, + "optional": false + }, + { + "name": "senderId", + "type": 10, + "optional": false + }, + { + "name": "senderName", + "type": 10, + "optional": false + }, + { + "name": "teamId", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "team-message" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 195, + "optional": false + }, + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + }, + { + "name": "replayState", + "type": 196, + "optional": true + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "model" + }, + { + "kind": "opaque", + "reason": "unknown" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 80, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 82, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 96, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 101, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 202, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "ptc-mode" + }, + { + "kind": "object", + "properties": [ + { + "name": "kind", + "type": 122, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 61, + "optional": false + }, + { + "name": "source", + "type": 39, + "optional": false, + "compatibility": { + "version": 1, + "policy": "session-source-attribution", + "binding": "session.user-message.source", + "discriminator": "kind", + "unknownKinds": "preserve", + "attributionKinds": [ + "tmux-context", + "user-question-reply", + "working-directory" + ] + } + } + ], + "indices": [] + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 206, + "optional": false + }, + { + "name": "source", + "type": 194, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "assistant" + }, + { + "kind": "object", + "properties": [ + { + "name": "content", + "type": 7, + "optional": false + }, + { + "name": "id", + "type": 10, + "optional": false + }, + { + "name": "role", + "type": 208, + "optional": false + }, + { + "name": "source", + "type": 49, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "system" + }, + { + "kind": "object", + "properties": [ + { + "name": "model", + "type": 10, + "optional": false + }, + { + "name": "provider", + "type": 10, + "optional": false + } + ], + "indices": [] + }, + { + "kind": "literal", + "value": "session/title-llm-request" + } + ] + } + } + ], + "types": [] +} diff --git a/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md b/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md new file mode 100644 index 0000000000..39c05b5b52 --- /dev/null +++ b/docs/persistence-changes/2026-10-07-title-reasoning-effort.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-07-title-reasoning-effort + +[English](2026-10-07-title-reasoning-effort.md) | 中文 + +## 概述 + +记录辅助会话标题请求选择的具体推理强度。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-07-title-reasoning-effort +baseline: false +changes: + - root: "event:session/title-llm-request" + previous: "2026-10-05-working-directory-attribution" + after: "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6" + decision: same-version +``` + + +## 兼容性 + +新增 reasoningEffort 字段为可选。现有 V4 记录仍有效,继续保留其未记录强度的事实。路由提供可选推理强度时,已准备的标题请求写入解析后的强度;无需提升 Session 格式版本。 + + +## 验证 + +LLM、适配器、标题提供方与回放的定向测试通过:50 个文件、1960 个测试。session-title-after-turn SDK 快照通过,记录从默认 high 的路由中选出的 low 强度。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index e9268ae0f7..ff0f757f2d 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -11890,7 +11890,7 @@ "kind": "event", "event": "session/title-llm-request", "surface": false, - "digest": "19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd", + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", "schema": { "root": 0, "nodes": [ @@ -11943,6 +11943,11 @@ "type": 4, "optional": false }, + { + "name": "reasoningEffort", + "type": 10, + "optional": true + }, { "name": "route", "type": 209, @@ -22041,6 +22046,15 @@ "names": [], "sources": [] }, + { + "digest": "045926475a46f625f84ae2be292b384817059f16927c69a9adab93717923ac19", + "names": [ + "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" + ], + "sources": [ + "packages/session/session-title-llm/src/index.ts:34" + ] + }, { "digest": "05a2796621b1ee6c03ef7abe560b25116bb4da4d592312117242a78415a32b04", "names": [], @@ -22359,11 +22373,6 @@ "names": [], "sources": [] }, - { - "digest": "19fab83e64cf84ee8a6caf62c592e85c9b195bfd19e84a2667c6693a395681fd", - "names": [], - "sources": [] - }, { "digest": "1a2718b00937b236a0d0910ef5dab2f9f8b65e4bde410cc21f15fbbf29f584d3", "names": [], @@ -22758,7 +22767,7 @@ "packages/llm/llm/src/call-config.ts#LlmCallConfigAdapterDefaults" ], "sources": [ - "packages/llm/llm/src/call-config.ts:36" + "packages/llm/llm/src/call-config.ts:50" ] }, { @@ -23537,15 +23546,6 @@ "packages/llm/llm/src/call-config.ts:23" ] }, - { - "digest": "681028d4517ac1ac96af970b6d1d084e8c257d9ba7941478985922880b0eccff", - "names": [ - "packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData" - ], - "sources": [ - "packages/session/session-title-llm/src/index.ts:34" - ] - }, { "digest": "68141cbe3994a5ffcd064c07c75996538437fd9dbf77ce8a2927e89ec577fabe", "names": [], @@ -24691,6 +24691,11 @@ "packages/experimental/agent-team/src/types.ts:71" ] }, + { + "digest": "b33545b3244861bcbf73b407d24b53402710f5acacab7ae939537e5504a68dc6", + "names": [], + "sources": [] + }, { "digest": "b3681c026a0b98e98b103f057dfd217fbe904129c090efd668253ec9aea7fece", "names": [], diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index f6f62760f2..6828f1f8e0 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -554,9 +554,9 @@ interface LlmModelReasoningInfo { /** Supported efforts in adapter-preferred display order. */ efforts: readonly LlmReasoningEffortInfo[] /** - * The least effort this route accepts. A caller selects it when the request - * must not spend its output on reasoning. Absence means the adapter reports - * no selectable effort. + * The least selectable effort this route accepts. Minimum-effort call + * preparation requires this field when reasoning controls are available. + * It does not guarantee that the provider disables reasoning. */ floorEffort?: ReasoningEffortId /** @@ -767,6 +767,24 @@ interface LlmCallConfig { } ``` +```ts type-equiv +/** Exact effort or a request to select the route's least controllable reasoning. */ +type ReasoningSelection = ReasoningEffortId | { readonly select: 'minimum' } +``` + +```ts type-equiv +/** + * Preparation input. Concrete call configs are accepted unchanged. A minimum + * selection uses the adapter's declared floor; selectable efforts without a + * declared floor reject. A route with no selectable reasoning leaves the + * effort unset. Minimum selection does not guarantee zero reasoning tokens. + */ +interface LlmCallRequest extends Omit { + /** Omission uses the adapter default; an id requires that exact effort. */ + reasoningEffort?: ReasoningSelection +} +``` + ```ts type-equiv /** * Effective config fields supplied by exact-model adapter resolution rather @@ -1060,11 +1078,14 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise +async prepareCall(request: LlmCallRequest, signal?: AbortSignal): Promise /** * Stream one model call as raw chunks (token-level deltas). Replay state is diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 2f9517f3d0..fbd188836a 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -560,9 +560,9 @@ interface LlmModelReasoningInfo { /** Supported efforts in adapter-preferred display order. */ efforts: readonly LlmReasoningEffortInfo[] /** - * The least effort this route accepts. A caller selects it when the request - * must not spend its output on reasoning. Absence means the adapter reports - * no selectable effort. + * The least selectable effort this route accepts. Minimum-effort call + * preparation requires this field when reasoning controls are available. + * It does not guarantee that the provider disables reasoning. */ floorEffort?: ReasoningEffortId /** @@ -773,6 +773,24 @@ interface LlmCallConfig { } ``` +```ts type-equiv +/** Exact effort or a request to select the route's least controllable reasoning. */ +type ReasoningSelection = ReasoningEffortId | { readonly select: 'minimum' } +``` + +```ts type-equiv +/** + * Preparation input. Concrete call configs are accepted unchanged. A minimum + * selection uses the adapter's declared floor; selectable efforts without a + * declared floor reject. A route with no selectable reasoning leaves the + * effort unset. Minimum selection does not guarantee zero reasoning tokens. + */ +interface LlmCallRequest extends Omit { + /** Omission uses the adapter default; an id requires that exact effort. */ + reasoningEffort?: ReasoningSelection +} +``` + ```ts type-equiv /** * Effective config fields supplied by exact-model adapter resolution rather @@ -1066,11 +1084,14 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise +async prepareCall(request: LlmCallRequest, signal?: AbortSignal): Promise /** * Stream one model call as raw chunks (token-level deltas). Replay state is diff --git a/docs/subsystems/session-title.i18n.yaml b/docs/subsystems/session-title.i18n.yaml index 1ca280db5c..a701f0ad9d 100644 --- a/docs/subsystems/session-title.i18n.yaml +++ b/docs/subsystems/session-title.i18n.yaml @@ -9,8 +9,8 @@ en: c6be2d86647daa6f zh: fb259d9fb4738f39 /session-titles/auxiliary-request-record: - en: 4942bec7a7d1d9de - zh: 9edfc1c908577c37 + en: c36916905dad76f9 + zh: c82ff6c3d62e53e5 /session-titles/provider-input-and-output: en: 886e036e442ae06a zh: 49f95782b8aa9bf3 diff --git a/docs/subsystems/session-title.md b/docs/subsystems/session-title.md index 75e17f06d7..699b0148a6 100644 --- a/docs/subsystems/session-title.md +++ b/docs/subsystems/session-title.md @@ -64,7 +64,7 @@ interface SessionTitleSnapshot extends SessionTitleEventData { ## Auxiliary request record -The shared LLM helper records each validated, dispatchable title request before calling the model. The payload reproduces the model-visible system and message input, routing, output limit, provider ownership, and source-message attribution even when generation later fails. +The shared LLM helper records each validated, dispatchable title request before calling the model. The payload reproduces the model-visible system and message input, routing, output limit, resolved reasoning effort, provider ownership, and source-message attribution even when generation later fails. ```ts type-equiv /** Exact model-visible request recorded before one auxiliary title dispatch. */ @@ -81,6 +81,8 @@ interface SessionTitleLlmRequestEventData { readonly messages: Message[] /** Exact auxiliary output-token cap. */ readonly maxTokens: number + /** Resolved reasoning effort when recorded; older requests can omit it. */ + readonly reasoningEffort?: ReasoningEffortId } ``` diff --git a/docs/subsystems/session-title.zh.md b/docs/subsystems/session-title.zh.md index 187dad8f3a..1b360411a8 100644 --- a/docs/subsystems/session-title.zh.md +++ b/docs/subsystems/session-title.zh.md @@ -64,7 +64,7 @@ interface SessionTitleSnapshot extends SessionTitleEventData { ## 辅助请求记录 -共享 LLM 辅助组件会在调用模型前,记录每一项已经过验证且可分发的标题请求。即使后续生成失败,载荷仍会复现模型可见的系统输入与消息输入、路由、输出上限、提供方归属和源消息归因。 +共享 LLM 辅助组件会在调用模型前,记录每一项已经过验证且可分发的标题请求。即使后续生成失败,载荷仍会复现模型可见的系统输入与消息输入、路由、输出上限、解析后的推理强度、提供方归属和源消息归因。 ```ts type-equiv /** Exact model-visible request recorded before one auxiliary title dispatch. */ @@ -81,6 +81,8 @@ interface SessionTitleLlmRequestEventData { readonly messages: Message[] /** Exact auxiliary output-token cap. */ readonly maxTokens: number + /** Resolved reasoning effort when recorded; older requests can omit it. */ + readonly reasoningEffort?: ReasoningEffortId } ``` diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml new file mode 100644 index 0000000000..37f4e2927f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md +/: + en: 87becdc19338ba63 + zh: a9a177200eb0900f +/prepare-custom-title-routes: + en: cab86bb2cc427297 + zh: 7a14e268bf53fc73 +/prepare-custom-title-routes/change: + en: 9bfbb27b3183431d + zh: 7ca1eed2ec9d33fa +/prepare-custom-title-routes/migration: + en: 35e465b350feb1ff + zh: 89f766ee9aee5398 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md new file mode 100644 index 0000000000..85a17eb1cb --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "Custom title routes require registered adapters and minimum-effort metadata." +--- + +# Prepare custom title routes + +English | [中文](guide.zh.md) + +## Change + +Model-backed session titles prepare their requests through `ctx.llm.prepareCall()`. Compositions that serve title routes only through `llm/stream` middleware must also register an adapter. Custom adapters that expose selectable reasoning efforts must declare `reasoning.floorEffort`; otherwise minimum-effort preparation rejects before dispatch. Existing concrete `prepareCall()` inputs remain valid. + +## Migration + +1. Register the title route with `ctx.llm.registerAdapter()`. Replay compositions can configure the existing `llm-replay` plugin's `providers` list. +2. In the adapter's model metadata, set `reasoning.floorEffort` to the least selectable id in `reasoning.efforts`. Replay models use `floorReasoningEffort` alongside `reasoningEfforts`. Omit reasoning metadata when the route has no selectable controls. +3. Generate a title and check that `session/title-llm-request` records the selected effort and a provider title is saved. Existing Session records without that optional field remain readable. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md new file mode 100644 index 0000000000..2a616128dd --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/session-title-adapters/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "自定义标题路由要求注册适配器并提供最低推理强度元数据。" +--- + +# 准备自定义标题路由 + +[English](guide.md) | 中文 + +## 变更 + +模型生成的会话标题通过 `ctx.llm.prepareCall()` 准备请求。仅通过 `llm/stream` 中间件提供标题路由的组合还必须注册适配器。公开可选推理强度的自定义适配器必须声明 `reasoning.floorEffort`;否则最低强度准备会在分发前拒绝。现有具体 `prepareCall()` 输入仍然有效。 + +## 迁移 + +1. 通过 `ctx.llm.registerAdapter()` 注册标题路由。回放组合可配置现有 `llm-replay` 插件的 `providers` 列表。 +2. 在适配器的模型元数据中,将 `reasoning.floorEffort` 设为 `reasoning.efforts` 中最低可选强度的 id。回放模型使用 `floorReasoningEffort` 和 `reasoningEfforts`。路由没有可选控制时省略推理元数据。 +3. 生成标题,确认 `session/title-llm-request` 记录了所选强度,且提供方标题已保存。不含该可选字段的现有 Session 记录仍可读取。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 819dce9e50..21dcc9a765 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1465,9 +1465,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'a detached config only when a default must be materialized.', }, { - signature: 'async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise', - description: 'Resolve one call under its current adapter registration. The returned one-shot handle keeps that registration across header logging and dispatch, so HMR cannot combine one adapter\'s capability result with another adapter.', - parameters: [{ name: 'config', description: 'provider/model route and optional request controls.' }, { name: 'signal', description: 'optional cancellation for adapter-owned capability lookup.' }], + signature: 'async prepareCall(request: LlmCallRequest, signal?: AbortSignal): Promise', + description: 'Resolve one call under its current adapter registration. The returned one-shot handle keeps that registration across header logging and dispatch, so HMR cannot combine one adapter\'s capability result with another adapter. Minimum reasoning selects the adapter-declared floor, or leaves the effort unset when the route has no selectable reasoning. An undeclared floor for selectable efforts rejects with `UNSUPPORTED_REASONING_SELECTION`.', + parameters: [{ name: 'request', description: 'provider/model route and concrete or intent-based controls.' }, { name: 'signal', description: 'optional cancellation for adapter-owned capability lookup.' }], returns: 'a prepared config and its registration-bound stream entry point.', }, { @@ -5731,6 +5731,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'LlmCallConfigAdapterDefaults', declaration: 'export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n}', }, + { + name: 'LlmCallRequest', + declaration: 'export interface LlmCallRequest extends Omit {\n reasoningEffort?: ReasoningSelection;\n}', + }, { name: 'LlmConfigurableProvider', declaration: 'export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n error?: string;\n}', @@ -5781,7 +5785,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmRuntime', - declaration: 'export class LlmRuntime extends TypertRemoteService {\n constructor(ctx: Context);\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n @Remote\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n @Remote\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal): Promise;\n @Remote(\'discoverModels\')\n async remoteDiscoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal): Promise;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n imageRequestPricing(provider: string, model: string): LlmImageRequestPricing | undefined;\n fileRequestText(ref: FileAttachmentRef): string;\n async listModels(provider: string): Promise;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise;\n async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise;\n stream(options: GenerateOptions) /* …truncated — full shape in source */', + declaration: 'export class LlmRuntime extends TypertRemoteService {\n constructor(ctx: Context);\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n @Remote\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n @Remote\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal): Promise;\n @Remote(\'discoverModels\')\n async remoteDiscoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal): Promise;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n imageRequestPricing(provider: string, model: string): LlmImageRequestPricing | undefined;\n fileRequestText(ref: FileAttachmentRef): string;\n async listModels(provider: string): Promise;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise;\n async prepareCall(request: LlmCallRequest, signal?: AbortSignal): Promise;\n stream(options: GenerateOption /* …truncated — full shape in source */', }, { name: 'LocalAtInput', @@ -6363,6 +6367,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ReasoningEffortId', declaration: 'export type ReasoningEffortId = Branded<\'ReasoningEffortId\'>;', }, + { + name: 'ReasoningSelection', + declaration: 'export type ReasoningSelection = ReasoningEffortId | {\n readonly select: \'minimum\';\n};', + }, { name: 'RecurringScheduleRecord', declaration: 'export type RecurringScheduleRecord = EveryScheduleRecord | DailyScheduleRecord | WeeklyScheduleRecord | CronScheduleRecord;', diff --git a/packages/llm/llm-pi-ai/README.i18n.yaml b/packages/llm/llm-pi-ai/README.i18n.yaml index bb63129e2b..1a29ea7187 100644 --- a/packages/llm/llm-pi-ai/README.i18n.yaml +++ b/packages/llm/llm-pi-ai/README.i18n.yaml @@ -30,8 +30,8 @@ en: 376f83206858b601 zh: 38bb6993609c96a6 /deepseek-ai-dsh-llm-pi-ai/use-this-package/run-with-reasoning-and-wire-compatibility: - en: b1f319877df7c6ba - zh: 351a49c549a1850f + en: d3a4f32e13ea83a3 + zh: b0506ebbf17dcaaf /deepseek-ai-dsh-llm-pi-ai/use-this-package/change-configuration-at-runtime: en: 92f554e1718614ca zh: f17ed3ad3e5d28e2 diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 00bcef63d0..33db28d13e 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -102,7 +102,7 @@ A profile's `models` list replaces the route's installed catalog rather than ext `reasoningEfforts` declares a model's selectable thinking levels: each key is a level selectors offer, its value the spelling dispatch sends on the wire, so `max: ultra` renames a level for a gateway with its own vocabulary. Omitting the field keeps the installed catalog entry's capability; `false` declares a non-reasoning model. `compat` switches reshape the request for endpoints pi-ai cannot recognize — which role carries the system prompt, which field caps output, how a thinking level travels — configurable per route and per model. A model neither the entry nor the installed catalog sizes takes the route's `defaultContextWindow` and `defaultMaxTokens` fallbacks. -Exact-model metadata reports `reasoning.floorEffort`, the least level the route accepts: `off` when the model can stop reasoning, otherwise its lowest declared level. A caller that must not spend its output on reasoning selects that effort, and this adapter dispatches the effort it received. `off` dispatches exactly as a selected `off` does, so a protocol that expresses it by omitting the reasoning field leaves the gateway's default in effect; reasoning tokens at any selected level count toward the request's output cap. +Exact-model metadata reports `reasoning.floorEffort`, the least selectable level the route accepts. `prepareCall()` can resolve a minimum-effort request to this id; the adapter receives only the resolved effort. Selecting `off` does not guarantee disabled reasoning: a protocol that expresses it by omitting the reasoning field leaves the gateway's default in effect. Reasoning tokens at any selected level count toward the request's output cap. For self-hosted Chat Completions endpoints, `thinkingTokenBudgetField` selects the reasoning-budget parameter, and `vllmPriority` sets an integer scheduler priority when the server enables priority scheduling. Template arguments accept `$var: thinking.budget`. `openai-responses` gateways can set `supportsMaxOutputTokens: false` to omit `max_output_tokens`; Azure and Codex transports ignore this shared compatibility field. These controls are opt-in; catalog-owned Anthropic effort and fallback capabilities are not configurable switches. diff --git a/packages/llm/llm-pi-ai/README.zh.md b/packages/llm/llm-pi-ai/README.zh.md index c518c9b307..4924ff9882 100644 --- a/packages/llm/llm-pi-ai/README.zh.md +++ b/packages/llm/llm-pi-ai/README.zh.md @@ -102,7 +102,7 @@ profile 的 `models` 列表会替换而非扩展路由的已安装目录;每 `reasoningEfforts` 声明模型可选择的 thinking 等级:每个键都是选择器提供的等级,其值是分派时在协议中发送的拼写,因此 `max: ultra` 可以为拥有自有词汇的网关重命名等级。省略该字段时保留已安装目录条目的能力;`false` 声明非推理模型。对于 pi-ai 无法识别的端点,`compat` 开关重塑请求——哪个角色携带系统提示词、哪个字段限制输出、thinking 等级如何传递——可逐路由、逐模型配置。条目与已安装目录都没有尺寸的模型,会采用路由的 `defaultContextWindow` 与 `defaultMaxTokens` 回退值。 -确切模型元数据会公布 `reasoning.floorEffort`,即该路由接受的最低等级:模型能停止推理时为 `off`,否则为声明的最低等级。需要避免把输出花在推理上的调用方选择该强度,本适配器分派收到的强度。`off` 的分派方式与手动选择 `off` 完全相同,因此以省略推理字段表达 `off` 的协议会沿用网关默认行为;所选等级下产生的推理 token 都计入该请求的输出上限。 +确切模型元数据公布 `reasoning.floorEffort`,即该路由接受的最低可选等级。`prepareCall()` 可将最低强度请求解析为此 id;适配器只接收解析后的强度。选择 `off` 不保证禁用推理:以省略推理字段表达该值的协议会沿用网关默认行为。所选等级下产生的推理 token 都计入该请求的输出上限。 对于自托管 Chat Completions 端点,`thinkingTokenBudgetField` 选择推理预算参数,`vllmPriority` 在服务端启用优先级调度时设置整数调度优先级。模板参数接受 `$var: thinking.budget`。`openai-responses` 网关可设置 `supportsMaxOutputTokens: false` 来省略 `max_output_tokens`;Azure 与 Codex 传输会忽略这个共享兼容字段。这些控制均需显式启用;目录拥有的 Anthropic effort 和回退能力不是可配置开关。 diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index 458c07e85e..bbf2e9b1e6 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -21,8 +21,8 @@ en: c3ecb5a1421f27db zh: 4bbd3c0e513845e1 /deepseek-ai-dsh-llm/use-this-package/minimal-composition: - en: 7550d716b90fcb09 - zh: 77891a44ee623c7a + en: edb00bff3175f3b6 + zh: 927048e5604787b0 /deepseek-ai-dsh-llm/use-this-package/what-you-can-do: en: b45d7a414141734e zh: a641c8ea3827cee6 diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 66b7d0219a..0788b0c809 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -60,6 +60,8 @@ After a successful mount, `ctx.llm.listProviders()` reports the registered route `GenerateOptions.messages` accepts durable `Message` values and request-only `RequestUserInput` values. Request-only inputs carry user-role content with no `id` or `source`; Session writes and Agent delivery still require durable messages. Callers keep auxiliary inputs unchanged until the stream settles. A caller that records its exact request, such as session-title generation, must use durable messages. +`prepareCall(request, signal)` accepts existing `LlmCallConfig` values and `reasoningEffort: { select: 'minimum' }`. It returns a frozen concrete configuration for recording and dispatch. An exact effort id is validated unchanged; omission uses the route default. Minimum selection uses the declared `floorEffort`, rejects selectable efforts without a floor, and leaves effort unspecified on routes without reasoning controls. It does not guarantee zero reasoning tokens. Preparation requires a registered adapter and rejects before streaming when resolution fails. + ### What you can do - **Stream one model call** — `ctx.llm.stream(options)` yields raw chunks (token-level deltas) for any registered provider and model; consumers assemble them with `BlockAssembler`. diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index 6957c8a83b..f8e8edb681 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -60,6 +60,8 @@ for await (const chunk of ctx.llm.stream({ `GenerateOptions.messages` 接受持久 `Message` 值和仅供请求使用的 `RequestUserInput` 值。仅供请求使用的输入包含 user-role 内容,不含 `id` 或 `source`;Session 写入和 Agent 投递仍然要求持久消息。调用方必须在流结束前保持辅助输入不变。会记录完整请求的调用方(例如会话标题生成)必须使用持久消息。 +`prepareCall(request, signal)` 接受现有的 `LlmCallConfig` 值,也接受 `reasoningEffort: { select: 'minimum' }`。它返回用于记录与分发的冻结具体配置。确切强度 id 保持原值并接受校验;省略时使用路由默认值。最低强度选择使用声明的 `floorEffort`;有可选强度却没有最低强度时拒绝,没有推理控制时不指定强度。它不保证零推理 token。准备操作要求注册适配器,解析失败会在流式调用前拒绝。 + ### 你可以做什么 - **流式发起一次模型调用**——`ctx.llm.stream(options)` 为任何已注册提供方与模型产出原始分片(token 级增量);消费方用 `BlockAssembler` 组装。 diff --git a/packages/llm/llm/src/call-config.ts b/packages/llm/llm/src/call-config.ts index 9d012cd5c1..b9ddf14840 100644 --- a/packages/llm/llm/src/call-config.ts +++ b/packages/llm/llm/src/call-config.ts @@ -29,6 +29,20 @@ export interface LlmCallConfig { stop?: string[] } +/** Exact effort or a request to select the route's least controllable reasoning. */ +export type ReasoningSelection = ReasoningEffortId | { readonly select: 'minimum' } + +/** + * Preparation input. Concrete call configs are accepted unchanged. A minimum + * selection uses the adapter's declared floor; selectable efforts without a + * declared floor reject. A route with no selectable reasoning leaves the + * effort unset. Minimum selection does not guarantee zero reasoning tokens. + */ +export interface LlmCallRequest extends Omit { + /** Omission uses the adapter default; an id requires that exact effort. */ + reasoningEffort?: ReasoningSelection +} + /** * Effective config fields supplied by exact-model adapter resolution rather * than by the caller's request proposal. diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index 25f405bac2..35c357424d 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -32,7 +32,7 @@ import { resolveRetryPolicy } from './retry-policy.ts' import type { ResolvedRetryPolicy } from './retry-policy.ts' import type { ProviderRequestId } from './brand.ts' import { callConfigEquals } from './call-config.ts' -import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts' +import type { LlmCallConfig, LlmCallConfigAdapterDefaults, LlmCallRequest } from './call-config.ts' import { HarnessError, INVALID_CREDENTIAL_CODE } from './error.ts' import { normalizeLlmFailure } from './adapter-failure.ts' import { normalizeApiKey } from './api-key.ts' @@ -52,7 +52,7 @@ export * from './message.ts' export * from './retry-policy.ts' export { BlockAssembler } from './assembler.ts' export { callConfigEquals, isAgentLoopRequest, markAgentLoopRequest } from './call-config.ts' -export type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts' +export type { LlmCallConfig, LlmCallConfigAdapterDefaults, LlmCallRequest, ReasoningSelection } from './call-config.ts' declare module '@deepseek-ai/cordis' { interface Context { @@ -929,21 +929,37 @@ export class LlmRuntime extends TypertRemoteService { * Resolve one call under its current adapter registration. The returned * one-shot handle keeps that registration across header logging and dispatch, * so HMR cannot combine one adapter's capability result with another adapter. - * @param config - provider/model route and optional request controls. + * Minimum reasoning selects the adapter-declared floor, or leaves the effort + * unset when the route has no selectable reasoning. An undeclared floor for + * selectable efforts rejects with `UNSUPPORTED_REASONING_SELECTION`. + * @param request - provider/model route and concrete or intent-based controls. * @param signal - optional cancellation for adapter-owned capability lookup. * @returns a prepared config and its registration-bound stream entry point. */ - async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise { - const registration = this.registration(config.provider) - const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal) - const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model) + async prepareCall(request: LlmCallRequest, signal?: AbortSignal): Promise { + const registration = this.registration(request.provider) + const adapterCall = await registration.adapter.prepareCall(request.provider, request.model, signal) + const modelInfo = this.normalizeModelInfo(registration, request.model, adapterCall.model) + const { reasoningEffort: selection, ...controls } = request + if (typeof selection === 'object' + && modelInfo.reasoning !== undefined && modelInfo.reasoning.floorEffort === undefined) { + throw new LlmError( + `provider "${request.provider}" model "${request.model}" does not declare a minimum reasoning effort`, + 'UNSUPPORTED_REASONING_SELECTION', + ) + } + const reasoningEffort = typeof selection === 'object' ? modelInfo.reasoning?.floorEffort : selection + const config: LlmCallConfig = { + ...controls, + ...reasoningEffort === undefined ? {} : { reasoningEffort }, + } const resolved = this.resolveCallWithInfo(config, modelInfo) const resolvedConfig = deepFreeze(structuredClone(resolved.config)) const context = resolved.context === undefined ? undefined : deepFreeze(structuredClone(resolved.context)) const adapterDefaults = deepFreeze({ - ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined + ...request.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined ? { reasoningEffort: true } : {}, ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index ff535365bf..b35eb497e2 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -381,9 +381,9 @@ export interface LlmModelReasoningInfo { /** Supported efforts in adapter-preferred display order. */ efforts: readonly LlmReasoningEffortInfo[] /** - * The least effort this route accepts. A caller selects it when the request - * must not spend its output on reasoning. Absence means the adapter reports - * no selectable effort. + * The least selectable effort this route accepts. Minimum-effort call + * preparation requires this field when reasoning controls are available. + * It does not guarantee that the provider disables reasoning. */ floorEffort?: ReasoningEffortId /** diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index f4aff272e3..0b09603768 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -18,6 +18,7 @@ import LlmRuntime, { createUserMessage, } from '@deepseek-ai/dsh-llm' import type { + LlmCallConfig, LlmModelContext, LlmModelInfo, LlmModelReasoningInfo, @@ -831,7 +832,7 @@ describe('LlmRuntime', () => { }) const preparedDefault = await ctx.llm.prepareCall({ provider: 'route', model: 'model' }) expect(preparedDefault.adapterDefaults).toEqual({ maxTokens: true }) - const explicit = { provider: 'route', model: 'model', maxTokens: 8_192 } + const explicit: LlmCallConfig = { provider: 'route', model: 'model', maxTokens: 8_192 } await expect(ctx.llm.resolveCallConfig(explicit)).resolves.toBe(explicit) const preparedExplicit = await ctx.llm.prepareCall(explicit) expect(preparedExplicit.adapterDefaults).toEqual({}) @@ -1031,6 +1032,65 @@ describe('LlmRuntime', () => { }) }) + it.each([ + [undefined, 'high', { reasoningEffort: true }], + [ReasoningEffortId('minimum'), 'minimum', {}], + [{ select: 'minimum' } as const, 'low', {}], + ])('prepares reasoning selection %s as a concrete effort', async (selection, expected, defaults) => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + const adapter = new class extends RecordingAdapter { + override resolveModel(provider: string, model: string): Promise { + return Promise.resolve({ + provider, id: model, name: model, + reasoning: { + efforts: ['high', 'minimum', 'low'].map(id => ({ id: ReasoningEffortId(id), name: id })), + floorEffort: ReasoningEffortId('low'), + defaultEffort: ReasoningEffortId('high'), + }, + }) + } + }(SCRIPT) + ctx.llm.registerAdapter(['route'], adapter) + const prepared = await ctx.llm.prepareCall({ + provider: 'route', model: 'model', + ...selection === undefined ? {} : { reasoningEffort: selection }, + }) + expect(prepared.config.reasoningEffort).toBe(expected) + expect(prepared.adapterDefaults).toEqual(defaults) + await collect(prepared.stream({ ...prepared.config, messages: [] })) + expect(adapter.lastOptions?.reasoningEffort).toBe(expected) + }) + + it('leaves minimum reasoning unspecified when a route has no selectable efforts', async () => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + const adapter = new RecordingAdapter(SCRIPT) + ctx.llm.registerAdapter(['route'], adapter) + const prepared = await ctx.llm.prepareCall({ + provider: 'route', model: 'model', reasoningEffort: { select: 'minimum' }, + }) + expect(prepared.config).not.toHaveProperty('reasoningEffort') + expect(prepared.adapterDefaults).toEqual({}) + await collect(prepared.stream({ ...prepared.config, messages: [] })) + expect(adapter.lastOptions).not.toHaveProperty('reasoningEffort') + }) + + it('rejects minimum selection when the advertised efforts declare no floor', async () => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + ctx.llm.registerAdapter(['route'], new CatalogAdapter( + { id: 'route', name: 'Route' }, [], {}, + { model: { efforts: [{ id: ReasoningEffortId('high'), name: 'High' }] } }, + )) + await expect(ctx.llm.prepareCall({ + provider: 'route', model: 'model', reasoningEffort: { select: 'minimum' }, + })).rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_SELECTION' }) + await expect(ctx.llm.prepareCall({ + provider: 'route', model: 'model', reasoningEffort: ReasoningEffortId('low'), + })).rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_EFFORT' }) + }) + it('reuses one exact-model lookup for prepared config and context metadata', async () => { const ctx = new Context() await ctx.plugin(LlmRuntime) @@ -1082,7 +1142,13 @@ describe('LlmRuntime', () => { override prepareCall(provider: string, model: string) { const captured = generation return Promise.resolve({ - model: { provider, id: model, name: model, inputModalities: ['text'] as const }, + model: { + provider, id: model, name: model, inputModalities: ['text'] as const, + reasoning: { + efforts: [{ id: ReasoningEffortId(captured), name: captured }], + floorEffort: ReasoningEffortId(captured), + }, + }, stream: (options: GenerateOptions) => { dispatched = captured return super.stream(options) @@ -1092,8 +1158,11 @@ describe('LlmRuntime', () => { }(SCRIPT) ctx.llm.registerAdapter(['route'], adapter) - const prepared = await ctx.llm.prepareCall({ provider: 'route', model: 'model' }) + const prepared = await ctx.llm.prepareCall({ + provider: 'route', model: 'model', reasoningEffort: { select: 'minimum' }, + }) generation = 'second' + expect(prepared.config.reasoningEffort).toBe('first') expect(prepared.inputModalities).toEqual(['text']) expect(Object.isFrozen(prepared.inputModalities)).toBe(true) await collect(prepared.stream({ ...prepared.config, messages: [] })) diff --git a/packages/session/session-title-llm/README.i18n.yaml b/packages/session/session-title-llm/README.i18n.yaml index abe2dfe55e..cd5079dd3a 100644 --- a/packages/session/session-title-llm/README.i18n.yaml +++ b/packages/session/session-title-llm/README.i18n.yaml @@ -21,11 +21,11 @@ en: cf61c021b1bdb52f zh: dac17588e4143815 /deepseek-ai-dsh-session-title-llm/use-this-package/route-and-failure-contract: - en: df5a8011c87fc6ca - zh: 86eaa45c30d4e2ac + en: ef4c7003eaa84cf6 + zh: 5a13dde678ea3e14 /deepseek-ai-dsh-session-title-llm/use-this-package/configuration: - en: 2fca3f5b5a4ab43c - zh: cdc5b285e31cc4bf + en: b06986867581393e + zh: 9b4d0b1ac115f800 /deepseek-ai-dsh-session-title-llm/understand-the-implementation: en: aa0fd15305df90bb zh: d28e3a28bb324167 @@ -36,8 +36,8 @@ en: 0fdc928704abdd3a zh: bd4c0a7dd47578cf /deepseek-ai-dsh-session-title-llm/understand-the-implementation/request-flow: - en: 5a093319f914059b - zh: f881f282bbd2fbf5 + en: a47f30c863234c24 + zh: 6d36b42b872a6d5d /deepseek-ai-dsh-session-title-llm/further-exploration: en: 641f31c35c48916b zh: 8e809fbdfef61b61 @@ -57,8 +57,8 @@ en: 5d3b5b7c8d8b4b67 zh: 1b231d96bf6ca545 /deepseek-ai-dsh-session-title-llm/known-limitations-and-deferred-work: - en: 43c7d18b582537dc - zh: 984df0095b18adb9 + en: 990bc9065ed9f225 + zh: c8848b7d05e3eeb0 /deepseek-ai-dsh-session-title-llm/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/session/session-title-llm/README.md b/packages/session/session-title-llm/README.md index 577c2d950b..2317508f6a 100644 --- a/packages/session/session-title-llm/README.md +++ b/packages/session/session-title-llm/README.md @@ -33,7 +33,7 @@ A provider plugin calls `registerSessionTitleLlmProvider(ctx, config, id, automa ### Route and failure contract -`provider` and `model` overrides are optional but must be supplied together as non-empty strings. Without that pair, the helper uses the exact provider/model route captured from the current session's logged `request/header`, so an explicit refresh before any route exists needs overrides. The helper measures the final JSON-framed user prompt against `maxInputBytes` before logging or dispatch instead of truncating it, and rechecks timeout and caller cancellation while consuming the stream and after it completes, so a late successful result cannot be accepted even if an interceptor or adapter ignores abort. Malformed or empty output, tool calls, and non-stop finish reasons reject; the session-title service decides whether that rejection is an automatic warning or an explicit caller failure. The accepted title is the first non-empty line of the model's text output, with one emphasis pair removed when it wraps that whole line, so commentary a model writes after the title cannot become the title. The dispatched output cap is the smaller of `maxOutputTokens` and the cap the session's latest folded `request/header` recorded; a session whose header records no cap leaves `maxOutputTokens` in force. +`provider` and `model` overrides are optional but must be supplied together as non-empty strings. Without that pair, the helper uses the exact provider/model route captured from the current session's logged `request/header`, so an explicit refresh before any route exists needs overrides. The helper measures the final JSON-framed user prompt against `maxInputBytes` before logging or dispatch instead of truncating it, and rechecks timeout and caller cancellation while consuming the stream and after it completes, so a late successful result cannot be accepted even if an interceptor or adapter ignores abort. Malformed or empty output, tool calls, and non-stop finish reasons reject; the session-title service decides whether that rejection is an automatic warning or an explicit caller failure. The accepted title is the first non-empty line of the model's text output, with one emphasis pair removed when it wraps that whole line, so commentary a model writes after the title cannot become the title. The title request uses `maxOutputTokens` independently of the conversation's cap. Its route must have a registered adapter so preparation can resolve the request before it is recorded. ### Configuration @@ -46,7 +46,7 @@ Every field is required except the paired route override; there are no library d | `targetWords` | required | Target word count for non-CJK titles | | `targetCjkCharacters` | required | Target character count for Chinese, Japanese, or Korean titles | | `maxInputBytes` | required | UTF-8 byte ceiling for the final JSON-framed user prompt | -| `maxOutputTokens` | required | Output-token ceiling; dispatch uses the smaller of this and the cap the session header recorded | +| `maxOutputTokens` | required | Title output-token cap, independent of conversation requests | | `timeoutMs` | required | End-to-end deadline within the runtime timer limit | | `provider`, `model` | optional | Explicit route; both or neither | @@ -72,7 +72,7 @@ One shared policy so provider plugins cannot drift: config validation, route res ### Request flow -A generation validates the config once at registration; each revision frames the selected messages as JSON, measures the framed prompt's UTF-8 bytes against `maxInputBytes`, resolves the route (the explicit pair or the logged `request/header`), reads the routed model's floor effort, appends a log-only `session/title-llm-request` event carrying the exact dispatchable request, then streams through `ctx.llm` under a composed timeout and cancellation deadline. The dispatched envelope carries `purpose: 'session-title'` for replay labeling and that floor effort when the route reports one, and deliberately lacks the agent loop's process-local request identity; a title request therefore does not spend its output ceiling on reasoning: the DeepSeek adapter disables thinking, and the pi-ai adapter dispatches the lowest level its model declares. Output assembles into text blocks only; tool calls, malformed or empty output, and non-stop finish reasons reject, and a later model failure leaves the request record intact. +Each revision frames the selected messages as JSON and checks `maxInputBytes`. `ctx.llm.prepareCall()` resolves minimum reasoning under the selected route's captured adapter generation. The helper records the exact input, output cap, and resolved effort in `session/title-llm-request`, then dispatches through that prepared call under the shared deadline. `purpose: 'session-title'` supplies attribution only. The request has no agent-loop identity and does not enter conversation history. Generation failures preserve the request record. @@ -117,7 +117,7 @@ These limits define the accepted generation shapes. They are current package con - **Text output only** — the helper accepts text output and rejects tool calls; structured-output adapters and provider-specific prompt variants are not exposed. - **Whole-prompt byte ceiling** — it enforces a byte ceiling for the whole framed user prompt rather than clipping individual messages or applying a retention policy. -- **Output ceiling follows the session header** — the dispatched cap is the smaller of `maxOutputTokens` and the cap the latest folded `request/header` recorded, which is the newest routed request rather than necessarily the request whose prompt is being titled. A deployment that caps main requests tightly also caps its titles, and a model that cannot stop reasoning then has that much less room for visible text; a title routed through an explicit `provider`/`model` override is bounded by the main route's cap instead of the override model's own. +- **Minimum reasoning is capability-dependent** — routes with selectable efforts must declare their minimum. Routes without selectable reasoning leave effort unspecified; minimum selection does not guarantee zero reasoning tokens or a complete title within the output cap. ### Dev Note diff --git a/packages/session/session-title-llm/README.zh.md b/packages/session/session-title-llm/README.zh.md index 39c31c9877..e9717c965f 100644 --- a/packages/session/session-title-llm/README.zh.md +++ b/packages/session/session-title-llm/README.zh.md @@ -33,7 +33,7 @@ kind: "package-library" ### 路由与失败约定 -`provider` 与 `model` 覆盖项都是可选的,但必须同时作为非空字符串提供。如果没有这一对取值,辅助函数使用当前会话已记录 `request/header` 中捕获的确切提供方/模型路由,因此在任何路由出现前显式刷新时必须提供覆盖项。辅助函数在记录或分发前,依据 `maxInputBytes` 检查最终 JSON 封装用户提示词的大小,而不是将其截断,并在消费流期间与完成后重新检查超时与调用方取消,因此即使拦截器或适配器忽略 abort,也不能接受迟到的成功结果。格式错误或空输出、工具调用与非 stop 结束原因都会拒绝;会话标题服务决定该拒绝属于自动警告还是显式调用方失败。被接受的标题取模型文本输出的第一个非空行;当整行被一对强调标记包裹时去掉这对标记,因此模型写在标题之后的说明不会成为标题。分发的输出上限取 `maxOutputTokens` 与会话最新折叠 `request/header` 记录的上限中的较小值;会话 header 未记录上限时,`maxOutputTokens` 生效。 +`provider` 与 `model` 覆盖项都是可选的,但必须同时作为非空字符串提供。如果没有这一对取值,辅助函数使用当前会话已记录 `request/header` 中捕获的确切提供方/模型路由,因此在任何路由出现前显式刷新时必须提供覆盖项。辅助函数在记录或分发前,依据 `maxInputBytes` 检查最终 JSON 封装用户提示词的大小,而不是将其截断,并在消费流期间与完成后重新检查超时与调用方取消,因此即使拦截器或适配器忽略 abort,也不能接受迟到的成功结果。格式错误或空输出、工具调用与非 stop 结束原因都会拒绝;会话标题服务决定该拒绝属于自动警告还是显式调用方失败。被接受的标题取模型文本输出的第一个非空行;当整行被一对强调标记包裹时去掉这对标记,因此模型写在标题之后的说明不会成为标题。标题请求独立使用 `maxOutputTokens`,不继承对话请求的上限。其路由必须注册适配器,以便在记录前完成请求准备。 ### 配置 @@ -46,7 +46,7 @@ kind: "package-library" | `targetWords` | 必填 | 非 CJK 标题的目标词数 | | `targetCjkCharacters` | 必填 | 中文、日文或韩文标题的目标字符数 | | `maxInputBytes` | 必填 | 最终 JSON 封装用户提示词的 UTF-8 字节上限 | -| `maxOutputTokens` | 必填 | 输出 token 上限;分发取它与会话 header 记录上限的较小值 | +| `maxOutputTokens` | 必填 | 标题输出 token 上限,独立于对话请求 | | `timeoutMs` | 必填 | 运行时定时器限制内的端到端时限 | | `provider`, `model` | 可选 | 显式路由;二者同时提供或同时省略 | @@ -72,7 +72,7 @@ kind: "package-library" ### 请求流程 -生成在注册时校验一次配置;每次修订把选中的消息封装为 JSON,依据 `maxInputBytes` 检查封装提示词的 UTF-8 字节数,解析路由(显式对或已记录 `request/header`),读取路由模型的最低强度,追加一条携带确切可分发请求的仅日志 `session/title-llm-request` 事件,然后在组合的超时与取消截止时间内通过 `ctx.llm` 流式生成。分发的封套携带用于重放标注的 `purpose: 'session-title'`,并在路由公布时携带该最低强度,且有意不包含 agent loop 的进程本地请求身份;因此标题请求不把输出上限花在推理上:DeepSeek 适配器禁用思考,pi-ai 适配器分派其模型声明的最低等级。输出只组装为文本块;工具调用、格式错误或空输出与非 stop 结束原因都会拒绝,后续模型失败会保留请求记录。 +每次修订把选中的消息封装为 JSON,并检查 `maxInputBytes`。`ctx.llm.prepareCall()` 在所选路由捕获的适配器代次中解析最低推理强度。辅助函数把确切输入、输出上限与解析后的强度记录到 `session/title-llm-request`,然后在共享截止时间内通过该已准备调用分发。`purpose: 'session-title'` 仅提供归属标注。请求不带 agent loop 身份,也不进入对话历史。生成失败仍保留请求记录。 @@ -117,7 +117,7 @@ kind: "package-library" - **仅文本输出**——辅助函数只接受文本输出并拒绝工具调用;不公开结构化输出适配器或提供方专用提示词变体。 - **整体提示词字节上限**——它对整个封装用户提示词强制执行字节上限,而不是剪裁单条消息或应用保留策略。 -- **输出上限跟随会话 header**——分发上限取 `maxOutputTokens` 与最新折叠 `request/header` 记录的上限中的较小值;该 header 是最新一次带路由的请求,不一定正是当前取标题的请求。把主请求上限配得很紧的部署同样会收紧标题,无法停止推理的模型留给可见文本的空间随之变小;通过显式 `provider`/`model` 覆盖路由的标题,受主路由上限约束,而不受覆盖模型自身上限保护。 +- **最低推理依赖能力声明**——提供可选强度的路由必须声明最低强度。没有可选推理控制的路由不指定强度;最低强度选择不保证零推理 token,也不保证在输出上限内生成完整标题。 ### 开发备注 diff --git a/packages/session/session-title-llm/src/index.ts b/packages/session/session-title-llm/src/index.ts index 7c62d9bd3e..be1397024c 100644 --- a/packages/session/session-title-llm/src/index.ts +++ b/packages/session/session-title-llm/src/index.ts @@ -44,6 +44,8 @@ export interface SessionTitleLlmRequestEventData { readonly messages: Message[] /** Exact auxiliary output-token cap. */ readonly maxTokens: number + /** Resolved reasoning effort when recorded; older requests can omit it. */ + readonly reasoningEffort?: ReasoningEffortId } declare module '@deepseek-ai/dsh-session/types' { @@ -64,7 +66,7 @@ export interface SessionTitleLlmConfig { readonly targetCjkCharacters: number /** Maximum UTF-8 bytes in the final JSON-framed user prompt. */ readonly maxInputBytes: number - /** Configured output-token ceiling; dispatch uses the smaller of this and the cap the session's header recorded. */ + /** Output-token cap for the title request, independent of conversation requests. */ readonly maxOutputTokens: number /** End-to-end auxiliary request deadline in milliseconds. */ readonly timeoutMs: number @@ -191,42 +193,6 @@ function resolveRoute( return request.route } -/** - * Bound the configured output cap by the cap the session's current main - * request materialized, so a route the deployment capped below the configured - * value is never asked for more. A session whose header records no cap leaves - * the configured cap unchanged. - * @param config - validated model-provider policy. - * @param request - service-owned session of this title request. - * @returns the smaller of the configured cap and the logged request cap. - */ -function titleMaxTokens( - config: ResolvedSessionTitleLlmConfig, - request: SessionTitleProviderRequest, -): number { - const materialized = request.session.requestHeader()?.config.maxTokens - return materialized === undefined ? config.maxOutputTokens : Math.min(config.maxOutputTokens, materialized) -} - -/** - * The least effort the routed model accepts, so a title request does not spend - * its output cap on reasoning. A route with no registered adapter cannot report - * model metadata, and a route that reports no selectable effort dispatches with - * none; both keep the adapter's own default in force. - * @param ctx - context exposing the registered LLM service. - * @param route - exact provider and model route of this title request. - * @param signal - cancellation covering the lookup. - * @returns the model's floor effort, or undefined when it reports none. - */ -async function titleReasoningEffort( - ctx: Context, - route: SessionTitleModelIdentity, - signal: AbortSignal, -): Promise { - if (!ctx.llm.listProviders().some(provider => provider.id === route.provider)) return undefined - return (await ctx.llm.resolveModelInfo(route.provider, route.model, signal)).reasoning?.floorEffort -} - /** Stable language-aware system instruction shared by both provider plugins. */ function systemPrompt(config: ResolvedSessionTitleLlmConfig): string { return [ @@ -314,17 +280,18 @@ export async function generateSessionTitleWithLlm( })] const system = systemPrompt(config) using callDeadline = deadline(request.signal, config.timeoutMs, SESSION_TITLE_TIMEOUT_CODE) - const maxTokens = titleMaxTokens(config, request) - const floorEffort = await titleReasoningEffort(ctx, route, callDeadline.signal) + const maxTokens = config.maxOutputTokens + const call = await ctx.llm.prepareCall({ + ...route, + maxTokens, + reasoningEffort: { select: 'minimum' }, + }, callDeadline.signal) const options: GenerateOptions = deepFreeze({ - provider: route.provider, - model: route.model, + ...call.config, messages, system, - maxTokens, sessionId: request.session.id, purpose: 'session-title', - ...floorEffort === undefined ? {} : { reasoningEffort: floorEffort }, signal: callDeadline.signal, }) request.session.append('session/title-llm-request', { @@ -334,10 +301,11 @@ export async function generateSessionTitleWithLlm( system, messages, maxTokens, + ...call.config.reasoningEffort === undefined ? {} : { reasoningEffort: call.config.reasoningEffort }, }) callDeadline.signal.throwIfAborted() const assembler = new BlockAssembler() - for await (const chunk of ctx.llm.stream(options)) { + for await (const chunk of call.stream(options)) { callDeadline.signal.throwIfAborted() assembler.push(chunk) } diff --git a/packages/session/session-title-llm/tests/llm.spec.ts b/packages/session/session-title-llm/tests/llm.spec.ts index 341f6ade61..f2a65d6a7b 100644 --- a/packages/session/session-title-llm/tests/llm.spec.ts +++ b/packages/session/session-title-llm/tests/llm.spec.ts @@ -201,10 +201,11 @@ describe('generateSessionTitleWithLlm', () => { system: options.system, messages: options.messages, maxTokens: 32, + reasoningEffort: ReasoningEffortId('off'), }) }) - it('bounds the dispatched output cap by a smaller cap the session request recorded', async () => { + it('keeps the title output cap independent of a smaller conversation cap', async () => { const { ctx, adapter } = await withScript(SCRIPT) const providerRequest = request(ctx, undefined, 8) @@ -216,9 +217,9 @@ describe('generateSessionTitleWithLlm', () => { TITLE_PROVIDER, )).resolves.toMatchObject({ title: '五个字标题' }) - expect(adapter.requests[0]?.maxTokens).toBe(8) + expect(adapter.requests[0]?.maxTokens).toBe(32) expect(providerRequest.session.snapshotEvents() - .findLast(event => event.type === 'session/title-llm-request')?.data.maxTokens).toBe(8) + .findLast(event => event.type === 'session/title-llm-request')?.data.maxTokens).toBe(32) }) it('keeps the configured output cap when the session request recorded a larger one', async () => { @@ -236,13 +237,11 @@ describe('generateSessionTitleWithLlm', () => { expect(adapter.requests[0]?.maxTokens).toBe(32) }) - it('sends no effort when the route has no registered adapter', async () => { + it('rejects an unregistered route before recording or dispatching a title request', async () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(LlmRuntime) const requests: GenerateOptions[] = [] - // A composition can serve a route without a registered adapter, as the - // recorded replay lanes do; the lookup then reports no floor. ctx.on('llm/stream', (options: GenerateOptions) => { requests.push(options) return (async function* (): AsyncIterable { yield * SCRIPT })() @@ -255,9 +254,10 @@ describe('generateSessionTitleWithLlm', () => { providerRequest, providerRequest.messages, TITLE_PROVIDER, - )).resolves.toMatchObject({ title: '五个字标题' }) + )).rejects.toMatchObject({ code: 'NO_ADAPTER' }) - expect(requests[0]).not.toHaveProperty('reasoningEffort') + expect(requests).toEqual([]) + expect(providerRequest.session.snapshotEvents().some(event => event.type === 'session/title-llm-request')).toBe(false) }) it('sends no effort when the route reports no selectable level', async () => { diff --git a/packages/test-support/llm-replay/README.i18n.yaml b/packages/test-support/llm-replay/README.i18n.yaml index cc8d1c27ee..90b66a1319 100644 --- a/packages/test-support/llm-replay/README.i18n.yaml +++ b/packages/test-support/llm-replay/README.i18n.yaml @@ -18,8 +18,8 @@ en: e2116be9c7a5c18f zh: dc89da423decf4aa /deepseek-ai-dsh-llm-replay/use-this-package/mounting-it: - en: 8885809b3bc7da3e - zh: cb3f1a3268164d11 + en: 431b305f7f1f5d02 + zh: 227dcbaeaf4bdfef /deepseek-ai-dsh-llm-replay/use-this-package/how-the-fixture-works: en: d7dd048cd69e723d zh: 9f5dfa5186549cd0 diff --git a/packages/test-support/llm-replay/README.md b/packages/test-support/llm-replay/README.md index 58149f64e2..56c8a597b6 100644 --- a/packages/test-support/llm-replay/README.md +++ b/packages/test-support/llm-replay/README.md @@ -29,6 +29,8 @@ This package gives a keyless test a real agent with a fixed model transcript: mo ### Mounting it +Prepared callers require the routed adapter mode. Configure `models[].floorReasoningEffort` together with `reasoningEfforts` to replay minimum-effort selection; display order alone does not establish the minimum. + With `providers` configured, the plugin registers a replay-only adapter whose catalog is available to scenarios that exercise model discovery; without `providers`, it installs the catch-all `llm/stream` waterfall used by tests that do not need discovery: ```yaml diff --git a/packages/test-support/llm-replay/README.zh.md b/packages/test-support/llm-replay/README.zh.md index ec59b78552..94e6d52df0 100644 --- a/packages/test-support/llm-replay/README.zh.md +++ b/packages/test-support/llm-replay/README.zh.md @@ -29,6 +29,8 @@ kind: "package-reference" ### 挂载它 +已准备调用要求使用路由适配器模式。将 `models[].floorReasoningEffort` 与 `reasoningEfforts` 一起配置,即可回放最低强度选择;仅凭显示顺序不能确定最低强度。 + 配置 `providers` 后,插件会注册仅用于回放的适配器,其模型目录可供测试模型发现功能的场景使用;未配置 `providers` 时,它安装无需模型发现功能的测试所用的 catch-all `llm/stream` waterfall(瀑布式事件): ```yaml diff --git a/packages/test-support/llm-replay/src/index.ts b/packages/test-support/llm-replay/src/index.ts index 43d4bec570..103ff2d881 100644 --- a/packages/test-support/llm-replay/src/index.ts +++ b/packages/test-support/llm-replay/src/index.ts @@ -98,6 +98,8 @@ export interface ReplayModelConfig { imageRequestTokens?: number /** Optional reasoning-effort ids the replay route accepts, in display order. */ reasoningEfforts?: string[] + /** Optional least selectable effort; must appear in {@link reasoningEfforts}. */ + floorReasoningEffort?: string /** * Optional effort materialized when callers omit one; must appear in * {@link reasoningEfforts} or call resolution rejects the route. @@ -932,6 +934,9 @@ class ReplayAdapter extends LlmAdapter { : { reasoning: { efforts: configuredModel.reasoningEfforts.map(id => ({ id: ReasoningEffortId(id), name: id })), + ...configuredModel.floorReasoningEffort === undefined + ? {} + : { floorEffort: ReasoningEffortId(configuredModel.floorReasoningEffort) }, ...configuredModel.defaultReasoningEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(configuredModel.defaultReasoningEffort) }, diff --git a/packages/test-support/llm-replay/tests/llm-replay.spec.ts b/packages/test-support/llm-replay/tests/llm-replay.spec.ts index f306497798..eeff300274 100644 --- a/packages/test-support/llm-replay/tests/llm-replay.spec.ts +++ b/packages/test-support/llm-replay/tests/llm-replay.spec.ts @@ -1414,6 +1414,7 @@ describe('installLlmReplay (through the real LlmRuntime)', () => { inputModalities: ['text', 'image'], defaultMaxTokens: 64_000, reasoningEfforts: ['off', 'max'], + floorReasoningEffort: 'off', defaultReasoningEffort: 'max', systemPromptUpdate: 'in-history', toolUpdate: 'addition-only', @@ -1440,6 +1441,7 @@ describe('installLlmReplay (through the real LlmRuntime)', () => { defaultMaxTokens: 64_000, reasoning: { efforts: [{ id: 'off', name: 'off' }, { id: 'max', name: 'max' }], + floorEffort: 'off', defaultEffort: 'max', }, systemPromptUpdate: 'in-history', @@ -1470,7 +1472,11 @@ describe('installLlmReplay (through the real LlmRuntime)', () => { maxDelayMs: 10_000, jitterRatio: 0.1, }) - expect(await drain(ctx.llm.stream({ provider: 'deepseek', model: 'pro', messages: [] }))).toEqual(TEXT_CHUNKS) + const prepared = await ctx.llm.prepareCall({ + provider: 'deepseek', model: 'flash', reasoningEffort: { select: 'minimum' }, + }) + expect(prepared.config.reasoningEffort).toBe('off') + expect(await drain(prepared.stream({ ...prepared.config, messages: [] }))).toEqual(TEXT_CHUNKS) dispose() expect(ctx.llm.listProviders()).toEqual([]) diff --git a/packages/test-support/loader-smoke/tests/fixtures/cli-mock-llm.ts b/packages/test-support/loader-smoke/tests/fixtures/cli-mock-llm.ts index 4dbdb23b0b..a68e771f2e 100644 --- a/packages/test-support/loader-smoke/tests/fixtures/cli-mock-llm.ts +++ b/packages/test-support/loader-smoke/tests/fixtures/cli-mock-llm.ts @@ -27,6 +27,7 @@ class CliMockAdapter extends LlmAdapter { { id: OFF, name: 'Off' }, { id: HIGH, name: 'High' }, ], + floorEffort: OFF, defaultEffort: HIGH, }, } diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 9cf231ca74..91a027e7e1 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -339,6 +339,7 @@ export const LINK_MAP: Readonly> = { DeepSeekLlmApiExtensionProvider: 'llm-streaming.md', DeepSeekLlmApiExtensionRequest: 'llm-streaming.md', LlmCallConfig: 'llm-streaming.md', + LlmCallRequest: 'llm-streaming.md', LlmModelContext: 'llm-streaming.md', LlmModelReasoningInfo: 'llm-streaming.md', LlmResolvedModelInfo: 'llm-streaming.md', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 7378a28421..ce0cf95018 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -126,6 +126,16 @@ "symbol": "LlmCallConfig", "source": "packages/llm/llm/src/call-config.ts" }, + { + "doc": "docs/subsystems/llm-streaming.md", + "symbol": "ReasoningSelection", + "source": "packages/llm/llm/src/call-config.ts" + }, + { + "doc": "docs/subsystems/llm-streaming.md", + "symbol": "LlmCallRequest", + "source": "packages/llm/llm/src/call-config.ts" + }, { "doc": "docs/subsystems/llm-streaming.md", "symbol": "LlmCallConfigAdapterDefaults", diff --git a/snapshots/sdk/session-title-after-turn/cordis.snapshot.yml b/snapshots/sdk/session-title-after-turn/cordis.snapshot.yml index e829d71eb3..8145980454 100644 --- a/snapshots/sdk/session-title-after-turn/cordis.snapshot.yml +++ b/snapshots/sdk/session-title-after-turn/cordis.snapshot.yml @@ -49,6 +49,9 @@ name: Title replay models: - id: title-model + reasoningEfforts: [high, low] + floorReasoningEffort: low + defaultReasoningEffort: high - id: session-title-provider name: '@deepseek-ai/dsh-session-title-first-prompt-llm' config: diff --git a/snapshots/sdk/session-title-after-turn/session.v4.jsonl b/snapshots/sdk/session-title-after-turn/session.v4.jsonl index 3e21e4cad4..e4b66cdafb 100644 --- a/snapshots/sdk/session-title-after-turn/session.v4.jsonl +++ b/snapshots/sdk/session-title-after-turn/session.v4.jsonl @@ -12,7 +12,7 @@ {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Reply with exactly TITLE_DONE. Do","messageSeqs":[8],"source":{"kind":"fallback"}}} -{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Reply with exactly TITLE_DONE. Do not use tools.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:4}}"}],"maxTokens":32}} +{"type":"session/title-llm-request","data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[8],"route":{"provider":"title-replay","model":"title-model"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.\nIf the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":8,\"text\":\"Reply with exactly TITLE_DONE. Do not use tools.\"}]"}],"source":{"kind":"dsh-session-title-llm"},"role":"user","id":"{{message:4}}"}],"maxTokens":32,"reasoningEffort":"low"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"TITLE_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1788699457976,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788699457976,"index":0,"dt":[],"texts":["TITLE_DONE"]},{"type":"chunk","time":1788699457976,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"TITLE_DONE"}}},{"type":"chunk","time":1788699457976,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1788699457976,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}