refactor(llm): report a route's floor effort and let the caller select it

`purpose` is a label for replay and transport metadata, so no adapter should
branch on one caller's value. Exact-model metadata now reports
`reasoning.floorEffort`, the least effort a route accepts, and the title
service selects it for its request. `llm-pi-ai` and `llm-deepseek` dispatch
the effort they receive and no longer recognize `session-title`; the
`minimizeReasoning` field is gone. A route with no registered adapter, such
as the recorded replay lanes serve, reports no floor, so the title request
keeps the adapter default instead of failing before dispatch.

Replaces the `minimizeReasoning` field from the previous commit.
This commit is contained in:
Tianyi Cui committed 2026-10-06 18:34:32 +08:00
1 parent 32a561f3a6
commit 9e36f20675
26 files changed
+209 -128

No files matched your search

+22 -22
View File
@@ -540,8 +540,8 @@
en: 028791baa24a6a0a
zh: 028791baa24a6a0a
/session-persistence-event-catalog/resolved-persistence-types/in-history:
en: 7ee8e3e9b3947c22
zh: 035ceca2d5031800
en: 8670ed3a7c75b1b2
zh: d0161500ca0eca47
/session-persistence-event-catalog/resolved-persistence-types/in-progress:
en: 61de2b0c9de8c15b
zh: 61de2b0c9de8c15b
@@ -1128,8 +1128,8 @@
en: 07e04c3e336f880f
zh: 6f8986ede9994fa1
/session-persistence-event-catalog/resolved-persistence-types/replayenvelope:
en: 9a80b6f5eb08eaea
zh: 061c632b8f3e4d87
en: d166ff7b19affbab
zh: a749b1a02ea2ef66
/session-persistence-event-catalog/resolved-persistence-types/requestcontext:
en: cc8bb7075d546941
zh: 77209abf30744be0
@@ -1176,8 +1176,8 @@
en: ecc15ebadb77afdf
zh: 97ac27d6a4175f6f
/session-persistence-event-catalog/resolved-persistence-types/streamchunk:
en: 0d571899222eb04f
zh: de28c6d67d1763c1
en: 4c7342a2921d0cf3
zh: 3c8d6057c9715ace
/session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent:
en: 725539dcd5c335ed
zh: 5ca08d3667f84ff9
@@ -1245,8 +1245,8 @@
en: 2258683c3cfa4ce3
zh: 5663aa76c911761b
/session-persistence-event-catalog/resolved-persistence-types/toolschema:
en: 6e1e6ebc7c1c38c8
zh: e47a3ca80c4c3b71
en: 2d99dd4714e356ea
zh: a628abffea35a7ac
/session-persistence-event-catalog/resolved-persistence-types/toolschema~2:
en: 12c14727bb7834e2
zh: b0f3c877873c36db
@@ -1890,11 +1890,11 @@
en: bb507524bcc39c53
zh: 8314898562437473
/session-persistence-event-catalog/resolved-persistence-types/type-block-end:
en: 8c5a05fa6adf3e70
zh: 66768b25d6fb3e07
en: 751e387163ebb8d9
zh: ea0f510adbd3739d
/session-persistence-event-catalog/resolved-persistence-types/type-block-start:
en: 5caed426cab416ee
zh: 86f47f90f8c440f2
en: 85410c5bb7e09732
zh: 619ff3bc86ecc65b
/session-persistence-event-catalog/resolved-persistence-types/type-chunk:
en: 2186ede3e9917aea
zh: 0cfef64045dce95b
@@ -1932,8 +1932,8 @@
en: 14e4b48d856bcf57
zh: 72ba176535dc7f3f
/session-persistence-event-catalog/resolved-persistence-types/type-finish:
en: 21627e72a24990ff
zh: d71375de325ee9f8
en: b073b33ea2bdc924
zh: 4121d8178d6f8755
/session-persistence-event-catalog/resolved-persistence-types/type-goal-change:
en: aad2516b62d3ee83
zh: e7204c4dc4e8e408
@@ -1965,8 +1965,8 @@
en: 34e5591d75322649
zh: a216839f305c9796
/session-persistence-event-catalog/resolved-persistence-types/type-reasoning-delta:
en: a18feb76be195fa0
zh: 3db7147ee4c5c963
en: 32be5bf5ee54cfba
zh: 5f28dfcea660d09a
/session-persistence-event-catalog/resolved-persistence-types/type-request-context:
en: f8addb1a01da9ca1
zh: 4ef11ebddd302c05
@@ -2025,8 +2025,8 @@
en: 1484738718de5312
zh: 3e6fbe9c64068225
/session-persistence-event-catalog/resolved-persistence-types/type-text-delta:
en: 47ae2d57be710450
zh: b072753349ac505e
en: e8736975e930c16f
zh: 61b5e30e58dc6aa1
/session-persistence-event-catalog/resolved-persistence-types/type-todo-write:
en: 2fa7ba77598a95b4
zh: 50972b51d77e9155
@@ -2034,8 +2034,8 @@
en: 32f732b97739e4d2
zh: f288e48e356969c0
/session-persistence-event-catalog/resolved-persistence-types/type-tool-call-delta:
en: 1df89f427ccac815
zh: 1bec3d025ea1f7e3
en: d32aa20dc53f4ad9
zh: 05ff20fcf7d6acd1
/session-persistence-event-catalog/resolved-persistence-types/type-tool-workflow-agent-end:
en: 8eb5e1eb95722bc7
zh: 8dd5980029600648
@@ -2067,8 +2067,8 @@
en: 1b045de96172be4b
zh: 8a15c6f35b576b37
/session-persistence-event-catalog/resolved-persistence-types/type-usage:
en: fa542ccb91698e56
zh: 7b725c813d69588d
en: 729b66be0e2b9122
zh: eaca5d90f09f9329
/session-persistence-event-catalog/resolved-persistence-types/type-user-message:
en: fb49c375aec5952c
zh: 19b80920c1068b86
+11 -11
View File
@@ -1954,7 +1954,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8`
SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5`
Sources: [`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:402`](../packages/llm/llm/src/types.ts)
`"in-history"`
@@ -4314,7 +4314,7 @@ Sources: [`packages/llm/llm/src/types.ts:68`](../packages/llm/llm/src/types.ts)
SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555`
Sources: [`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:436`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -4608,7 +4608,7 @@ Sources: [`packages/skill/skill/src/index.ts:146`](../packages/skill/skill/src/i
SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
Sources: [`packages/llm/llm/src/types.ts:452`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
One of:
@@ -5033,7 +5033,7 @@ Sources: [`packages/llm/llm/src/message.ts:173`](../packages/llm/llm/src/message
SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2`
Sources: [`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:479`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7830,7 +7830,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625`
SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
Sources: [`packages/llm/llm/src/types.ts:457`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:463`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -7844,7 +7844,7 @@ Sources: [`packages/llm/llm/src/types.ts:457`](../packages/llm/llm/src/types.ts)
SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90`
Sources: [`packages/llm/llm/src/types.ts:453`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8050,7 +8050,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8`
SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1`
Sources: [`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:465`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8224,7 +8224,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:28`](../packages/llm/llm/src
SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095`
Sources: [`packages/llm/llm/src/types.ts:455`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:461`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8544,7 +8544,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:21`](../packages/llm/llm/src
SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65`
Sources: [`packages/llm/llm/src/types.ts:454`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:460`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8592,7 +8592,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:35`](../packages/llm/llm/src
SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a`
Sources: [`packages/llm/llm/src/types.ts:456`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:462`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
@@ -8770,7 +8770,7 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8`
SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
Sources: [`packages/llm/llm/src/types.ts:464`](../packages/llm/llm/src/types.ts)
| Property | Presence | Type |
|---|---|---|
+11 -11
View File
@@ -1956,7 +1956,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8`
SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5`
来源:[`packages/llm/llm/src/types.ts:396`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:402`](../packages/llm/llm/src/types.ts)
`"in-history"`
@@ -4316,7 +4316,7 @@ SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e`
SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555`
来源:[`packages/llm/llm/src/types.ts:430`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:436`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -4610,7 +4610,7 @@ SHA-256: `672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db`
SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
来源:[`packages/llm/llm/src/types.ts:452`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
以下类型之一:
@@ -5035,7 +5035,7 @@ SHA-256: `4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731`
SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2`
来源:[`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:479`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7832,7 +7832,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625`
SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
来源:[`packages/llm/llm/src/types.ts:457`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:463`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -7846,7 +7846,7 @@ SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d`
SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90`
来源:[`packages/llm/llm/src/types.ts:453`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8052,7 +8052,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8`
SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1`
来源:[`packages/llm/llm/src/types.ts:459`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:465`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8226,7 +8226,7 @@ SHA-256: `71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd`
SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095`
来源:[`packages/llm/llm/src/types.ts:455`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:461`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8546,7 +8546,7 @@ SHA-256: `e72269f931559e901e5e8fe1cfcbd0669fb795f43cc038ff39fba62f60403c33`
SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65`
来源:[`packages/llm/llm/src/types.ts:454`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:460`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8594,7 +8594,7 @@ SHA-256: `3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6`
SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a`
来源:[`packages/llm/llm/src/types.ts:456`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:462`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
@@ -8772,7 +8772,7 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8`
SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
来源:[`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
来源:[`packages/llm/llm/src/types.ts:464`](../packages/llm/llm/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+11 -11
View File
@@ -22117,7 +22117,7 @@
"packages/llm/llm/src/types.ts#ReplayEnvelope"
],
"sources": [
"packages/llm/llm/src/types.ts:430"
"packages/llm/llm/src/types.ts:436"
]
},
{
@@ -22643,7 +22643,7 @@
"digest": "2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:455"
"packages/llm/llm/src/types.ts:461"
]
},
{
@@ -22880,7 +22880,7 @@
"digest": "3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:453"
"packages/llm/llm/src/types.ts:459"
]
},
{
@@ -23016,7 +23016,7 @@
"packages/llm/llm/src/types.ts#StreamChunk"
],
"sources": [
"packages/llm/llm/src/types.ts:452"
"packages/llm/llm/src/types.ts:458"
]
},
{
@@ -23516,7 +23516,7 @@
"packages/llm/llm/src/types.ts#ToolSchema"
],
"sources": [
"packages/llm/llm/src/types.ts:473"
"packages/llm/llm/src/types.ts:479"
]
},
{
@@ -24035,7 +24035,7 @@
"digest": "88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:456"
"packages/llm/llm/src/types.ts:462"
]
},
{
@@ -24202,7 +24202,7 @@
"digest": "95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:454"
"packages/llm/llm/src/types.ts:460"
]
},
{
@@ -24390,7 +24390,7 @@
"digest": "a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:459"
"packages/llm/llm/src/types.ts:465"
]
},
{
@@ -25071,7 +25071,7 @@
"digest": "d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:457"
"packages/llm/llm/src/types.ts:463"
]
},
{
@@ -25459,7 +25459,7 @@
"digest": "f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515",
"names": [],
"sources": [
"packages/llm/llm/src/types.ts:458"
"packages/llm/llm/src/types.ts:464"
]
},
{
@@ -25487,7 +25487,7 @@
"packages/llm/llm/src/types.ts#SystemPromptUpdate"
],
"sources": [
"packages/llm/llm/src/types.ts:396"
"packages/llm/llm/src/types.ts:402"
]
},
{
+6 -9
View File
@@ -553,6 +553,12 @@ interface LlmReasoningEffortInfo {
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.
*/
floorEffort?: ReasoningEffortId
/**
* Adapter-configured default materialized into requests when callers omit
* an effort. Absence preserves the provider's own default.
@@ -638,15 +644,6 @@ interface GenerateOptions {
* requests leave it unset.
*/
purpose?: 'compaction' | 'session-title'
/**
* Dispatch with the least reasoning the routed model supports: `off` when
* the model offers it, otherwise its lowest declared level. An auxiliary
* caller sets this when reasoning would consume the output budget it
* reserved for a short visible result. The request's own effort and the
* profile default do not apply, and an effort the model does not support is
* not validated for such a request.
*/
minimizeReasoning?: boolean
}
```
+6 -9
View File
@@ -559,6 +559,12 @@ interface LlmReasoningEffortInfo {
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.
*/
floorEffort?: ReasoningEffortId
/**
* Adapter-configured default materialized into requests when callers omit
* an effort. Absence preserves the provider's own default.
@@ -644,15 +650,6 @@ interface GenerateOptions {
* requests leave it unset.
*/
purpose?: 'compaction' | 'session-title'
/**
* Dispatch with the least reasoning the routed model supports: `off` when
* the model offers it, otherwise its lowest declared level. An auxiliary
* caller sets this when reasoning would consume the output budget it
* reserved for a short visible result. The request's own effort and the
* profile default do not apply, and an effort the model does not support is
* not validated for such a request.
*/
minimizeReasoning?: boolean
}
```
@@ -5389,7 +5389,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'GenerateOptions',
declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n messages: RequestMessage[];\n system?: string;\n tools?: ToolSchema[];\n toolHistory?: ToolHistory;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\' | \'session-title\';\n minimizeReasoning?: boolean;\n}',
declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n messages: RequestMessage[];\n system?: string;\n tools?: ToolSchema[];\n toolHistory?: ToolHistory;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\' | \'session-title\';\n}',
},
{
name: 'GenericCallView',
@@ -5765,7 +5765,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'LlmModelReasoningInfo',
declaration: 'export interface LlmModelReasoningInfo {\n efforts: readonly LlmReasoningEffortInfo[];\n defaultEffort?: ReasoningEffortId;\n}',
declaration: 'export interface LlmModelReasoningInfo {\n efforts: readonly LlmReasoningEffortInfo[];\n floorEffort?: ReasoningEffortId;\n defaultEffort?: ReasoningEffortId;\n}',
},
{
name: 'LlmProviderInfo',
+2 -2
View File
@@ -30,8 +30,8 @@
en: 3853ba1a1ffbb46c
zh: 97203e80ad933e59
/deepseek-ai-dsh-llm-deepseek/use-this-package/streaming-with-thinking-and-images:
en: 1d7ba1bedfd11a18
zh: 9e1a3372f610e9aa
en: aa523e10ffa24d7a
zh: 11c2f049652facc6
/deepseek-ai-dsh-llm-deepseek/use-this-package/dynamic-configuration:
en: 49290fc5647136b4
zh: 96fa9ce0f9e83c13
+1 -1
View File
@@ -99,7 +99,7 @@ Cached ids are scoped by endpoint and credential, refreshed before expiry, inval
Files mode bounds retained request versions by `maxRequestFilesBytes` and `maxImagesPerRequest`; inline fallback has its own base64 budget. Both remove an oldest prefix in configured byte or count quanta. Each omitted image gets its own model-visible placeholder with its display name or attachment id and, when available, normalized dimensions, media type, and current read-only path. The stepped high-watermark policy avoids rewriting an old request prefix after every new image.
`reasoningEffort` selects the advertised default. Exact-model metadata exposes ordered `off`, `low`, `high`, and `max` efforts with selection guidance when deployment policy permits thinking. `low`, `high`, and `max` enable thinking and serialize as `output_config.effort`, while adapter-owned `off` sends `thinking.type: disabled` instead. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O, and `thinking: disabled` rejects any non-`off` effort at plugin load. Requests with `GenerateOptions.minimizeReasoning` force thinking off to reserve output for a short visible result. The adapter forwards an explicit `temperature`; DeepSeek accepts it with thinking enabled but ignores its value in that mode.
`reasoningEffort` selects the advertised default. Exact-model metadata exposes ordered `off`, `low`, `high`, and `max` efforts with selection guidance when deployment policy permits thinking, and reports `off` as the floor effort that a caller selects when the request must not spend output on reasoning. `low`, `high`, and `max` enable thinking and serialize as `output_config.effort`, while adapter-owned `off` sends `thinking.type: disabled` instead. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O, and `thinking: disabled` rejects any non-`off` effort at plugin load. The adapter forwards an explicit `temperature`; DeepSeek accepts it with thinking enabled but ignores its value in that mode.
### Dynamic configuration
+1 -1
View File
@@ -99,7 +99,7 @@ Messages 和 Files 请求通过 `x-dsh-auth-token` 发送账号 token,不加 B
Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留请求版本;内联回退有独立 base64 预算。两种模式都按配置的字节或数量量子移除最旧前缀。每张省略图片都有自己的模型可见占位符,包含显示名或附件 id,以及可用时的规范化尺寸、媒体类型与当前只读路径。分阶高水位策略避免每新增一张图片都改写旧请求前缀。
`reasoningEffort` 选择公布的默认值。当部署策略允许 thinking 时,确切模型元数据会按顺序公开 `off`、`low`、`high` 与 `max` 强度及选择指引。`low`、`high` 与 `max` 启用 thinking,并以 `output_config.effort` 序列化,适配器自有的 `off` 则发送 `thinking.type: disabled`。不支持的取值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败;`thinking: disabled` 会在插件加载时拒绝任何非 `off` 强度。携带 `GenerateOptions.minimizeReasoning` 的请求会强制关闭 thinking,把输出留给简短的可见结果。适配器转发显式 `temperature`;DeepSeek 在启用 thinking 时接受该参数,但忽略其值。
`reasoningEffort` 选择公布的默认值。当部署策略允许 thinking 时,确切模型元数据会按顺序公开 `off`、`low`、`high` 与 `max` 强度及选择指引,并把 `off` 公布为最低强度,供需要避免把输出花在推理上的调用方选择。`low`、`high` 与 `max` 启用 thinking,并以 `output_config.effort` 序列化,适配器自有的 `off` 则发送 `thinking.type: disabled`。不支持的取值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败;`thinking: disabled` 会在插件加载时拒绝任何非 `off` 强度。适配器转发显式 `temperature`;DeepSeek 在启用 thinking 时接受该参数,但忽略其值。
### 动态配置
@@ -81,12 +81,14 @@ export function modelInfo(
? {
reasoning: {
efforts: OFF_ONLY_REASONING_EFFORTS,
floorEffort: OFF_REASONING_EFFORT,
defaultEffort: OFF_REASONING_EFFORT,
},
}
: {
reasoning: {
efforts: REASONING_EFFORTS,
floorEffort: OFF_REASONING_EFFORT,
defaultEffort: connection.defaults.reasoningEffort === 'off'
? OFF_REASONING_EFFORT
: connection.defaults.reasoningEffort === 'low'
+1 -1
View File
@@ -143,7 +143,7 @@ export function serialize(
}
}
if (pending.size > 0) throw new LlmError('DeepSeek Messages history ends with unresolved tools', 'INVALID_REQUEST')
const effort = options.minimizeReasoning === true ? 'off' : options.reasoningEffort ?? (connection.defaults.reasoningEffort ?? (connection.defaults.thinking === 'disabled' ? 'off' : 'high'))
const effort = options.reasoningEffort ?? (connection.defaults.reasoningEffort ?? (connection.defaults.thinking === 'disabled' ? 'off' : 'high'))
if (!['off', 'low', 'high', 'max'].includes(effort) || (connection.defaults.thinking === 'disabled' && effort !== 'off')) {
throw new LlmError(`DeepSeek Messages does not support reasoning effort ${effort}`, 'UNSUPPORTED_REASONING_EFFORT')
}
@@ -285,8 +285,9 @@ describe('Messages request conversion', () => {
expect(request.output_config).toEqual(effort === 'off' ? undefined : { effort })
})
it('disables thinking for a minimized-reasoning request, passes temperature with thinking and refuses unsupported effort', () => {
expect(body([user()], { minimizeReasoning: true, temperature: 0 })).toMatchObject({ thinking: { type: 'disabled' }, temperature: 0 })
it('disables thinking for the off effort, passes temperature with thinking and refuses unsupported effort', () => {
expect(body([user()], { reasoningEffort: ReasoningEffortId('off'), temperature: 0 }))
.toMatchObject({ thinking: { type: 'disabled' }, temperature: 0 })
expect(body([user()], { temperature: 0 })).toMatchObject({ thinking: { type: 'enabled' }, temperature: 0 })
expect(() => body([user()], { reasoningEffort: ReasoningEffortId('medium') })).toThrow(/effort/)
const disabled = resolveAdapterOptions({ thinking: 'disabled' })
@@ -396,7 +397,7 @@ describe('Messages request conversion', () => {
describe('validated configuration', () => {
it('advertises exact model metadata and allows unlisted text models', () => {
expect(modelInfo(connection, 'deepseek-official', MODEL)).toMatchObject({ context: { contextWindow: 1_000_000 }, defaultMaxTokens: 256_000, reasoning: { defaultEffort: 'high' } })
expect(modelInfo(connection, 'deepseek-official', MODEL)).toMatchObject({ context: { contextWindow: 1_000_000 }, defaultMaxTokens: 256_000, reasoning: { floorEffort: 'off', defaultEffort: 'high' } })
expect(modelInfo(connection, 'deepseek-official', 'custom').inputModalities).toEqual(['text'])
expect(modelInfo(connection, 'deepseek-official', MODEL).systemPromptUpdate).toBeUndefined()
expect(modelInfo(connection, 'deepseek-official', 'custom').systemPromptUpdate).toBeUndefined()
+2 -2
View File
@@ -30,8 +30,8 @@
en: 376f83206858b601
zh: 38bb6993609c96a6
/deepseek-ai-dsh-llm-pi-ai/use-this-package/run-with-reasoning-and-wire-compatibility:
en: 38f5251e412f2445
zh: f492ef40cdafc20e
en: b1f319877df7c6ba
zh: 351a49c549a1850f
/deepseek-ai-dsh-llm-pi-ai/use-this-package/change-configuration-at-runtime:
en: 92f554e1718614ca
zh: f17ed3ad3e5d28e2
+1 -1
View File
@@ -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.
A request carrying `GenerateOptions.minimizeReasoning` dispatches at the lowest level its model supports, whatever effort the request or profile names: `off` when the model offers it, otherwise the lowest declared level. Such a caller reserves its output budget for a short visible result, so it does not spend that budget on reasoning. `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 of these levels count toward the request's output cap. The requested effort is not validated for such a request, so a name the model does not support fails an ordinary call rather than this one.
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.
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.
+1 -1
View File
@@ -102,7 +102,7 @@ profile 的 `models` 列表会替换而非扩展路由的已安装目录;每
`reasoningEfforts` 声明模型可选择的 thinking 等级:每个键都是选择器提供的等级,其值是分派时在协议中发送的拼写,因此 `max: ultra` 可以为拥有自有词汇的网关重命名等级。省略该字段时保留已安装目录条目的能力;`false` 声明非推理模型。对于 pi-ai 无法识别的端点,`compat` 开关重塑请求——哪个角色携带系统提示词、哪个字段限制输出、thinking 等级如何传递——可逐路由、逐模型配置。条目与已安装目录都没有尺寸的模型,会采用路由的 `defaultContextWindow` 与 `defaultMaxTokens` 回退值。
携带 `GenerateOptions.minimizeReasoning` 的请求会以其模型支持的最低等级分派,不论请求或 profile 指定何种推理强度:模型提供 `off` 时使用 `off`,否则使用声明的最低等级。这类调用方把输出预算留给简短的可见结果,因此不把该预算花在推理上。`off` 的分派方式与手动选择 `off` 完全相同,因此以省略推理字段表达 `off` 的协议会沿用网关默认行为;这些等级下产生的推理 token 都计入该请求的输出上限。该请求不校验所请求的强度,因此模型不支持的名称会让普通调用失败,而不是该请求。
确切模型元数据会公布 `reasoning.floorEffort`,即该路由接受的最低等级:模型能停止推理时为 `off`,否则为声明的最低等级。需要避免把输出花在推理上的调用方选择该强度,本适配器分派收到的强度。`off` 的分派方式与手动选择 `off` 完全相同,因此以省略推理字段表达 `off` 的协议会沿用网关默认行为;所选等级下产生的推理 token 都计入该请求的输出上限。
对于自托管 Chat Completions 端点,`thinkingTokenBudgetField` 选择推理预算参数,`vllmPriority` 在服务端启用优先级调度时设置整数调度优先级。模板参数接受 `$var: thinking.budget`。`openai-responses` 网关可设置 `supportsMaxOutputTokens: false` 来省略 `max_output_tokens`;Azure 与 Codex 传输会忽略这个共享兼容字段。这些控制均需显式启用;目录拥有的 Anthropic effort 和回退能力不是可配置开关。
+8 -12
View File
@@ -169,26 +169,20 @@ function resolveReasoningLevel(
}
/**
* The level one request dispatches with. A request asking to minimize reasoning
* takes the model's lowest supported level, which is `off` whenever the model
* can stop reasoning, because reasoning a model cannot switch off still counts
* toward the output budget the caller reserved. That request deliberately
* skips the requested-effort check, so a name the model does not support fails
* an ordinary call rather than this one. Every other request validates the
* requested or profile effort.
* The level one request dispatches with: the effort the caller selected, or
* the profile default. The adapter validates that choice and dispatches it; a
* caller that wants the least reasoning for a short result selects the floor
* effort the route reports.
* @param model - the resolved model descriptor.
* @param options - the request's reasoning preference and requested effort.
* @param options - the request's requested effort.
* @param profile - the route profile carrying the default effort.
* @returns the level to dispatch, or undefined to send none, which a
* minimized-reasoning request gets only for a model whose metadata leaves it
* no supported level.
* @returns the level to dispatch, or undefined to send none.
*/
function requestReasoningLevel(
model: Model<Api>,
options: GenerateOptions,
profile: ResolvedPiAiProviderProfile,
): ModelThinkingLevel | undefined {
if (options.minimizeReasoning === true) return getSupportedThinkingLevels(model)[0]
return resolveReasoningLevel(model, options.reasoningEffort ?? profile.reasoning)
}
@@ -214,12 +208,14 @@ function reasoningInfo(
): Pick<LlmResolvedModelInfo, 'reasoning'> | Record<string, never> {
if (!model.reasoning) return {}
const levels = getSupportedThinkingLevels(model)
const floor = levels[0]
return {
reasoning: {
efforts: levels.map(level => ({
id: ReasoningEffortId(level),
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`,
})),
...floor === undefined ? {} : { floorEffort: ReasoningEffortId(floor) },
...defaultLevel === undefined ? {} : { defaultEffort: ReasoningEffortId(defaultLevel) },
},
}
+12 -7
View File
@@ -788,7 +788,7 @@ describe('provider profile lifecycle', () => {
expect(server.requests[0]).not.toHaveProperty('reasoning_effort')
})
it('dispatches a minimized-reasoning request at the lowest level each model supports', async () => {
it('reports the floor effort and dispatches the level a caller selects', async () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([
{ events: anthropicTextEvents },
@@ -813,18 +813,23 @@ describe('provider profile lifecycle', () => {
},
},
})
const request = (model: string, minimizeReasoning?: true): Promise<unknown> => assemble(ctx, {
const request = (model: string, effort: string): Promise<unknown> => assemble(ctx, {
provider: 'adaptive-gateway',
model,
reasoningEffort: ReasoningEffortId('max'),
reasoningEffort: ReasoningEffortId(effort),
maxTokens: 64,
messages: [],
...minimizeReasoning === undefined ? {} : { minimizeReasoning },
})
await request('always-thinks')
await request('always-thinks', true)
await request('may-think', true)
// Each route names its own floor, so a caller can select the least reasoning.
await expect(ctx.llm.resolveModelInfo('adaptive-gateway', 'always-thinks'))
.resolves.toMatchObject({ reasoning: { floorEffort: ReasoningEffortId('low') } })
await expect(ctx.llm.resolveModelInfo('adaptive-gateway', 'may-think'))
.resolves.toMatchObject({ reasoning: { floorEffort: ReasoningEffortId('off') } })
await request('always-thinks', 'max')
await request('always-thinks', 'low')
await request('may-think', 'off')
expect(server.requests[0]).toMatchObject({ thinking: { type: 'adaptive' }, output_config: { effort: 'max' } })
expect(server.requests[1]).toMatchObject({
+7
View File
@@ -849,10 +849,17 @@ export class LlmRuntime extends TypertRemoteService {
'INVALID_MODEL_REASONING',
)
}
if (reasoning.floorEffort !== undefined && !seen.has(reasoning.floorEffort)) {
throw new LlmError(
`adapter returned an unknown floor reasoning effort for provider "${provider}" model "${model}"`,
'INVALID_MODEL_REASONING',
)
}
return {
...info,
reasoning: {
efforts,
...reasoning.floorEffort === undefined ? {} : { floorEffort: reasoning.floorEffort },
...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
},
}
+6 -9
View File
@@ -380,6 +380,12 @@ export interface LlmReasoningEffortInfo {
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.
*/
floorEffort?: ReasoningEffortId
/**
* Adapter-configured default materialized into requests when callers omit
* an effort. Absence preserves the provider's own default.
@@ -550,13 +556,4 @@ export interface GenerateOptions {
* requests leave it unset.
*/
purpose?: 'compaction' | 'session-title'
/**
* Dispatch with the least reasoning the routed model supports: `off` when
* the model offers it, otherwise its lowest declared level. An auxiliary
* caller sets this when reasoning would consume the output budget it
* reserved for a short visible result. The request's own effort and the
* profile default do not apply, and an effort the model does not support is
* not validated for such a request.
*/
minimizeReasoning?: boolean
}
+1
View File
@@ -861,6 +861,7 @@ describe('LlmRuntime', () => {
[{ efforts: [{ id: 'valid', name: 'Valid', description: 1 }] }, 'non-string description'],
[{ efforts: [{ id: 'same', name: 'One' }, { id: 'same', name: 'Two' }] }, 'duplicate id'],
[{ efforts: [{ id: 'valid', name: 'Valid' }], defaultEffort: 'other' }, 'unknown default'],
[{ efforts: [{ id: 'valid', name: 'Valid' }], floorEffort: 'other' }, 'unknown floor'],
] as const)('rejects invalid model reasoning metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
@@ -36,8 +36,8 @@
en: 0fdc928704abdd3a
zh: bd4c0a7dd47578cf
/deepseek-ai-dsh-session-title-llm/understand-the-implementation/request-flow:
en: d60754b3baf4e947
zh: e347c55c9dba1987
en: 5a093319f914059b
zh: f881f282bbd2fbf5
/deepseek-ai-dsh-session-title-llm/further-exploration:
en: 641f31c35c48916b
zh: 8e809fbdfef61b61
+1 -1
View File
@@ -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`), 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 `minimizeReasoning`, and deliberately lacks the agent loop's process-local request identity; adapters dispatch a minimized-reasoning request with the least reasoning the model supports, so a title request does not spend its output ceiling on reasoning: the DeepSeek adapter disables thinking, and the pi-ai adapter selects the model's lowest supported level. 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.
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.
</details>
@@ -72,7 +72,7 @@ kind: "package-library"
### 请求流程
生成在注册时校验一次配置;每次修订把选中的消息封装为 JSON,依据 `maxInputBytes` 检查封装提示词的 UTF-8 字节数,解析路由(显式对或已记录 `request/header`),追加一条携带确切可分发请求的仅日志 `session/title-llm-request` 事件,然后在组合的超时与取消截止时间内通过 `ctx.llm` 流式生成。分发的封套携带用于重放标注的 `purpose: 'session-title'` 与 `minimizeReasoning`,且有意不包含 agent loop 的进程本地请求身份;适配器以模型支持的最少推理分发最小化推理请求,使标题请求不把输出上限花在推理上:DeepSeek 适配器禁用思考,pi-ai 适配器选择模型支持的最低等级。输出只组装为文本块;工具调用、格式错误或空输出与非 stop 结束原因都会拒绝,后续模型失败会保留请求记录。
生成在注册时校验一次配置;每次修订把选中的消息封装为 JSON,依据 `maxInputBytes` 检查封装提示词的 UTF-8 字节数,解析路由(显式对或已记录 `request/header`),读取路由模型的最低强度,追加一条携带确切可分发请求的仅日志 `session/title-llm-request` 事件,然后在组合的超时与取消截止时间内通过 `ctx.llm` 流式生成。分发的封套携带用于重放标注的 `purpose: 'session-title'`,并在路由公布时携带该最低强度,且有意不包含 agent loop 的进程本地请求身份;因此标题请求不把输出上限花在推理上:DeepSeek 适配器禁用思考,pi-ai 适配器分派其模型声明的最低等级。输出只组装为文本块;工具调用、格式错误或空输出与非 stop 结束原因都会拒绝,后续模型失败会保留请求记录。
</details>
@@ -14,7 +14,7 @@ declare module '@deepseek-ai/dsh-llm' {
}
}
import type { FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import type { FinishReason, GenerateOptions, Message, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { deadline, MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { deepFreeze } from '@deepseek-ai/dsh-util-values'
import type { SessionSeq } from '@deepseek-ai/dsh-session'
@@ -208,6 +208,25 @@ function titleMaxTokens(
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<ReasoningEffortId | undefined> {
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 [
@@ -296,6 +315,7 @@ 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 options: GenerateOptions = deepFreeze({
provider: route.provider,
model: route.model,
@@ -304,7 +324,7 @@ export async function generateSessionTitleWithLlm(
maxTokens,
sessionId: request.session.id,
purpose: 'session-title',
minimizeReasoning: true,
...floorEffort === undefined ? {} : { reasoningEffort: floorEffort },
signal: callDeadline.signal,
})
request.session.append('session/title-llm-request', {
@@ -1,7 +1,7 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import LlmRuntime, { createUserMessage, ToolCallId, isAgentLoopRequest, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { FinishReason, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, ToolCallId, isAgentLoopRequest, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { FinishReason, GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import { SessionTitleProviderId } from '@deepseek-ai/dsh-session-title'
import type { SessionTitleProviderRequest } from '@deepseek-ai/dsh-session-title'
@@ -19,10 +19,29 @@ class RecordingAdapter extends LlmAdapter {
constructor(
private readonly script: readonly StreamChunk[],
private readonly onDispatch?: () => void,
private readonly reasoningEfforts?: readonly string[],
) {
super()
}
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
const efforts = this.reasoningEfforts
const floor = efforts?.[0]
return Promise.resolve({
provider,
id: model,
name: model,
...efforts === undefined || floor === undefined
? {}
: {
reasoning: {
efforts: efforts.map(id => ({ id: ReasoningEffortId(id), name: id })),
floorEffort: ReasoningEffortId(floor),
},
},
})
}
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.onDispatch?.()
this.requests.push(options)
@@ -116,20 +135,20 @@ function requestWithoutRoute(ctx: Context, signal = new AbortController().signal
return { session: routed.session, messages: routed.messages, signal }
}
async function withScript(script: readonly StreamChunk[]): Promise<{
async function withScript(script: readonly StreamChunk[], reasoningEfforts?: readonly string[]): Promise<{
ctx: Context
adapter: RecordingAdapter
}> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(LlmRuntime)
const adapter = new RecordingAdapter(script)
const adapter = new RecordingAdapter(script, undefined, reasoningEfforts)
ctx.llm.registerAdapter(['current-route'], adapter)
return { ctx, adapter }
}
describe('generateSessionTitleWithLlm', () => {
it('uses the exact logged route, language targets, full framed input, and output token cap', async () => {
it('uses the exact logged route, language targets, framed input, cap, and floor effort', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(LlmRuntime)
@@ -138,7 +157,7 @@ describe('generateSessionTitleWithLlm', () => {
const adapter = new RecordingAdapter(SCRIPT, () => {
requestWasLoggedAtDispatch = providerRequest.session.snapshotEvents()
.some(event => event.type === 'session/title-llm-request')
})
}, ['off', 'low', 'high', 'max'])
ctx.llm.registerAdapter(['current-route'], adapter)
const result = await generateSessionTitleWithLlm(
@@ -166,7 +185,7 @@ describe('generateSessionTitleWithLlm', () => {
maxTokens: 32,
sessionId: providerRequest.session.id,
purpose: 'session-title',
minimizeReasoning: true,
reasoningEffort: ReasoningEffortId('off'),
})
expect(options.system).toContain('5 words')
expect(options.system).toContain('10 CJK characters')
@@ -217,6 +236,45 @@ describe('generateSessionTitleWithLlm', () => {
expect(adapter.requests[0]?.maxTokens).toBe(32)
})
it('sends no effort when the route has no registered adapter', 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<StreamChunk> { yield * SCRIPT })()
})
const providerRequest = request(ctx)
await expect(generateSessionTitleWithLlm(
ctx,
resolveSessionTitleLlmConfig(CONFIG),
providerRequest,
providerRequest.messages,
TITLE_PROVIDER,
)).resolves.toMatchObject({ title: '五个字标题' })
expect(requests[0]).not.toHaveProperty('reasoningEffort')
})
it('sends no effort when the route reports no selectable level', async () => {
const { ctx, adapter } = await withScript(SCRIPT)
const providerRequest = request(ctx)
await expect(generateSessionTitleWithLlm(
ctx,
resolveSessionTitleLlmConfig(CONFIG),
providerRequest,
providerRequest.messages,
TITLE_PROVIDER,
)).resolves.toMatchObject({ title: '五个字标题' })
expect(adapter.requests[0]).not.toHaveProperty('reasoningEffort')
})
it('uses paired explicit overrides and bounds the final framed input before model dispatch', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)