@x1a0f3n9/dsh-llm 0.1.5-rc.4 → 0.1.7-rc.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
@@ -1,6 +1,64 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/llm/llm/README.md
5
- README.md: 75a5d3e1d51ada12338a41e54b42ad8d6d21c279
6
- README.zh.md: 6c0d42dc1cb01fe8d5fc01d3384e909b6cdc044a
5
+ /:
6
+ en: 860509fb08c860f4
7
+ zh: b2c4448e8afc1f12
8
+ /x1a0f3n9-dsh-llm:
9
+ en: a5bc4cf7bfe516fe
10
+ zh: b6eb8b63a69621cc
11
+ /x1a0f3n9-dsh-llm/summary:
12
+ en: 174bcde9c848c4be
13
+ zh: bf9329d52c9cf9ce
14
+ /x1a0f3n9-dsh-llm/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /x1a0f3n9-dsh-llm/use-this-package:
18
+ en: 33738e61ce410ac0
19
+ zh: 363760460f4d4bed
20
+ /x1a0f3n9-dsh-llm/use-this-package/when-to-choose-it:
21
+ en: c3ecb5a1421f27db
22
+ zh: 4bbd3c0e513845e1
23
+ /x1a0f3n9-dsh-llm/use-this-package/minimal-composition:
24
+ en: 7550d716b90fcb09
25
+ zh: 77891a44ee623c7a
26
+ /x1a0f3n9-dsh-llm/use-this-package/what-you-can-do:
27
+ en: b45d7a414141734e
28
+ zh: a641c8ea3827cee6
29
+ /x1a0f3n9-dsh-llm/use-this-package/failures-and-recovery:
30
+ en: 311d8c92447b7094
31
+ zh: 68cf5cb1c3853cc1
32
+ /x1a0f3n9-dsh-llm/understand-the-implementation:
33
+ en: 9630d7e26193a512
34
+ zh: 94222c2a487c4b8c
35
+ /x1a0f3n9-dsh-llm/understand-the-implementation/design-philosophy:
36
+ en: 12d0444c18a397c7
37
+ zh: 9d4a5468b4bb7fcd
38
+ /x1a0f3n9-dsh-llm/understand-the-implementation/source-map:
39
+ en: ae1618577f4e8364
40
+ zh: a6e8ab1563c08dfb
41
+ /x1a0f3n9-dsh-llm/understand-the-implementation/main-flow:
42
+ en: 6dc955a0d80f6ca0
43
+ zh: f81dbcaeacd8b269
44
+ /x1a0f3n9-dsh-llm/understand-the-implementation/invariants:
45
+ en: 0163a050239248e5
46
+ zh: 885c95bedcbe2ff4
47
+ /x1a0f3n9-dsh-llm/further-exploration:
48
+ en: 37e1d696afef8a88
49
+ zh: 81f99c4507663cad
50
+ /x1a0f3n9-dsh-llm/model-experience:
51
+ en: fa5677003ad6bc0a
52
+ zh: ba33cfc860d9c37c
53
+ /x1a0f3n9-dsh-llm/model-experience/kv-cache-effect:
54
+ en: a81634136baef72f
55
+ zh: b92dda883986b0e1
56
+ /x1a0f3n9-dsh-llm/known-limitations-and-deferred-work:
57
+ en: 1a96d843acf13feb
58
+ zh: 3067b1bfe4e1d219
59
+ /x1a0f3n9-dsh-llm/known-limitations-and-deferred-work/dev-note:
60
+ en: ecbc333b88e14d88
61
+ zh: 77c725d92efd8646
62
+ /x1a0f3n9-dsh-llm/known-limitations-and-deferred-work/dev-note/open-items:
63
+ en: 6b1a7e903e13ee21
64
+ zh: bb62f2b1cc484bd5
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- Use `@x1a0f3n9/dsh-llm` to stream model calls through configured provider adapters, discover models, and resolve model capabilities and call defaults. Every dispatched request remains reconstructable from the session log. Requests are deep-frozen before dispatch, so extensions and adapters can read them but cannot rewrite them. Each stream is one provider attempt: provider-specific translation stays with its adapter, while the optional `@x1a0f3n9/dsh-llm-retry` package re-runs failed requests. Streams always end with a terminal result, so callers can handle success, failure, and cancellation consistently.
12
+ Use `@x1a0f3n9/dsh-llm` to stream model calls through configured provider adapters, discover models, and resolve model capabilities and call defaults. Callers must keep all model-visible input reconstructable from the session log. Loop-built requests arrive deep-frozen, so extensions and adapters cannot rewrite them. Each stream is one provider attempt: provider-specific translation stays with its adapter, while the optional `@x1a0f3n9/dsh-llm-retry` package re-runs failed requests. Streams always end with a terminal result, so callers can handle success, failure, and cancellation consistently.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,6 +25,8 @@ Use `@x1a0f3n9/dsh-llm` to stream model calls through configured provider adapte
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
+ `listModels` describes models offered by catalog-driven interfaces. Core resolution and streaming can still accept unlisted ids. The GUI requires catalog membership for selection and submission; adapters intended for GUI use must implement `listModels` and advertise their available models. The base implementation returns an empty list and therefore offers no GUI models.
29
+
28
30
  Any composition that calls a model provider — an agent loop, a session-title generator, a compaction summarizer — streams its requests through this service. Mount it together with at least one provider adapter; the service itself has no configuration and no provider wire code.
