@deepseek-ai/dsh-llm 0.2.0-rc.2 → 0.2.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -21,8 +21,8 @@
21
21
  en: c3ecb5a1421f27db
22
22
  zh: 4bbd3c0e513845e1
23
23
  /deepseek-ai-dsh-llm/use-this-package/minimal-composition:
24
- en: 7550d716b90fcb09
25
- zh: 77891a44ee623c7a
24
+ en: 9d481d19dc52e404
25
+ zh: aa09cf4462e93ba6
26
26
  /deepseek-ai-dsh-llm/use-this-package/what-you-can-do:
27
27
  en: b45d7a414141734e
28
28
  zh: a641c8ea3827cee6
@@ -39,8 +39,8 @@
39
39
  en: ae1618577f4e8364
40
40
  zh: a6e8ab1563c08dfb
41
41
  /deepseek-ai-dsh-llm/understand-the-implementation/main-flow:
42
- en: 6dc955a0d80f6ca0
43
- zh: f81dbcaeacd8b269
42
+ en: da12553c289e1705
43
+ zh: 28827a81cd63ff84
44
44
  /deepseek-ai-dsh-llm/understand-the-implementation/invariants:
45
45
  en: 0163a050239248e5
46
46
  zh: 885c95bedcbe2ff4
package/README.md CHANGED
@@ -60,6 +60,25 @@ After a successful mount, `ctx.llm.listProviders()` reports the registered route
60
60
 
61
61
  `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.
62
62
 
63
+ `prepareCall(config, signal, configure?)` accepts concrete `LlmCallConfig` values. The optional synchronous, pure `ConfigureCall` function receives detached, deeply frozen `LlmCallControls` and model metadata from the captured adapter generation. Callers can compose ordinary functions to choose concrete controls; later writes replace earlier ones. Preparation retains the captured route, applies defaults to omitted controls, validates the result, and returns a frozen configuration for recording and dispatch. Callback failures or cancellation reject before dispatch. Preparation requires a registered adapter.
64
+
65
+ Configuration functions select controls before adapter defaults are materialized. For example, combine least-effort selection with an explicit output cap, then pass `configure(maxTokens)` as the third argument:
66
+
67
+ ```ts
68
+ import type { ConfigureCall } from '@deepseek-ai/dsh-llm'
69
+
70
+ const leastReasoning: ConfigureCall = (controls, model) => {
71
+ const first = model.reasoning?.efforts[0]
72
+ return first === undefined ? controls : { ...controls, reasoningEffort: first.id }
73
+ }
74
+
75
+ const outputLimit = (maxTokens: number): ConfigureCall =>
76
+ controls => ({ ...controls, maxTokens })
77
+
78
+ const configure = (maxTokens: number): ConfigureCall =>
79
+ (controls, model) => outputLimit(maxTokens)(leastReasoning(controls, model), model)
80
+ ```
81
+
63
82
  ### What you can do
64
83
 
65
84
  - **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`.
@@ -107,7 +126,7 @@ The service is built on one separation: **the logical contract is provider-neutr
107
126
 
108
127
  A request is validated against its exact model's capability — context window, output default, reasoning efforts, input modalities, and `systemPromptUpdate` mode — and any adapter-configured defaults are materialized. The runtime preserves freezing for already-frozen input; hand-built callers own input immutability. `prepareCall()` binds those facts, detached context, and retry policy to the exact adapter generation that performs terminal dispatch, so HMR or dynamic settings cannot combine one generation's image capability with another generation's endpoint. An image-capable adapter projects durable references into route-specific request versions; `resolveImageAttachmentAccess()` separately maps an attachment provider's optional host object into the current tool execution world without changing the request image or its `variantId`. A text-only route receives deterministic per-image placeholders, including tool-role result images, without rewriting append-only session history. Durable `FileBlock` references never reach any adapter: request assembly replaces each one, including tool-role result occurrences, with deterministic handle text naming the file and its saved read-only path, resolved through the mounted attachment and filesystem providers. `ctx.llm.fileRequestText(ref)` exposes that exact synchronous projection to request measurement. An image occurrence derived with `offloaded: true` reaches every route as placeholder text through `projectOffloadedImages()`. An image-capable route whose retained occurrences exceed its `LlmImageRequestBudget` at their exact bytes fails with `IMAGE_OFFLOAD_REQUIRED` naming the additional oldest occurrences (`requiredImageOffload()`), never with an unlogged projection; `dsh-compaction-image-offload` logs the selected occurrences in one `image/offload` event and retries. Adapters that charge visual tokens declare per-route `imageRequestPricing`, which `ctx.llm.imageRequestPricing(provider, model)` resolves synchronously for the token meter. Dispatch goes through the `llm/stream` waterfall, then chunks return as token-level deltas and every adapter outcome reaches the consumer as one terminal `finish` chunk.
109
128
 
110
- File detection reads current content, including tool-role result content, on every request without caching message identities or freeze state. The [file-scan decision](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.md) records the measured traversal cost.
129
+ File detection reads current content, including tool-role result content, on every request without caching message identities or freeze state. The [archived file-scan decision](../../../.agents/notes/archived/simplification/2026-09-07-file-content-scan.md) records the measured traversal cost.
111
130
 
112
131
  ### Invariants
113
132
 
package/README.zh.md CHANGED
@@ -60,6 +60,25 @@ for await (const chunk of ctx.llm.stream({
60
60
 
61
61
  `GenerateOptions.messages` 接受持久 `Message` 值和仅供请求使用的 `RequestUserInput` 值。仅供请求使用的输入包含 user-role 内容,不含 `id` 或 `source`;Session 写入和 Agent 投递仍然要求持久消息。调用方必须在流结束前保持辅助输入不变。会记录完整请求的调用方(例如会话标题生成)必须使用持久消息。
62
62
 
63
+ `prepareCall(config, signal, configure?)` 接受具体的 `LlmCallConfig` 值。可选的同步纯函数 `ConfigureCall` 接收已分离且深度冻结的 `LlmCallControls`,以及捕获的适配器代次所提供的模型元数据。调用方可组合普通函数来选择具体控制项;后执行的写入替换先前的值。准备操作保留捕获的路由,为省略的控制项应用默认值,校验结果,再返回用于记录与分发的冻结配置。回调失败或取消会在分发前拒绝。准备操作要求注册适配器。
64
+
65
+ 配置函数在适配器默认值物化之前选择控制项。例如,将最低强度选择与显式输出上限组合,再将 `configure(maxTokens)` 作为第三个参数传入:
66
+
67
+ ```ts
68
+ import type { ConfigureCall } from '@deepseek-ai/dsh-llm'
69
+
70
+ const leastReasoning: ConfigureCall = (controls, model) => {
71
+ const first = model.reasoning?.efforts[0]
72
+ return first === undefined ? controls : { ...controls, reasoningEffort: first.id }
73
+ }
74
+
75
+ const outputLimit = (maxTokens: number): ConfigureCall =>
76
+ controls => ({ ...controls, maxTokens })
77
+
78
+ const configure = (maxTokens: number): ConfigureCall =>
79
+ (controls, model) => outputLimit(maxTokens)(leastReasoning(controls, model), model)
80
+ ```
81
+
63
82
  ### 你可以做什么
64
83
 
65
84
  - **流式发起一次模型调用**——`ctx.llm.stream(options)` 为任何已注册提供方与模型产出原始分片(token 级增量);消费方用 `BlockAssembler` 组装。
@@ -107,7 +126,7 @@ for await (const chunk of ctx.llm.stream({
107
126
 
108
127
  请求会对照其精确模型的能力校验,包括上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式,并填入任何适配器配置的默认值。运行时保留已冻结输入的冻结状态;手动构建请求的调用方负责保证输入不可变。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR(热模块替换)或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括 tool-role 结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括 tool-role 结果中的出现)替换为确定性句柄文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。派生后带有 `offloaded: true` 的图片出现位置,经 `projectOffloadedImages()` 以占位文本到达每条路由。支持图片的路由在保留的出现位置按精确字节超过其 `LlmImageRequestBudget` 时,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置(`requiredImageOffload()`),绝不发送未记录的投影;`dsh-compaction-image-offload` 用一条 `image/offload` 事件记录所选位置并重试。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall(瀑布式事件),随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
109
128
 
110
- 文件检测在每次请求时读取当前内容,包括 tool-role 结果内容,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。
129
+ 文件检测在每次请求时读取当前内容,包括 tool-role 结果内容,不缓存消息身份或冻结状态。[已归档的文件扫描决策](../../../.agents/notes/archived/simplification/2026-09-07-file-content-scan.md)记录了实测遍历成本。
111
130
 
112
131
  ### 不变式
113
132
 
package/lib/index.js CHANGED
@@ -119,7 +119,7 @@ function createToolResultMessage(input) {
119
119
  */
120
120
  /**
121
121
  * Base class for all harness errors. Carries a `code` (stable, programmatic —
122
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
122
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
123
123
  * human-readable `message`, and supports `cause` chaining via the standard
124
124
  * `ErrorOptions`. `name` defaults to the subclass constructor name.
125
125
  */
@@ -840,7 +840,7 @@ function projectToolUpdates(messages, tools, toolUpdate, history) {
840
840
  /**
841
841
  * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
842
842
  * adapters from drifting. See
843
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
843
+ * `docs/subsystems/llm-streaming.md#appidentity--app-attribution`.
844
844
  *
845
845
  * App-attribution vocabulary for provider requests.
846
846
  * @module @deepseek-ai/dsh-llm/attribution
@@ -1802,20 +1802,14 @@ let LlmRuntime = (() => {
1802
1802
  }
1803
1803
  /** Notify topology observers without letting one broken listener veto the commit. */
1804
1804
  emitAdaptersUpdated() {
1805
- let invariantFailure;
1806
1805
  for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
1807
1806
  const returned = listener();
1808
1807
  if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
1809
1808
  this.warnAdaptersListenerFailure(error);
1810
1809
  });
1811
1810
  } catch (error) {
1812
- if (error?.code === "INVARIANT") {
1813
- invariantFailure ??= error;
1814
- continue;
1815
- }
1816
1811
  this.warnAdaptersListenerFailure(error);
1817
1812
  }
1818
- if (invariantFailure !== void 0) throw invariantFailure;
1819
1813
  }
1820
1814
  /** Contained-listener diagnostic shared by the sync and async failure paths. */
1821
1815
  warnAdaptersListenerFailure(error) {
@@ -2196,20 +2190,33 @@ let LlmRuntime = (() => {
2196
2190
  * Resolve one call under its current adapter registration. The returned
2197
2191
  * one-shot handle keeps that registration across header logging and dispatch,
2198
2192
  * so HMR cannot combine one adapter's capability result with another adapter.
2199
- * @param config - provider/model route and optional request controls.
2193
+ * An optional synchronous callback selects concrete controls using captured
2194
+ * model metadata. Defaults and validation apply to its result. Callback
2195
+ * failures and cancellation reject preparation before dispatch.
2196
+ * @param config - provider/model route and optional concrete request controls.
2200
2197
  * @param signal - optional cancellation for adapter-owned capability lookup.
2198
+ * @param configure - pure control selection, called once before defaults and validation.
2201
2199
  * @returns a prepared config and its registration-bound stream entry point.
2202
2200
  */
2203
- async prepareCall(config, signal) {
2204
- const registration = this.registration(config.provider);
2205
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
2206
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
2207
- const resolved = this.resolveCallWithInfo(config, modelInfo);
2201
+ async prepareCall(config, signal, configure) {
2202
+ signal?.throwIfAborted();
2203
+ const { provider, model, ...controls } = config;
2204
+ const registration = this.registration(provider);
2205
+ const adapterCall = await registration.adapter.prepareCall(provider, model, signal);
2206
+ signal?.throwIfAborted();
2207
+ const modelInfo = this.normalizeModelInfo(registration, model, adapterCall.model);
2208
+ const proposed = configure === void 0 ? config : {
2209
+ ...configure(deepFreeze(structuredClone(controls)), deepFreeze(modelInfo)),
2210
+ provider,
2211
+ model
2212
+ };
2213
+ signal?.throwIfAborted();
2214
+ const resolved = this.resolveCallWithInfo(proposed, modelInfo);
2208
2215
  const resolvedConfig = deepFreeze(structuredClone(resolved.config));
2209
2216
  const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
2210
2217
  const adapterDefaults = deepFreeze({
2211
- ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
2212
- ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
2218
+ ...proposed.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
2219
+ ...proposed.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
2213
2220
  });
2214
2221
  let dispatched = false;
2215
2222
  return Object.freeze({
@@ -74,7 +74,7 @@ export const TYPERT = {
74
74
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
75
75
  create: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
76
76
  },
77
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":638,"column":9},
77
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":631,"column":9},
78
78
  },
79
79
  {
80
80
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -89,7 +89,7 @@ export const TYPERT = {
89
89
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
90
90
  create: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
91
91
  },
92
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":550,"column":3},
92
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":543,"column":3},
93
93
  },
94
94
  {
95
95
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -104,7 +104,7 @@ export const TYPERT = {
104
104
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
105
105
  create: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
106
106
  },
107
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":478,"column":3},
107
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":471,"column":3},
108
108
  },
109
109
  ],
110
110
  model: {
@@ -211,9 +211,9 @@ export const TYPERT = {
211
211
  {
212
212
  "kind": "method",
213
213
  "name": "prepareCall",
214
- "signature": "async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>",
214
+ "signature": "async prepareCall( config: LlmCallConfig, signal?: AbortSignal, configure?: ConfigureCall, ): Promise<PreparedLlmCall>",
215
215
  "summary": "Resolve one call under its current adapter registration.",
216
- "jsDoc": "/**\n * Resolve one call under its current adapter registration. The returned\n * one-shot handle keeps that registration across header logging and dispatch,\n * so HMR cannot combine one adapter's capability result with another adapter.\n * @param config - provider/model route and optional request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @returns a prepared config and its registration-bound stream entry point.\n */"
216
+ "jsDoc": "/**\n * Resolve one call under its current adapter registration. The returned\n * one-shot handle keeps that registration across header logging and dispatch,\n * so HMR cannot combine one adapter's capability result with another adapter.\n * An optional synchronous callback selects concrete controls using captured\n * model metadata. Defaults and validation apply to its result. Callback\n * failures and cancellation reject preparation before dispatch.\n * @param config - provider/model route and optional concrete request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @param configure - pure control selection, called once before defaults and validation.\n * @returns a prepared config and its registration-bound stream entry point.\n */"
217
217
  },
218
218
  {
219
219
  "kind": "method",
@@ -264,6 +264,10 @@ export const TYPERT = {
264
264
  "name": "CompactionId",
265
265
  "declaration": "export type CompactionId = Branded<'CompactionId'>;"
266
266
  },
267
+ {
268
+ "name": "ConfigureCall",
269
+ "declaration": "export type ConfigureCall = (controls: Readonly<LlmCallControls>, model: Readonly<LlmResolvedModelInfo>) => LlmCallControls;"
270
+ },
267
271
  {
268
272
  "name": "ContentBlock",
269
273
  "declaration": "export type ContentBlock = ContentBlockMap[ContentBlockType];"
@@ -338,12 +342,16 @@ export const TYPERT = {
338
342
  },
339
343
  {
340
344
  "name": "LlmCallConfig",
341
- "declaration": "export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n}"
345
+ "declaration": "export interface LlmCallConfig extends LlmCallControls {\n provider: string;\n model: string;\n}"
342
346
  },
343
347
  {
344
348
  "name": "LlmCallConfigAdapterDefaults",
345
349
  "declaration": "export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n}"
346
350
  },
351
+ {
352
+ "name": "LlmCallControls",
353
+ "declaration": "export interface LlmCallControls {\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n}"
354
+ },
347
355
  {
348
356
  "name": "LlmConfigurableProvider",
349
357
  "declaration": "export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n error?: string;\n}"
@@ -414,7 +422,7 @@ export const TYPERT = {
414
422
  },
415
423
  {
416
424
  "name": "MessageSourceMap",
417
- "declaration": "export interface MessageSourceMap {\n user: { kind: 'user'; };\n model: ModelMessageSource;\n tool: ToolMessageSource;\n 'system-prompt': SystemPromptMessageSource;\n 'model-selection': { kind: 'model-selection'; } & ContextFormed;\n 'user-approval': { kind: 'user-approval'; } & ContextFormed;\n 'ptc-mode': { kind: 'ptc-mode'; };\n 'tool-registry': { kind: 'tool-registry'; };\n 'user-rpc': { kind: 'user'; rpcId: SessionRequestId; clientTimeZone?: string; };\n 'agent-message': AgentMessageSource;\n 'subagent-settled': SubagentSettledMessageSource;\n 'skill-invocation': SkillInvocationSource;\n 'cordis-host-runner': { kind: 'cordis-host-runner'; };\n goal: GoalMessageSource;\n schedule: { kind: 'schedule'; } & ContextFormed;\n 'compact-checkpoint': CompactionCheckpointSource;\n 'session-reference': SessionReferenceSource;\n 'user-question-reply': { kind: 'user-question-reply'; callId: ToolCallId; outcome: 'answered'; };\n}"
425
+ "declaration": "export interface MessageSourceMap {\n user: { kind: 'user'; };\n model: ModelMessageSource;\n tool: ToolMessageSource;\n 'system-prompt': SystemPromptMessageSource;\n 'model-selection': { kind: 'model-selection'; } & ContextFormed;\n 'working-directory': { kind: 'working-directory'; };\n 'user-approval': { kind: 'user-approval'; } & ContextFormed;\n 'ptc-mode': { kind: 'ptc-mode'; };\n 'tool-registry': { kind: 'tool-registry'; };\n 'user-rpc': { kind: 'user'; rpcId: SessionRequestId; clientTimeZone?: string; };\n 'agent-message': AgentMessageSource;\n 'subagent-settled': SubagentSettledMessageSource;\n 'skill-invocation': SkillInvocationSource;\n 'cordis-host-runner': { kind: 'cordis-host-runner'; };\n 'compact-checkpoint': CompactionCheckpointSource;\n 'user-question-reply': { kind: 'user-question-reply'; callId: ToolCallId; outcome: 'answered'; };\n goal: GoalMessageSource;\n schedule: { kind: 'schedule'; } & ContextFormed;\n 'session-reference': SessionReferenceSource;\n}"
418
426
  },
419
427
  {
420
428
  "name": "ModelMessageSource",
@@ -71,7 +71,7 @@ export const TYPERT_REMOTE = {
71
71
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
72
72
  create: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
73
73
  },
74
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":638,"column":9},
74
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":631,"column":9},
75
75
  },
76
76
  {
77
77
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -86,7 +86,7 @@ export const TYPERT_REMOTE = {
86
86
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
87
87
  create: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
88
88
  },
89
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":550,"column":3},
89
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":543,"column":3},
90
90
  },
91
91
  {
92
92
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -101,7 +101,7 @@ export const TYPERT_REMOTE = {
101
101
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
102
102
  create: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
103
103
  },
104
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":478,"column":3},
104
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":471,"column":3},
105
105
  },
106
106
  ],
107
107
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
3
3
  * adapters from drifting. See
4
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
4
+ * `docs/subsystems/llm-streaming.md#appidentity--app-attribution`.
5
5
  *
6
6
  * App-attribution vocabulary for provider requests.
7
7
  * @module @deepseek-ai/dsh-llm/attribution
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
3
3
  * adapters from drifting. See
4
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
4
+ * `docs/subsystems/llm-streaming.md#appidentity--app-attribution`.
5
5
  *
6
6
  * App-attribution vocabulary for provider requests.
7
7
  * @module @deepseek-ai/dsh-llm/attribution
@@ -5,22 +5,35 @@
5
5
  * changed snapshots instead of allowing silent per-call drift.
6
6
  * @module dsh-llm/call-config
7
7
  */
8
- import type { GenerateOptions } from './types.ts';
8
+ import type { GenerateOptions, LlmResolvedModelInfo } from './types.ts';
9
9
  import type { ReasoningEffortId } from './brand.ts';
10
+ /** Concrete generation settings; omitted controls use the selected route's defaults. */
11
+ export interface LlmCallControls {
12
+ reasoningEffort?: ReasoningEffortId;
13
+ temperature?: number;
14
+ maxTokens?: number;
15
+ stop?: string[];
16
+ }
10
17
  /**
11
18
  * Provider, model, reasoning effort, and sampling scalars of one conversation's
12
19
  * requests. Every field maps 1:1 onto the same-named `GenerateOptions` field;
13
20
  * the loop builds requests from the logged header rather than accepting these
14
21
  * per call.
15
22
  */
16
- export interface LlmCallConfig {
23
+ export interface LlmCallConfig extends LlmCallControls {
17
24
  provider: string;
18
25
  model: string;
19
- reasoningEffort?: ReasoningEffortId;
20
- temperature?: number;
21
- maxTokens?: number;
22
- stop?: string[];
23
26
  }
27
+ /**
28
+ * Synchronous, pure configuration of one call before defaults and validation.
29
+ * Compose functions in the desired order; later writes replace earlier ones.
30
+ * The returned controls cannot change the captured route. Errors reject
31
+ * preparation before dispatch; only the resolved configuration is recordable.
32
+ * @param controls - detached, deeply frozen proposed controls, without defaults.
33
+ * @param model - detached, deeply frozen metadata from the captured adapter generation.
34
+ * @returns concrete controls; omitted fields receive the route's defaults.
35
+ */
36
+ export type ConfigureCall = (controls: Readonly<LlmCallControls>, model: Readonly<LlmResolvedModelInfo>) => LlmCallControls;
24
37
  /**
25
38
  * Effective config fields supplied by exact-model adapter resolution rather
26
39
  * than by the caller's request proposal.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  /**
7
7
  * Base class for all harness errors. Carries a `code` (stable, programmatic —
8
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
8
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
9
9
  * human-readable `message`, and supports `cause` chaining via the standard
10
10
  * `ErrorOptions`. `name` defaults to the subclass constructor name.
11
11
  */
@@ -5,7 +5,7 @@
5
5
  */
6
6
  /**
7
7
  * Base class for all harness errors. Carries a `code` (stable, programmatic —
8
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
8
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
9
9
  * human-readable `message`, and supports `cause` chaining via the standard
10
10
  * `ErrorOptions`. `name` defaults to the subclass constructor name.
11
11
  */
@@ -10,7 +10,7 @@ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
10
10
  import type { GenerateOptions, LlmConfigurableProvider, LlmDiscoveredModel, LlmFailure, LlmImageRequestPricing, LlmModelContext, LlmModelDiscoveryRequest, LlmModelInfo, LlmResolvedModelInfo, LlmProviderInfo, ModelModality, StreamChunk, SystemPromptUpdate, ToolUpdate } from './types.ts';
11
11
  import type { ResolvedRetryPolicy } from './retry-policy.ts';
12
12
  import type { ProviderRequestId } from './brand.ts';
13
- import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts';
13
+ import type { ConfigureCall, LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts';
14
14
  import { HarnessError } from './error.ts';
15
15
  import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment';
16
16
  export * from './attribution.ts';
@@ -24,7 +24,7 @@ export * from './message.ts';
24
24
  export * from './retry-policy.ts';
25
25
  export { BlockAssembler } from './assembler.ts';
26
26
  export { callConfigEquals, isAgentLoopRequest, markAgentLoopRequest } from './call-config.ts';
27
- export type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts';
27
+ export type { ConfigureCall, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmCallControls } from './call-config.ts';
28
28
  declare module '@deepseek-ai/cordis' {
29
29
  interface Context {
30
30
  llm: LlmRuntime;
@@ -379,11 +379,15 @@ export declare class LlmRuntime extends TypertRemoteService {
379
379
  * Resolve one call under its current adapter registration. The returned
380
380
  * one-shot handle keeps that registration across header logging and dispatch,
381
381
  * so HMR cannot combine one adapter's capability result with another adapter.
382
- * @param config - provider/model route and optional request controls.
382
+ * An optional synchronous callback selects concrete controls using captured
383
+ * model metadata. Defaults and validation apply to its result. Callback
384
+ * failures and cancellation reject preparation before dispatch.
385
+ * @param config - provider/model route and optional concrete request controls.
383
386
  * @param signal - optional cancellation for adapter-owned capability lookup.
387
+ * @param configure - pure control selection, called once before defaults and validation.
384
388
  * @returns a prepared config and its registration-bound stream entry point.
385
389
  */
386
- prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>;
390
+ prepareCall(config: LlmCallConfig, signal?: AbortSignal, configure?: ConfigureCall): Promise<PreparedLlmCall>;
387
391
  private registration;
388
392
  /** Remove replay state whose historical route is owned by another adapter. */
389
393
  private forAdapter;
@@ -237,30 +237,22 @@ let LlmRuntime = (() => {
237
237
  emitAdaptersUpdated() {
238
238
  // Cordis emit uses Array.map: one synchronous throw starves later
239
239
  // listeners. Registry notifications are non-vetoing, so contain each
240
- // callback independently; INVARIANT-coded failures still surface.
241
- let invariantFailure;
240
+ // callback independently.
242
241
  for (const listener of this.ctx.events.dispatch('emit', ['llm/adapters-updated'])) {
243
242
  try {
244
243
  const returned = listener();
245
244
  if (returned != null && typeof returned.then === 'function') {
246
245
  // An emit listener may still be an async function; its rejection
247
- // cannot reach the synchronous INVARIANT rethrow below, so it is
248
- // contained here instead of becoming an unhandled rejection.
246
+ // is contained here instead of becoming an unhandled rejection.
249
247
  void Promise.resolve(returned).then(undefined, (error) => {
250
248
  this.warnAdaptersListenerFailure(error);
251
249
  });
252
250
  }
253
251
  }
254
252
  catch (error) {
255
- if (error?.code === 'INVARIANT') {
256
- invariantFailure ??= error;
257
- continue;
258
- }
259
253
  this.warnAdaptersListenerFailure(error);
260
254
  }
261
255
  }
262
- if (invariantFailure !== undefined)
263
- throw invariantFailure;
264
256
  }
265
257
  /** Contained-listener diagnostic shared by the sync and async failure paths. */
266
258
  warnAdaptersListenerFailure(error) {
@@ -724,24 +716,35 @@ let LlmRuntime = (() => {
724
716
  * Resolve one call under its current adapter registration. The returned
725
717
  * one-shot handle keeps that registration across header logging and dispatch,
726
718
  * so HMR cannot combine one adapter's capability result with another adapter.
727
- * @param config - provider/model route and optional request controls.
719
+ * An optional synchronous callback selects concrete controls using captured
720
+ * model metadata. Defaults and validation apply to its result. Callback
721
+ * failures and cancellation reject preparation before dispatch.
722
+ * @param config - provider/model route and optional concrete request controls.
728
723
  * @param signal - optional cancellation for adapter-owned capability lookup.
724
+ * @param configure - pure control selection, called once before defaults and validation.
729
725
  * @returns a prepared config and its registration-bound stream entry point.
730
726
  */
731
- async prepareCall(config, signal) {
732
- const registration = this.registration(config.provider);
733
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
734
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
735
- const resolved = this.resolveCallWithInfo(config, modelInfo);
727
+ async prepareCall(config, signal, configure) {
728
+ signal?.throwIfAborted();
729
+ const { provider, model, ...controls } = config;
730
+ const registration = this.registration(provider);
731
+ const adapterCall = await registration.adapter.prepareCall(provider, model, signal);
732
+ signal?.throwIfAborted();
733
+ const modelInfo = this.normalizeModelInfo(registration, model, adapterCall.model);
734
+ const proposed = configure === undefined
735
+ ? config
736
+ : { ...configure(deepFreeze(structuredClone(controls)), deepFreeze(modelInfo)), provider, model };
737
+ signal?.throwIfAborted();
738
+ const resolved = this.resolveCallWithInfo(proposed, modelInfo);
736
739
  const resolvedConfig = deepFreeze(structuredClone(resolved.config));
737
740
  const context = resolved.context === undefined
738
741
  ? undefined
739
742
  : deepFreeze(structuredClone(resolved.context));
740
743
  const adapterDefaults = deepFreeze({
741
- ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined
744
+ ...proposed.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined
742
745
  ? { reasoningEffort: true }
743
746
  : {},
744
- ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined
747
+ ...proposed.maxTokens === undefined && resolvedConfig.maxTokens !== undefined
745
748
  ? { maxTokens: true }
746
749
  : {},
747
750
  });
@@ -348,7 +348,7 @@ export interface LlmReasoningEffortInfo {
348
348
  }
349
349
  /** Selectable reasoning efforts for one exact provider/model route. */
350
350
  export interface LlmModelReasoningInfo {
351
- /** Supported efforts in adapter-preferred display order. */
351
+ /** Supported efforts from least to greatest selectable reasoning effort, not predicted token use or latency. */
352
352
  efforts: readonly LlmReasoningEffortInfo[];
353
353
  /**
354
354
  * Adapter-configured default materialized into requests when callers omit
@@ -524,8 +524,8 @@ export interface GenerateOptions {
524
524
  sessionId?: Branded<'SessionId'>;
525
525
  /**
526
526
  * Provider-neutral classification for an auxiliary model call. Adapters may
527
- * map the purpose to model-hidden transport metadata or purpose-specific
528
- * generation policy. Ordinary conversation requests leave it unset.
527
+ * map the purpose to model-hidden transport metadata. Ordinary conversation
528
+ * requests leave it unset.
529
529
  */
530
530
  purpose?: 'compaction' | 'session-title';
531
531
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-llm",
3
3
  "description": "Provider-neutral LLM service interface for the DeepSeek Harness",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,10 +18,6 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
24
- },
25
21
  "./types": {
26
22
  "types": "./lib/types/types.d.ts",
27
23
  "default": "./lib/types/types.js"
@@ -51,7 +47,6 @@
51
47
  },
52
48
  "files": [
53
49
  "lib/index.js",
54
- "lib/invariant.js",
55
50
  "lib/types/**/*.js",
56
51
  "lib/types/**/*.d.ts",
57
52
  "lib/typert.host.js",
@@ -61,20 +56,19 @@
61
56
  ],
62
57
  "license": "MIT",
63
58
  "peerDependencies": {
64
- "@deepseek-ai/cordis": "~4.0.4"
59
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
65
60
  },
66
61
  "dependencies": {
62
+ "@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.2",
63
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
67
64
  "zod": "^4.4.3",
68
- "@deepseek-ai/dsh-util-crypto": "0.2.0-rc.2",
69
- "@deepseek-ai/dsh-timeout": "0.2.0-rc.2",
70
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
71
- "@deepseek-ai/schemastery": "~3.18.4",
72
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
73
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.2"
65
+ "@deepseek-ai/dsh-timeout": "0.2.1-alpha.2",
66
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
67
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.2",
68
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2"
74
69
  },
75
70
  "devDependencies": {
76
- "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
78
- "@deepseek-ai/cordis": "~4.0.4"
71
+ "@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
72
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
79
73
  }
80
74
  }
package/lib/invariant.js DELETED
@@ -1,84 +0,0 @@
1
- //#region lib/types/invariant.js
2
- /** Package-owned LLM stream-protocol invariants. @module @deepseek-ai/dsh-llm/invariant */
3
- const PACKAGE_NAME = "@deepseek-ai/dsh-llm";
4
- /** Cordis companion plugin name. */
5
- const name = "llm-invariant";
6
- /** Service required before the companion can reserve package ownership. */
7
- const inject = ["invariants"];
8
- /** Require one chunk index to be a non-negative safe integer. */
9
- function validateIndex(index, fail) {
10
- if (!Number.isSafeInteger(index) || index < 0) fail(`LLM stream block index must be a non-negative safe integer, got ${index}`);
11
- }
12
- /** Require a delta to address an open block of its matching type. */
13
- function validateDelta(open, index, expected, fail) {
14
- validateIndex(index, fail);
15
- const actual = open.get(index);
16
- if (actual !== expected) fail(`${expected} delta at index ${index} requires an open ${expected} block, got ${String(actual)}`);
17
- }
18
- /** Wrap one provider stream and enforce its grammar as chunks are consumed. */
19
- async function* validateStream(source, fail) {
20
- const open = /* @__PURE__ */ new Map();
21
- let usageSeen = false;
22
- let finished = false;
23
- for await (const chunk of source) {
24
- if (finished) fail(`LLM stream emitted ${chunk.type} after terminal finish`);
25
- switch (chunk.type) {
26
- case "block-start":
27
- validateIndex(chunk.index, fail);
28
- if (open.has(chunk.index)) fail(`LLM stream repeated block-start index ${chunk.index}`);
29
- open.set(chunk.index, chunk.blockType);
30
- break;
31
- case "text-delta":
32
- validateDelta(open, chunk.index, "text", fail);
33
- break;
34
- case "reasoning-delta":
35
- validateDelta(open, chunk.index, "reasoning", fail);
36
- break;
37
- case "tool-call-delta":
38
- validateDelta(open, chunk.index, "tool-call", fail);
39
- break;
40
- case "block-end": {
41
- validateIndex(chunk.index, fail);
42
- const blockType = open.get(chunk.index);
43
- if (blockType === void 0) fail(`LLM stream block-end index ${chunk.index} has no open block`);
44
- if (chunk.block.type !== blockType) fail(`LLM stream block-end index ${chunk.index} closes ${chunk.block.type}, expected ${blockType}`);
45
- open.delete(chunk.index);
46
- break;
47
- }
48
- case "usage":
49
- if (usageSeen) fail("LLM stream emitted usage more than once");
50
- usageSeen = true;
51
- break;
52
- case "finish":
53
- if (open.size > 0 && chunk.reason.kind !== "error" && chunk.reason.kind !== "aborted") fail(`LLM stream finished with ${open.size} open block(s)`);
54
- finished = true;
55
- break;
56
- }
57
- yield chunk;
58
- }
59
- if (!finished) fail("LLM stream ended without a terminal finish chunk");
60
- }
61
- /** Install validation around every provider stream. */
62
- const install = (ctx, fail) => {
63
- ctx.on("llm/stream", (_options, next) => validateStream(next(), fail), {
64
- global: true,
65
- prepend: true
66
- });
67
- ctx.on("llm/adapters-updated", () => {
68
- const llm = ctx.get("llm");
69
- if (llm === void 0) return;
70
- for (const provider of llm.listProviders()) try {
71
- llm.providerRetryPolicy(provider.id);
72
- } catch {
73
- fail(`llm/adapters-updated fired while provider "${provider.id}" has no readable registration`);
74
- }
75
- }, { global: true });
76
- };
77
- /**
78
- * Register the LLM invariant companion.
79
- * @param ctx - Cordis context carrying the invariant service.
80
- * @returns the installed registration's disposer after setup succeeds.
81
- */
82
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
83
- //#endregion
84
- export { apply, inject, name };
@@ -1,13 +0,0 @@
1
- /** Package-owned LLM stream-protocol invariants. @module @deepseek-ai/dsh-llm/invariant */
2
- import type { Context } from '@deepseek-ai/cordis';
3
- /** Cordis companion plugin name. */
4
- export declare const name = "llm-invariant";
5
- /** Service required before the companion can reserve package ownership. */
6
- export declare const inject: string[];
7
- /**
8
- * Register the LLM invariant companion.
9
- * @param ctx - Cordis context carrying the invariant service.
10
- * @returns the installed registration's disposer after setup succeeds.
11
- */
12
- export declare const apply: (ctx: Context) => Promise<() => void>;
13
- //# sourceMappingURL=invariant.d.ts.map
@@ -1,100 +0,0 @@
1
- /** Package-owned LLM stream-protocol invariants. @module @deepseek-ai/dsh-llm/invariant */
2
- const PACKAGE_NAME = '@deepseek-ai/dsh-llm';
3
- /** Cordis companion plugin name. */
4
- export const name = 'llm-invariant';
5
- /** Service required before the companion can reserve package ownership. */
6
- export const inject = ['invariants'];
7
- /** Require one chunk index to be a non-negative safe integer. */
8
- function validateIndex(index, fail) {
9
- if (!Number.isSafeInteger(index) || index < 0) {
10
- fail(`LLM stream block index must be a non-negative safe integer, got ${index}`);
11
- }
12
- }
13
- /** Require a delta to address an open block of its matching type. */
14
- function validateDelta(open, index, expected, fail) {
15
- validateIndex(index, fail);
16
- const actual = open.get(index);
17
- if (actual !== expected) {
18
- fail(`${expected} delta at index ${index} requires an open ${expected} block, got ${String(actual)}`);
19
- }
20
- }
21
- /** Wrap one provider stream and enforce its grammar as chunks are consumed. */
22
- async function* validateStream(source, fail) {
23
- const open = new Map();
24
- let usageSeen = false;
25
- let finished = false;
26
- for await (const chunk of source) {
27
- if (finished)
28
- fail(`LLM stream emitted ${chunk.type} after terminal finish`);
29
- switch (chunk.type) {
30
- case 'block-start':
31
- validateIndex(chunk.index, fail);
32
- if (open.has(chunk.index))
33
- fail(`LLM stream repeated block-start index ${chunk.index}`);
34
- open.set(chunk.index, chunk.blockType);
35
- break;
36
- case 'text-delta':
37
- validateDelta(open, chunk.index, 'text', fail);
38
- break;
39
- case 'reasoning-delta':
40
- validateDelta(open, chunk.index, 'reasoning', fail);
41
- break;
42
- case 'tool-call-delta':
43
- validateDelta(open, chunk.index, 'tool-call', fail);
44
- break;
45
- case 'block-end': {
46
- validateIndex(chunk.index, fail);
47
- const blockType = open.get(chunk.index);
48
- if (blockType === undefined)
49
- fail(`LLM stream block-end index ${chunk.index} has no open block`);
50
- if (chunk.block.type !== blockType) {
51
- fail(`LLM stream block-end index ${chunk.index} closes ${chunk.block.type}, expected ${blockType}`);
52
- }
53
- open.delete(chunk.index);
54
- break;
55
- }
56
- case 'usage':
57
- if (usageSeen)
58
- fail('LLM stream emitted usage more than once');
59
- usageSeen = true;
60
- break;
61
- case 'finish':
62
- if (open.size > 0 && chunk.reason.kind !== 'error' && chunk.reason.kind !== 'aborted') {
63
- fail(`LLM stream finished with ${open.size} open block(s)`);
64
- }
65
- finished = true;
66
- break;
67
- }
68
- yield chunk;
69
- }
70
- if (!finished)
71
- fail('LLM stream ended without a terminal finish chunk');
72
- }
73
- /** Install validation around every provider stream. */
74
- const install = (ctx, fail) => {
75
- ctx.on('llm/stream', (_options, next) => validateStream(next(), fail), { global: true, prepend: true });
76
- ctx.on('llm/adapters-updated', () => {
77
- // A disposer-time emit can outlive the service-store entry during whole-
78
- // context teardown; only a live service promises a readable registry.
79
- const llm = ctx.get('llm');
80
- if (llm === undefined)
81
- return;
82
- for (const provider of llm.listProviders()) {
83
- try {
84
- llm.providerRetryPolicy(provider.id);
85
- }
86
- catch {
87
- // Reaching here IS the violation: the notification promised a readable
88
- // registry, and only that broken promise can make the lookup throw.
89
- fail(`llm/adapters-updated fired while provider "${provider.id}" has no readable registration`);
90
- }
91
- }
92
- }, { global: true });
93
- };
94
- /**
95
- * Register the LLM invariant companion.
96
- * @param ctx - Cordis context carrying the invariant service.
97
- * @returns the installed registration's disposer after setup succeeds.
98
- */
99
- export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
100
- //# sourceMappingURL=invariant.js.map