29
31
 
30
32
  ### When to choose it
@@ -37,7 +39,7 @@ Mount the service and at least one adapter, then select the provider by name in
37
39
 
38
40
  ```yaml
39
41
  - name: '@x1a0f3n9/dsh-llm'
40
- - name: '@x1a0f3n9/dsh-llm-deepseek'
42
+ - name: '@x1a0f3n9/dsh-llm-deepseek-api-key'
41
43
  config:
42
44
  apiKeyEnv: DEEPSEEK_API_KEY
43
45
  ```
@@ -48,7 +50,7 @@ A stream returns token-level chunks and always ends with one terminal `finish` c
48
50
  for await (const chunk of ctx.llm.stream({
49
51
  provider: 'deepseek-official',
50
52
  model: 'deepseek-v4-flash',
51
- messages: [createUserMessage({ content: [{ type: 'text', text: 'Hello' }] })],
53
+ messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }],
52
54
  })) {
53
55
  // chunks: block-start, text-delta, ..., usage, finish
54
56
  }
@@ -56,18 +58,20 @@ for await (const chunk of ctx.llm.stream({
56
58
 
57
59
  After a successful mount, `ctx.llm.listProviders()` reports the registered routes in registration order.
58
60
 
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
+
59
63
  ### What you can do
60
64
 
61
65
  - **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`.
62
66
  - **Register provider adapters** — an adapter owns one or more provider routes, and its registration captures that route's retry policy; registering the same route twice fails with `DUPLICATE_ADAPTER`.
63
67
  - **Expose and activate providers through configuration** — adapters declare configurable-provider routes plus a settings namespace, so configuration surfaces can activate dormant providers and edit connection facts without a restart. `LlmConfigurableProvider.error` reports a configuration diagnostic for repair; unaffected models can remain serviceable.
64
- - **Discover and resolve models** — list the models an adapter advertises, interrogate an endpoint for the models it serves, and resolve one exact model's context window, output default, reasoning efforts, input modalities, and system prompt update mode: `LlmResolvedModelInfo.systemPromptUpdate` is `'in-history'` when the model reads the latest `system` message at any position as the effective system prompt and absent when only a leading system message is read; `normalizeModelInfo` rejects any other value with `INVALID_MODEL_INFO`.
68
+ - **Discover and resolve models** — List the models an adapter advertises, interrogate an endpoint for the models it serves (candidates carry optional `inputModalities`; absence means unknown), and resolve one exact model's context window, output default, reasoning efforts, input modalities, and system prompt update mode: `LlmResolvedModelInfo.systemPromptUpdate` is `'in-history'` when the model reads the latest `system` message at any position as the effective system prompt and absent when only a leading system message is read; `normalizeModelInfo` rejects any other value with `INVALID_MODEL_INFO`.
65
69
  - **Validate call config** — an explicit or configured reasoning effort is checked against the exact model before any provider I/O, and an adapter-configured output cap is materialized when the request omits one.
66
70
  - **Read an embedded Assistant stream without expanding it** — `assistantStreamFirstTokenTime` (first token), `assistantStreamHasVisibleContent` (any visible content), and `assistantStreamHasVisibleText` (any visible text) answer their questions from the compact records with early exit; `lastAssistantStreamChunk` scans backward to the last raw chunk of one type, `assistantStreamChunks` and `joinAssistantStreamText` scan the whole stream, and `assembleAssistantStream` feeds a `BlockAssembler` one joined delta per run with the same blocks, usage, and replay state as the per-member expansion. `runFirstTokenTime` and `runFirstVisibleTime` do the early-exit scan for one packed run, and `isTokenDelta`, `isVisibleChunk`, and `chunkHasVisibleText` define the token and visibility rules for a single chunk. `expandAssistantStream` remains the validating path for records read at a durable boundary; it is not memoized, because a retained expansion costs roughly ten times the compact stream for as long as the event lives.
67
71
 
68
72
  ### Failures and recovery
69
73
 
70
- Every stream ends in exactly one terminal `finish` chunk: `{ kind: 'error', failure }` on failure, `{ kind: 'aborted', failure }` on cancellation. Failures carry stable codes such as `NO_ADAPTER`, `MISSING_CREDENTIAL`, `AUTH`, `RATE_LIMIT`, and `CONTEXT_WINDOW_EXCEEDED`; consumers route on the code, never on message text. A request naming an unregistered provider fails with `NO_ADAPTER`, and a malformed credential fails with `INVALID_CREDENTIAL` instead of surfacing as an opaque fetch error. This service never re-runs a request: retrying is the job of `dsh-llm-retry` at the agent's failed-step extension point.
74
+ Every stream ends in exactly one terminal `finish` chunk: `{ kind: 'error', failure }` on failure, `{ kind: 'aborted', failure }` on cancellation. Failures carry stable codes such as `NO_ADAPTER`, `MISSING_CREDENTIAL`, `AUTH`, `RATE_LIMIT`, and `CONTEXT_WINDOW_EXCEEDED`; consumers route on the code, never on message text. `QUOTA` is provider-neutral exhaustion, while `ACCOUNT_QUOTA` is reserved for a first-party account balance that the current product can replenish. A request naming an unregistered provider fails with `NO_ADAPTER`, and a malformed credential fails with `INVALID_CREDENTIAL` instead of surfacing as an opaque fetch error. This service never re-runs a request: retrying is the job of `dsh-llm-retry` at the agent's failed-step extension point.
71
75
 
72
76
  -----
73
77
 
@@ -81,7 +85,7 @@ This section explains the design behind the service; the observable behavior is
81
85
 
82
86
  ### Design philosophy
83
87
 
84
- The service is built on one separation: **the logical contract is provider-neutral, adapters own the wire.** It defines the canonical message, content-block, and stream-chunk vocabulary once, and every provider adapter translates only its own wire format into that vocabulary. The registry is the topology owner — adapter routes, configurable-provider entries, and discovery offers all register here and are disposed with their fiber — while a request stays a pure function of the session log: loop-built requests arrive deep-frozen, so listeners and adapters read them and never rewrite them.
88
+ The service is built on one separation: **the logical contract is provider-neutral, adapters own the wire.** It defines the canonical message, content-block, and stream-chunk vocabulary once, and every provider adapter translates only its own wire format into that vocabulary. The registry is the topology owner — adapter routes, configurable-provider entries, and discovery offers all register here and are disposed with their fiber — while an agent-loop request stays a pure function of the session log: loop-built requests arrive deep-frozen, so listeners and adapters read them and never rewrite them.
85
89
 
86
90
  ### Source map
87
91
 
@@ -95,19 +99,19 @@ The service is built on one separation: **the logical contract is provider-neutr
95
99
  | [`src/call-config.ts`](src/call-config.ts) | Call-config validation, adapter-default materialization, and request freezing |
96
100
  | [`src/retry-policy.ts`](src/retry-policy.ts) | Provider-owned retry policy resolution (normal and always modes) |
97
101
  | [`src/error.ts`](src/error.ts) | `HarnessError`/`LlmError` taxonomy and provider-neutral failure codes |
98
- | [`src/content.ts`](src/content.ts) | Shared file and image projection helpers, including request-image offloading |
102
+ | [`src/content.ts`](src/content.ts) | Shared file and image helpers: content walks, file projection, image offload accounting, and offloaded-image projection |
99
103
  | [`src/api-key.ts`](src/api-key.ts) | Credential format check shared by every adapter |
100
104
  | [`src/adapter-failure.ts`](src/adapter-failure.ts) | Failure normalization into terminal finish chunks |
101
105
 
102
106
  ### Main flow
103
107
 
104
- 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, then the whole request is deep-frozen. `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 nested tool-result images, without rewriting append-only session history. Durable `FileBlock` references never reach any adapter: request assembly replaces each one, nested tool-result occurrences included, 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. `offloadRequestImagesWithPolicy()` removes oldest images deterministically by raw or base64 size and count or byte quanta; the pure `offloadedImagePrefixCount()` exposes that decision so route-owned request pricing can reproduce it without building the projection. 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.
108
+ 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.
105
109
 
106
- File detection reads current content, including nested tool results, 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.
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.
107
111
 
108
112
  ### Invariants
109
113
 
110
- - **Model-visible ⟺ logged** — anything that reaches a provider request is reconstructable from the session log; loop-built requests are deep-frozen and never rewritten.
114
+ - **Model-visible ⟺ logged** — callers must keep every provider request's model-visible input reconstructable from the session log; loop-built requests arrive deep-frozen and cannot be rewritten.
111
115
  - **Replay state travels only within one adapter** — assistant replay state rides along only when the same adapter instance owns the historical and target routes; otherwise it is dropped before dispatch.
112
116
  - **Prepared calls are one-shot** — a prepared call can be dispatched exactly once, and its call-config fields must match the prepared config.
113
117
  - **Image projection follows the captured route** — durable `ImageBlock` references become route-specific request versions only for image-capable models; text-only models receive stable placeholders.
@@ -125,7 +129,7 @@ File detection reads current content, including nested tool results, on every re
125
129
  Read these pages when the package-level contract is not enough. They move from the shared types to the concrete adapters, the retry executor, and the measurement service.
126
130
 
127
131
  - [LLM streaming subsystem](../../../docs/subsystems/llm-streaming.md) — the message and block types, compact Assistant stream records, the `StreamChunk` protocol, and the adapter contract.
128
- - [llm-deepseek adapter](../llm-deepseek/README.md) — the direct DeepSeek chat-completions implementation.
132
+ - [llm-deepseek adapter](../llm-deepseek/README.md) — the direct DeepSeek Messages implementation.
129
133
  - [llm-pi-ai adapter](../llm-pi-ai/README.md) — the pi-ai-backed multi-provider implementation.
130
134
  - [llm-retry](../llm-retry/README.md) — the retry executor that re-runs failed model requests.
131
135
  - [Token meter](../token-meter/README.md) — replay-aware request and context pressure measurement.
@@ -141,7 +145,7 @@ None, as the LLM service adds no content; adapters choose when to add the shared
141
145
 
142
146
  #### KV Cache effect
143
147
 
144
- Reasoning-effort materialization preserves the assembled request prefix. Image identity and request-preview text are deterministic, while an optional execution-world path is resolved for each request; a changed path or image-offload boundary can prevent reuse from that image.
148
+ Reasoning-effort materialization preserves the assembled request prefix. Image identity and request-preview text are deterministic, while an optional execution-world path is resolved for each request; a changed path or an offload decision can prevent reuse from that image.
145
149
 
146
150
  ## Known Limitations and Deferred Work
147
151
 
@@ -152,9 +156,10 @@ These limits define where this service stops and other packages or future work b
152
156
 
153
157
  - **No retry execution, caching, or rate limiting ships in this service** — provider registration stores the retry policy, but a stream remains a single provider attempt; `@x1a0f3n9/dsh-llm-retry` executes the policy at durable agent-step boundaries.
154
158
  - **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md)).
155
- - **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant have no producer ([Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)).
159
+ - **Variants normally require a producer** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant have no producer ([Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)).
156
160
  - **`BlockAssembler` handles core block kinds only** — a plugin-added block type whose stream is never closed by `block-end` makes `blocks()` throw.
157
161
  - **`GenerateOptions.sessionId` is a locally-declared brand** — importing dsh-session's `SessionId` would create a dependency cycle.
162
+ - **Tool updates require session history** — `GenerateOptions.tools` contains active definitions. `toolHistory` supplies the initial declarations and historically resolved additions from `Session.toolHistory()`. At adapter dispatch, `projectToolUpdates` constructs deferred declarations and retains removed definitions for `in-history`; `addition-only` omits removed definitions and removal messages. Unsupported routes receive active tools without developer messages or `deferLoading`. Missing history or a request prefix omitting recorded updates falls back to current declarations without developer messages. Explicitly deferred baseline tools remain deferred until their first retained addition block; declaring a deferred tool does not activate it.
158
163
 
159
164
  <a id="dev-note"></a>
160
165
  ### Dev Note
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 使用 `@x1a0f3n9/dsh-llm` 可通过已配置的提供方适配器流式调用模型、发现模型,并解析模型能力与调用默认值。每个已分发请求都可以从会话日志重建。请求在分发前会被深度冻结,因此扩展与适配器可以读取但不能改写。每个流只尝试调用提供方一次:提供方特定的转换由对应适配器完成,可选包 `@x1a0f3n9/dsh-llm-retry` 负责重跑失败的请求。流始终以终止结果结束,因此调用方可以一致地处理成功、失败与取消。
12
+ 使用 `@x1a0f3n9/dsh-llm` 可通过已配置的提供方适配器流式调用模型、发现模型,并解析模型能力与调用默认值。调用方必须确保所有模型可见输入都可以从会话日志重建。Loop 构建的请求以深度冻结状态到达,因此扩展与适配器不能改写。每个流只尝试调用提供方一次:提供方特定的转换由对应适配器完成,可选包 `@x1a0f3n9/dsh-llm-retry` 负责重跑失败的请求。流始终以终止结果结束,因此调用方可以一致地处理成功、失败与取消。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,6 +25,8 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
+ `listModels` 描述目录驱动界面提供的模型。核心解析与流式调用仍可接受未列出的 ID。GUI 的模型选择与提交要求模型出现在目录中;供 GUI 使用的适配器必须实现 `listModels`,公布其可用模型。基类实现返回空列表,因此不向 GUI 提供模型。
29
+
28
30
  任何调用模型提供方的组合——agent loop(智能体循环)、会话标题生成器、压缩(compaction)摘要器——都会通过本服务流式发起请求。与至少一个提供方适配器一起挂载它;服务本身没有任何配置,也不包含提供方协议代码。
29
31
 
30
32
  ### 何时选择
@@ -37,7 +39,7 @@ kind: "package-reference"
37
39
 
38
40
  ```yaml
39
41
  - name: '@x1a0f3n9/dsh-llm'
40
- - name: '@x1a0f3n9/dsh-llm-deepseek'
42
+ - name: '@x1a0f3n9/dsh-llm-deepseek-api-key'
41
43
  config:
42
44
  apiKeyEnv: DEEPSEEK_API_KEY
43
45
  ```
@@ -48,7 +50,7 @@ kind: "package-reference"
48
50
  for await (const chunk of ctx.llm.stream({
49
51
  provider: 'deepseek-official',
50
52
  model: 'deepseek-v4-flash',
51
- messages: [createUserMessage({ content: [{ type: 'text', text: 'Hello' }] })],
53
+ messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }],
52
54
  })) {
53
55
  // chunks: block-start, text-delta, ..., usage, finish
54
56
  }
@@ -56,18 +58,20 @@ for await (const chunk of ctx.llm.stream({
56
58
 
57
59
  挂载成功后,`ctx.llm.listProviders()` 会按注册顺序报告已注册路由。
58
60
 
61
+ `GenerateOptions.messages` 接受持久 `Message` 值和仅供请求使用的 `RequestUserInput` 值。仅供请求使用的输入包含 user-role 内容,不含 `id` 或 `source`;Session 写入和 Agent 投递仍然要求持久消息。调用方必须在流结束前保持辅助输入不变。会记录完整请求的调用方(例如会话标题生成)必须使用持久消息。
62
+
59
63
  ### 你可以做什么
60
64
 
61
65
  - **流式发起一次模型调用**——`ctx.llm.stream(options)` 为任何已注册提供方与模型产出原始分片(token 级增量);消费方用 `BlockAssembler` 组装。
62
66
  - **注册提供方适配器**——一个适配器拥有一个或多个提供方路由,其注册会捕获该路由的重试策略;重复注册同一路由会以 `DUPLICATE_ADAPTER` 失败。
63
67
  - **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接信息,无需重启。`LlmConfigurableProvider.error` 报告供修复的配置诊断;未受影响的模型仍可提供服务。
64
- - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型,并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度、输入模态与系统提示词更新模式:当模型把任意位置最新的 `system` 消息读作有效系统提示词时,`LlmResolvedModelInfo.systemPromptUpdate` 为 `'in-history'`;只读取开头 system 消息时该字段缺失;`normalizeModelInfo` 以 `INVALID_MODEL_INFO` 拒绝任何其他值。
68
+ - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型(候选携带可选的 `inputModalities`;缺省表示未知),并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度、输入模态与系统提示词更新模式:当模型把任意位置最新的 `system` 消息读作有效系统提示词时,`LlmResolvedModelInfo.systemPromptUpdate` 为 `'in-history'`;只读取开头 system 消息时该字段缺失;`normalizeModelInfo` 以 `INVALID_MODEL_INFO` 拒绝任何其他值。
65
69
  - **校验调用配置**——显式或配置的推理强度会在任何提供方 I/O 之前对照精确模型校验;请求省略输出上限时,会填入适配器配置的输出上限。
66
70
  - **不展开即读取内嵌 Assistant 流**——`assistantStreamFirstTokenTime`(首 token)、`assistantStreamHasVisibleContent`(任一可见内容)与 `assistantStreamHasVisibleText`(任一可见文本)通过可提前退出的扫描直接从紧凑记录得出结果;`lastAssistantStreamChunk` 反向扫描到某一类型的最后一个原始 chunk,`assistantStreamChunks` 与 `joinAssistantStreamText` 扫描整个流,`assembleAssistantStream` 向 `BlockAssembler` 每个 run 喂一段拼接 delta,blocks/usage/replayState 与逐成员展开相同。`runFirstTokenTime` 与 `runFirstVisibleTime` 对单个打包 run 做提前退出扫描,`isTokenDelta`、`isVisibleChunk` 与 `chunkHasVisibleText` 定义单个 chunk 的 token 与可见性规则。`expandAssistantStream` 仍是持久边界读取记录的校验路径;它不被记忆化,因为保留的展开在事件生命周期内约花费紧凑流的十倍内存。
67
71
 
68
72
  ### 失败与恢复
69
73
 
70
- 每个流都恰好以一个终止 `finish` 分片结束:失败为 `{ kind: 'error', failure }`,取消为 `{ kind: 'aborted', failure }`。失败携带稳定 code,如 `NO_ADAPTER`、`MISSING_CREDENTIAL`、`AUTH`、`RATE_LIMIT` 与 `CONTEXT_WINDOW_EXCEEDED`;消费方依据 code 路由,绝不解析消息文本。点名未注册提供方的请求会以 `NO_ADAPTER` 失败,格式错误的凭据会以 `INVALID_CREDENTIAL` 失败,而不是表现为不透明的 fetch 错误。本服务从不自行重跑请求:重试是 `dsh-llm-retry` 在 agent 失败步骤扩展点上的职责。
74
+ 每个流都恰好以一个终止 `finish` 分片结束:失败为 `{ kind: 'error', failure }`,取消为 `{ kind: 'aborted', failure }`。失败携带稳定 code,如 `NO_ADAPTER`、`MISSING_CREDENTIAL`、`AUTH`、`RATE_LIMIT` 与 `CONTEXT_WINDOW_EXCEEDED`;消费方依据 code 路由,绝不解析消息文本。`QUOTA` 表示提供方中立的额度耗尽,`ACCOUNT_QUOTA` 专用于当前产品能够充值的第一方账号余额不足。点名未注册提供方的请求会以 `NO_ADAPTER` 失败,格式错误的凭据会以 `INVALID_CREDENTIAL` 失败,而不是表现为不透明的 fetch 错误。本服务从不自行重跑请求:重试是 `dsh-llm-retry` 在 agent 失败步骤扩展点上的职责。
71
75
 
72
76
  -----
73
77
 
@@ -81,7 +85,7 @@ for await (const chunk of ctx.llm.stream({
81
85
 
82
86
  ### 设计理念
83
87
 
84
- 本服务基于一项职责分离原则:**逻辑约定是提供方无关的,适配器拥有协议。** 它一次性地定义规范消息、内容块与流式分片词汇,每个提供方适配器只把自己的协议格式翻译为该词汇。注册表是拓扑的拥有者——适配器路由、可配置提供方条目与发现 offer 都在这里注册,并随其 fiber 一起 dispose(资源释放)——而请求始终是会话日志的纯函数:loop 构建的请求以深度冻结状态到达,因此监听器与适配器只能读取,绝不能改写。
88
+ 本服务基于一项职责分离原则:**逻辑约定是提供方无关的,适配器拥有协议。** 它一次性地定义规范消息、内容块与流式分片词汇,每个提供方适配器只把自己的协议格式翻译为该词汇。注册表是拓扑的拥有者——适配器路由、可配置提供方条目与发现 offer 都在这里注册,并随其 fiber 一起 dispose(资源释放)——而 agent loop 的请求始终是会话日志的纯函数:loop 构建的请求以深度冻结状态到达,因此监听器与适配器只能读取,绝不能改写。
85
89
 
86
90
  ### 源码地图
87
91
 
@@ -95,19 +99,19 @@ for await (const chunk of ctx.llm.stream({
95
99
  | [`src/call-config.ts`](src/call-config.ts) | 调用配置校验、适配器默认值填入与请求冻结 |
96
100
  | [`src/retry-policy.ts`](src/retry-policy.ts) | 提供方自有重试策略解析(normal 与 always 模式) |
97
101
  | [`src/error.ts`](src/error.ts) | `HarnessError`/`LlmError` 分类体系与提供方无关失败 code |
98
- | [`src/content.ts`](src/content.ts) | 共享文件与图片投影辅助函数,包括请求图片卸载 |
102
+ | [`src/content.ts`](src/content.ts) | 共享文件与图片辅助函数:内容遍历、文件投影、图片卸载计数与已卸载图片投影 |
99
103
  | [`src/api-key.ts`](src/api-key.ts) | 每个适配器共享的凭据格式校验 |
100
104
  | [`src/adapter-failure.ts`](src/adapter-failure.ts) | 把失败归一化为终止 finish 分片 |
101
105
 
102
106
  ### 主流程
103
107
 
104
- 请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR(热模块替换)或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性句柄文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。`offloadRequestImagesWithPolicy()` 按原始字节或 base64 大小以及图片数或字节步长,确定性地从最旧图片开始移除;纯函数 `offloadedImagePrefixCount()` 公开同一决策,使路由所属的请求定价无需构建投影即可复现它。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall(瀑布式事件),随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
108
+ 请求会对照其精确模型的能力校验,包括上下文窗口、输出默认值、推理强度、输入模态与 `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` 分片到达消费方。
105
109
 
106
- 文件检测在每次请求时读取当前内容,包括嵌套工具结果,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。
110
+ 文件检测在每次请求时读取当前内容,包括 tool-role 结果内容,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。
107
111
 
108
112
  ### 不变式
109
113
 
110
- - **模型可见 ⟺ 已记录**——到达提供方请求的任何内容都可以从会话日志重建;loop 构建的请求被深度冻结,绝不改写。
114
+ - **模型可见 ⟺ 已记录**——调用方必须确保每个提供方请求中的模型可见输入都可以从会话日志重建;loop 构建的请求以深度冻结状态到达,不可改写。
111
115
  - **回放状态只在同一适配器内流动**——仅当同一适配器实例同时拥有历史路由与目标路由时,assistant 回放状态才会随行;否则在分发前被丢弃。
112
116
  - **已准备调用是一次性的**——已准备调用只能分发一次,且其调用配置字段必须与准备好的配置一致。
113
117
  - **图片投影遵循捕获的路由**——只有支持图片的模型会把持久 `ImageBlock` 引用转换为路由专用请求版本;纯文本模型接收稳定占位符。
@@ -125,7 +129,7 @@ for await (const chunk of ctx.llm.stream({
125
129
  当包级约定不够用时阅读以下页面。它们从共享类型逐步进入具体适配器、重试执行器与计量服务。
126
130
 
127
131
  - [LLM(大语言模型)流式子系统](../../../docs/subsystems/llm-streaming.zh.md)——消息与块类型、紧凑的 Assistant 流记录、`StreamChunk` 协议与适配器约定。
128
- - [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——DeepSeek chat-completions 直连实现。
132
+ - [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——DeepSeek Messages 直连实现。
129
133
  - [llm-pi-ai 适配器](../llm-pi-ai/README.zh.md)——基于 pi-ai 的多提供方实现。
130
134
  - [llm-retry](../llm-retry/README.zh.md)——重跑失败模型请求的重试执行器。
131
135
  - [Token 计量](../token-meter/README.zh.md)——具备回放感知的请求与上下文压力测量。
@@ -141,7 +145,7 @@ for await (const chunk of ctx.llm.stream({
141
145
 
142
146
  #### KV Cache 影响
143
147
 
144
- 推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或图片卸载边界变化可能从该图片起阻止复用。
148
+ 推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或一次省略决策可能从该图片起阻止复用。
145
149
 
146
150
  ## 已知限制与延期工作
147
151
 
@@ -152,9 +156,10 @@ for await (const chunk of ctx.llm.stream({
152
156
 
153
157
  - **本服务不提供重试执行、缓存或速率限制**——提供方注册会存储重试策略,但一次流仍是一次提供方尝试;`@x1a0f3n9/dsh-llm-retry` 在持久 agent 步骤边界上执行该策略。
154
158
  - **`GenerateOptions` 采样只包含 `temperature`/`maxTokens`/`stop`**——没有 `tool_choice`、`top_p` 或 penalty 字段;有产生方落地时词汇才会增长(见[已删除惰性旋钮](../../../.agents/notes/archived/simplification/2026-07-04-drop-inert-request-knobs.md))。
155
- - **只有出现实际产生方后,相应变体才会加入**——`prefill`、逐工具 `strict`、内容块 `cache` 提示和 `agent` 消息来源变体都没有产生方(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md))。
159
+ - **变体通常要求实际产生方**——`prefill`、逐工具 `strict`、内容块 `cache` 提示和 `agent` 消息来源变体都没有产生方(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md))。
156
160
  - **`BlockAssembler` 只处理核心块类型**——插件添加块类型的流若从未由 `block-end` 关闭,`blocks()` 会抛出异常。
157
161
  - **`GenerateOptions.sessionId` 是本地声明的品牌类型**——导入 dsh-session 的 `SessionId` 会产生依赖循环。
162
+ - **工具更新需要会话历史** — `GenerateOptions.tools` 包含当前有效定义。`toolHistory` 提供 `Session.toolHistory()` 派生的初始声明及已解析历史定义的添加记录。适配器分发时,`projectToolUpdates` 构造延迟声明,并在 `in-history` 模式保留已移除定义;`addition-only` 省略已移除定义和移除消息。不支持更新的路由接收有效工具,不携带 developer 消息或 `deferLoading`。缺少历史或请求前缀遗漏已记录更新时,回退为当前声明且不发送 developer 消息。 显式延迟加载的初始工具在首个保留的添加块出现前保持延迟状态;声明延迟加载工具不会使其激活。
158
163
 
159
164
  <a id="dev-note"></a>
160
165
  ### 开发备注