@deepseek-ai/dsh-llm 0.1.5-rc.1 → 0.1.6-alpha.1

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
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
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: e10706108d86f337c333b1f80a15c731fd2a00cf
5
+ README.md: 8e4e547190a039959f7db29898945f2ed68ff874
6
+ README.zh.md: 9164f4a6e6107e439b732539cddeb14716bc3fdc
package/README.md CHANGED
@@ -95,13 +95,13 @@ The service is built on one separation: **the logical contract is provider-neutr
95
95
  | [`src/call-config.ts`](src/call-config.ts) | Call-config validation, adapter-default materialization, and request freezing |
96
96
  | [`src/retry-policy.ts`](src/retry-policy.ts) | Provider-owned retry policy resolution (normal and always modes) |
97
97
  | [`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 |
98
+ | [`src/content.ts`](src/content.ts) | Shared file and image helpers: content walks, file projection, image offload accounting, and offloaded-image projection |
99
99
  | [`src/api-key.ts`](src/api-key.ts) | Credential format check shared by every adapter |
100
100
  | [`src/adapter-failure.ts`](src/adapter-failure.ts) | Failure normalization into terminal finish chunks |
101
101
 
102
102
  ### Main flow
103
103
 
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.
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. 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
105
 
106
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.
107
107
 
@@ -141,7 +141,7 @@ None, as the LLM service adds no content; adapters choose when to add the shared
141
141
 
142
142
  #### KV Cache effect
143
143
 
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.
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 an offload decision can prevent reuse from that image.
145
145
 
146
146
  ## Known Limitations and Deferred Work
147
147
 
package/README.zh.md CHANGED
@@ -25,7 +25,7 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 任何调用模型提供方的组合——agent loop、会话标题生成器、压缩(compaction)摘要器——都会通过本服务流式发起请求。与至少一个提供方适配器一起挂载它;服务本身没有任何配置,也不包含提供方协议代码。
28
+ 任何调用模型提供方的组合——agent loop(智能体循环)、会话标题生成器、压缩(compaction)摘要器——都会通过本服务流式发起请求。与至少一个提供方适配器一起挂载它;服务本身没有任何配置,也不包含提供方协议代码。
29
29
 
30
30
  ### 何时选择
31
31
 
@@ -60,10 +60,10 @@ for await (const chunk of ctx.llm.stream({
60
60
 
61
61
  - **流式发起一次模型调用**——`ctx.llm.stream(options)` 为任何已注册提供方与模型产出原始分片(token 级增量);消费方用 `BlockAssembler` 组装。
62
62
  - **注册提供方适配器**——一个适配器拥有一个或多个提供方路由,其注册会捕获该路由的重试策略;重复注册同一路由会以 `DUPLICATE_ADAPTER` 失败。
63
- - **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接事实,无需重启。`LlmConfigurableProvider.error` 报告供修复的配置诊断;未受影响的模型仍可提供服务。
63
+ - **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接信息,无需重启。`LlmConfigurableProvider.error` 报告供修复的配置诊断;未受影响的模型仍可提供服务。
64
64
  - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型,并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度、输入模态与系统提示词更新模式:当模型把任意位置最新的 `system` 消息读作有效系统提示词时,`LlmResolvedModelInfo.systemPromptUpdate` 为 `'in-history'`;只读取开头 system 消息时该字段缺失;`normalizeModelInfo` 以 `INVALID_MODEL_INFO` 拒绝任何其他值。
65
65
  - **校验调用配置**——显式或配置的推理强度会在任何提供方 I/O 之前对照精确模型校验;请求省略输出上限时,会填入适配器配置的输出上限。
66
- - **不展开即读取内嵌 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` 仍是持久边界读取记录的校验路径;它不被记忆化,因为保留的展开在事件生命周期内约花费紧凑流的十倍内存。
66
+ - **不展开即读取内嵌 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
67
 
68
68
  ### 失败与恢复
69
69
 
@@ -81,7 +81,7 @@ for await (const chunk of ctx.llm.stream({
81
81
 
82
82
  ### 设计理念
83
83
 
84
- 服务建立在一个分离之上:**逻辑约定是提供方无关的,适配器拥有协议。** 它一次性地定义规范消息、内容块与流式分片词汇,每个提供方适配器只把自己的协议格式翻译为该词汇。注册表是拓扑的拥有者——适配器路由、可配置提供方条目与发现 offer 都在这里注册,并随其 fiber 一起 dispose(资源释放)——而请求始终是会话日志的纯函数:loop 构建的请求以深度冻结状态到达,因此监听器与适配器只能读取,绝不能改写。
84
+ 本服务基于一项职责分离原则:**逻辑约定是提供方无关的,适配器拥有协议。** 它一次性地定义规范消息、内容块与流式分片词汇,每个提供方适配器只把自己的协议格式翻译为该词汇。注册表是拓扑的拥有者——适配器路由、可配置提供方条目与发现 offer 都在这里注册,并随其 fiber 一起 dispose(资源释放)——而请求始终是会话日志的纯函数:loop 构建的请求以深度冻结状态到达,因此监听器与适配器只能读取,绝不能改写。
85
85
 
86
86
  ### 源码地图
87
87
 
@@ -91,17 +91,17 @@ for await (const chunk of ctx.llm.stream({
91
91
  | [`src/types.ts`](src/types.ts) | `StreamChunk` 协议、内容块映射、结束原因与共享词汇 |
92
92
  | [`src/message.ts`](src/message.ts) | 投递、历史与请求共享的不可变消息构造函数 |
93
93
  | [`src/assembler.ts`](src/assembler.ts) | `BlockAssembler`:分片到块的增量组装 |
94
- | [`src/assistant-stream.ts`](src/assistant-stream.ts) | 紧凑带时间 Assistant stream 的累积、严格校验、精确展开与记录级读取器 |
94
+ | [`src/assistant-stream.ts`](src/assistant-stream.ts) | 带时间信息的紧凑 Assistant 流累积、严格校验、精确展开与记录级读取器 |
95
95
  | [`src/call-config.ts`](src/call-config.ts) | 调用配置校验、适配器默认值填入与请求冻结 |
96
96
  | [`src/retry-policy.ts`](src/retry-policy.ts) | 提供方自有重试策略解析(normal 与 always 模式) |
97
97
  | [`src/error.ts`](src/error.ts) | `HarnessError`/`LlmError` 分类体系与提供方无关失败 code |
98
- | [`src/content.ts`](src/content.ts) | 共享文件与图片投影辅助函数,包括请求图片卸载 |
98
+ | [`src/content.ts`](src/content.ts) | 共享文件与图片辅助函数:内容遍历、文件投影、图片卸载计数与已卸载图片投影 |
99
99
  | [`src/api-key.ts`](src/api-key.ts) | 每个适配器共享的凭据格式校验 |
100
100
  | [`src/adapter-failure.ts`](src/adapter-failure.ts) | 把失败归一化为终止 finish 分片 |
101
101
 
102
102
  ### 主流程
103
103
 
104
- 请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR 或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 handle 文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。`offloadRequestImagesWithPolicy()` 按原始字节或 base64 大小以及图片数或字节步长,确定性地从最旧图片开始移除;纯函数 `offloadedImagePrefixCount()` 公开同一决策,使路由所属的请求定价无需构建投影即可复现它。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall,随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
104
+ 请求会对照其精确模型的能力校验,包括上下文窗口、输出默认值、推理强度、输入模态与 `systemPromptUpdate` 模式,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR(热模块替换)或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 句柄文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`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
105
 
106
106
  文件检测在每次请求时读取当前内容,包括嵌套工具结果,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。
107
107
 
@@ -111,7 +111,7 @@ for await (const chunk of ctx.llm.stream({
111
111
  - **回放状态只在同一适配器内流动**——仅当同一适配器实例同时拥有历史路由与目标路由时,assistant 回放状态才会随行;否则在分发前被丢弃。
112
112
  - **已准备调用是一次性的**——已准备调用只能分发一次,且其调用配置字段必须与准备好的配置一致。
113
113
  - **图片投影遵循捕获的路由**——只有支持图片的模型会把持久 `ImageBlock` 引用转换为路由专用请求版本;纯文本模型接收稳定占位符。
114
- - **文件投影无条件进行**——没有任何提供方会收到文件字节;每条路由对每个 `FileBlock` 都得到一行确定性 handle 文本,模型在需要时用文件工具读取保存的副本。
114
+ - **文件投影无条件进行**——没有任何提供方会收到文件字节;每条路由对每个 `FileBlock` 都得到一行确定性句柄文本,模型在需要时用文件工具读取保存的副本。
115
115
  - **协议顺序**——`usage` 先于 `finish`,工具参数保持原始 JSON 字符串,终止 `finish` 之后不再有任何内容。
116
116
  - **注册表变更具有原子性**——路由与目录注册会在任何变动前整体校验候选集合,因此被拒绝的变更会让此前状态继续服务。
117
117
 
@@ -124,7 +124,7 @@ for await (const chunk of ctx.llm.stream({
124
124
 
125
125
  当包级约定不够用时阅读以下页面。它们从共享类型逐步进入具体适配器、重试执行器与计量服务。
126
126
 
127
- - [LLM 流式子系统](../../../docs/subsystems/llm-streaming.zh.md)——消息与块类型、紧凑 Assistant stream record、`StreamChunk` 协议与适配器约定。
127
+ - [LLM(大语言模型)流式子系统](../../../docs/subsystems/llm-streaming.zh.md)——消息与块类型、紧凑的 Assistant 流记录、`StreamChunk` 协议与适配器约定。
128
128
  - [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——DeepSeek chat-completions 直连实现。
129
129
  - [llm-pi-ai 适配器](../llm-pi-ai/README.zh.md)——基于 pi-ai 的多提供方实现。
130
130
  - [llm-retry](../llm-retry/README.zh.md)——重跑失败模型请求的重试执行器。
@@ -141,7 +141,7 @@ for await (const chunk of ctx.llm.stream({
141
141
 
142
142
  #### KV Cache 影响
143
143
 
144
- 推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或图片卸载边界变化可能从该图片起阻止复用。
144
+ 推理强度的具体化会保留已组装请求前缀。图片身份与请求预览文本是确定性的,可选执行世界路径则按请求解析;路径变化或一次省略决策可能从该图片起阻止复用。
145
145
 
146
146
  ## 已知限制与延期工作
147
147
 
package/lib/index.js CHANGED
@@ -219,6 +219,14 @@ function errorChain(value) {
219
219
  function isHarnessError(value) {
220
220
  return value instanceof HarnessError;
221
221
  }
222
+ /**
223
+ * Canonical code for a request an image-capable route cannot send until more
224
+ * of its images are offloaded. The failure's `offloadImages` names how many
225
+ * more of the oldest retained occurrences must be offloaded;
226
+ * `dsh-compaction-image-offload` records an `image/offload` selection before
227
+ * the agent or summarizer retries with freshly derived input.
228
+ */
229
+ const IMAGE_OFFLOAD_REQUIRED_CODE = "IMAGE_OFFLOAD_REQUIRED";
222
230
  //#endregion
223
231
  //#region lib/types/retry-policy.js
224
232
  /**
@@ -430,13 +438,15 @@ function failureSnapshot(value) {
430
438
  const status = candidate.status;
431
439
  const providerRetryAfterMs = candidate.providerRetryAfterMs;
432
440
  const requestId = candidate.requestId;
433
- if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0)) return void 0;
441
+ const offloadImages = candidate.offloadImages;
442
+ if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
434
443
  return Object.freeze({
435
444
  message,
436
445
  code,
437
446
  ...status === void 0 ? {} : { status },
438
447
  ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
439
- ...requestId === void 0 ? {} : { requestId }
448
+ ...requestId === void 0 ? {} : { requestId },
449
+ ...offloadImages === void 0 ? {} : { offloadImages }
440
450
  });
441
451
  } catch (_sdkFailureGetter) {
442
452
  return;
@@ -651,19 +661,21 @@ function projectFilesToText(messages, resolvePath) {
651
661
  function base64Length(bytes) {
652
662
  return Math.ceil(bytes / 3) * 4;
653
663
  }
654
- /** Collect represented image lengths in request and nested-block order. */
655
- function collectImageLengths(blocks, lengths, policy) {
656
- for (const block of blocks) if (block.type === "image") {
657
- const bytes = policy.byteLength === void 0 ? block.attachment.bytes : policy.byteLength(block.attachment);
658
- lengths.push(policy.representation === "base64" ? base64Length(bytes) : bytes);
659
- } else if (block.type === "tool-result") collectImageLengths(block.content, lengths, policy);
664
+ /**
665
+ * Visit every image occurrence of typed content in message order, including
666
+ * nested tool-result content.
667
+ * @param content - typed model content blocks.
668
+ * @param visit - called once per occurrence.
669
+ */
670
+ function visitImageBlocks(content, visit) {
671
+ for (const block of content) if (block.type === "image") visit(block);
672
+ else if (block.type === "tool-result") visitImageBlocks(block.content, visit);
660
673
  }
661
- /** Replace the first `remaining.count` image occurrences without mutating durable messages. */
662
- function replaceOldestImages(blocks, remaining, placeholder) {
674
+ /** Replace every offloaded occurrence, including nested tool results, with its placeholder. */
675
+ function replaceOffloadedImages(blocks, placeholder) {
663
676
  let next;
664
677
  for (const [index, block] of blocks.entries()) {
665
- if (block.type === "image" && remaining.count > 0) {
666
- remaining.count -= 1;
678
+ if (block.type === "image" && block.offloaded === true) {
667
679
  next ??= blocks.slice(0, index);
668
680
  next.push({
669
681
  type: "text",
@@ -672,7 +684,7 @@ function replaceOldestImages(blocks, remaining, placeholder) {
672
684
  continue;
673
685
  }
674
686
  if (block.type === "tool-result") {
675
- const content = replaceOldestImages(block.content, remaining, placeholder);
687
+ const content = replaceOffloadedImages(block.content, placeholder);
676
688
  if (content !== block.content) {
677
689
  next ??= blocks.slice(0, index);
678
690
  next.push({
@@ -686,6 +698,69 @@ function replaceOldestImages(blocks, remaining, placeholder) {
686
698
  }
687
699
  return next ?? blocks;
688
700
  }
701
+ /**
702
+ * Project the surface's offloaded occurrences into deterministic text for one
703
+ * request. The offloaded set is a durable surface fact, so every route sends
704
+ * the same set; only the placeholder text is route-owned.
705
+ * @param messages - derived request history.
706
+ * @param placeholder - build the model-visible replacement for one offloaded attachment.
707
+ * @returns the original list when nothing is offloaded, otherwise shallow message copies with placeholders.
708
+ */
709
+ function projectOffloadedImages(messages, placeholder) {
710
+ return messages.map((message) => {
711
+ const content = replaceOffloadedImages(message.content, placeholder);
712
+ return content === message.content ? message : {
713
+ ...message,
714
+ content
715
+ };
716
+ });
717
+ }
718
+ /**
719
+ * Number of oldest retained image occurrences one route budget removes, in
720
+ * whole count and byte quanta, once the budget is exceeded. The result depends
721
+ * only on the represented lengths, so every route names the count the same
722
+ * way.
723
+ * @param lengths - represented byte length of every retained occurrence, oldest first.
724
+ * @param budget - count/byte budgets and removal quanta; unbounded when absent.
725
+ * @returns how many leading occurrences to offload.
726
+ */
727
+ function offloadedImagePrefixCount(lengths, budget) {
728
+ const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
729
+ const excessCount = budget.maxImages === void 0 ? 0 : Math.max(0, lengths.length - budget.maxImages);
730
+ const excessBytes = budget.maxBytes === void 0 ? 0 : Math.max(0, total - budget.maxBytes);
731
+ if (excessCount === 0 && excessBytes === 0) return 0;
732
+ const countQuantum = budget.countQuantum ?? 1;
733
+ const byteQuantum = budget.byteQuantum ?? 1;
734
+ const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
735
+ const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
736
+ let count = 0;
737
+ let removedBytes = 0;
738
+ for (const imageBytes of lengths) {
739
+ if (count >= removeCount && (removeBytes === 0 || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes))) break;
740
+ removedBytes += imageBytes;
741
+ count += 1;
742
+ }
743
+ return count;
744
+ }
745
+ /**
746
+ * Number of oldest retained occurrences a route must still offload before a
747
+ * derived request fits its budget at the exact byte length the route sends;
748
+ * zero when the request fits. A route fails with `IMAGE_OFFLOAD_REQUIRED`
749
+ * carrying this count instead of offloading on its own.
750
+ * @param messages - derived request history carrying the surface's `offloaded` marks.
751
+ * @param budget - route representation, budgets, and removal quanta.
752
+ * @param versionBytes - exact request-version byte length of one retained occurrence.
753
+ * @returns how many more leading retained occurrences to offload.
754
+ */
755
+ function requiredImageOffload(messages, budget, versionBytes) {
756
+ const lengths = [];
757
+ for (const message of messages) visitImageBlocks(message.content, (block) => {
758
+ if (block.offloaded === true) return;
759
+ const bytes = versionBytes(block);
760
+ lengths.push(budget.representation === "base64" ? base64Length(bytes) : bytes);
761
+ });
762
+ return offloadedImagePrefixCount(lengths, budget);
763
+ }
689
764
  /** Replace every image occurrence, including nested tool results, for a text-only model. */
690
765
  function replaceImagesForTextModel(blocks) {
691
766
  let next;
@@ -728,58 +803,6 @@ function projectImagesForTextModel(messages) {
728
803
  };
729
804
  });
730
805
  }
731
- /**
732
- * Number of oldest image occurrences one request projection removes, in whole
733
- * count and byte quanta, once a route budget is exceeded. The result depends
734
- * only on the represented lengths, so provider request pricing reproduces the
735
- * exact serialization decision without building the projected messages.
736
- * @param lengths - represented byte length of every occurrence, in request order.
737
- * @param policy - count/byte budgets and removal quanta; unbounded when absent.
738
- * @returns how many leading occurrences the projection replaces with placeholders.
739
- */
740
- function offloadedImagePrefixCount(lengths, policy) {
741
- const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
742
- const excessCount = policy.maxImages === void 0 ? 0 : Math.max(0, lengths.length - policy.maxImages);
743
- const excessBytes = policy.maxBytes === void 0 ? 0 : Math.max(0, total - policy.maxBytes);
744
- if (excessCount === 0 && excessBytes === 0) return 0;
745
- const countQuantum = policy.countQuantum ?? 1;
746
- const byteQuantum = policy.byteQuantum ?? 1;
747
- const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
748
- const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
749
- let count = 0;
750
- let removedBytes = 0;
751
- for (const imageBytes of lengths) {
752
- if (count >= removeCount && (removeBytes === 0 || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes))) break;
753
- removedBytes += imageBytes;
754
- count += 1;
755
- }
756
- return count;
757
- }
758
- /**
759
- * Return a deterministic transient projection whose oldest images are replaced
760
- * in whole count and byte quanta after a route budget is exceeded. The target
761
- * depends only on complete durable history: at 129 one-megabyte images under
762
- * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
763
- * 64 MiB remain; that removed prefix stays fixed until total history exceeds
764
- * 192 MiB.
765
- * @param messages - complete request history, oldest first.
766
- * @param policy - route representation, budgets, and removal quanta.
767
- * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
768
- */
769
- function offloadRequestImagesWithPolicy(messages, policy) {
770
- const lengths = [];
771
- for (const message of messages) collectImageLengths(message.content, lengths, policy);
772
- const count = offloadedImagePrefixCount(lengths, policy);
773
- if (count === 0) return messages;
774
- const remaining = { count };
775
- return messages.map((message) => {
776
- const content = replaceOldestImages(message.content, remaining, policy.placeholder);
777
- return content === message.content ? message : {
778
- ...message,
779
- content
780
- };
781
- });
782
- }
783
806
  //#endregion
784
807
  //#region lib/types/attribution.js
785
808
  /**
@@ -1582,7 +1605,8 @@ var LlmError = class extends HarnessError {
1582
1605
  code,
1583
1606
  ...options?.status === void 0 ? {} : { status: options.status },
1584
1607
  ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
1585
- ...options?.requestId === void 0 ? {} : { requestId: options.requestId }
1608
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
1609
+ ...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
1586
1610
  });
1587
1611
  }
1588
1612
  };
@@ -2323,4 +2347,4 @@ function adapterFailureChunk(error, signal) {
2323
2347
  };
2324
2348
  }
2325
2349
  //#endregion
2326
- export { APP_IDENTITY, AssistantStreamAccumulator, BlockAssembler, CONTEXT_SUMMARY_MAX_CHARS, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, HarnessError, INVALID_CREDENTIAL_CODE, LlmAdapter, LlmAttemptId, LlmError, LlmRuntime, LlmRuntime as default, MessageId, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, ToolCallId, assembleAssistantStream, assertUsableApiKey, assistantStreamChunks, assistantStreamFirstTokenTime, assistantStreamHasVisibleContent, assistantStreamHasVisibleText, attributionHeaders, boundContextSummary, callConfigEquals, chunkHasVisibleText, contentHasFile, contentHasImage, createAssistantMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, errorChain, expandAssistantStream, fileHandleText, freezeMessage, isAgentLoopRequest, isContextWindowExceededError, isHarnessError, isQuotaExceededError, isTokenDelta, isVisibleChunk, joinAssistantStreamText, lastAssistantStreamChunk, markAgentLoopRequest, normalizeApiKey, offloadRequestImagesWithPolicy, offloadedImagePrefixCount, offloadedImageText, projectFilesToText, projectImagesForTextModel, requestImageHandleText, resolveImageAttachmentAccess, resolveRetryPolicy, runFirstTokenTime, runFirstVisibleTime, textOnlyImageText, userAgent };
2350
+ export { APP_IDENTITY, AssistantStreamAccumulator, BlockAssembler, CONTEXT_SUMMARY_MAX_CHARS, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, HarnessError, IMAGE_OFFLOAD_REQUIRED_CODE, INVALID_CREDENTIAL_CODE, LlmAdapter, LlmAttemptId, LlmError, LlmRuntime, LlmRuntime as default, MessageId, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, ToolCallId, assembleAssistantStream, assertUsableApiKey, assistantStreamChunks, assistantStreamFirstTokenTime, assistantStreamHasVisibleContent, assistantStreamHasVisibleText, attributionHeaders, boundContextSummary, callConfigEquals, chunkHasVisibleText, contentHasFile, contentHasImage, createAssistantMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, errorChain, expandAssistantStream, fileHandleText, freezeMessage, isAgentLoopRequest, isContextWindowExceededError, isHarnessError, isQuotaExceededError, isTokenDelta, isVisibleChunk, joinAssistantStreamText, lastAssistantStreamChunk, markAgentLoopRequest, normalizeApiKey, offloadedImageText, projectFilesToText, projectImagesForTextModel, projectOffloadedImages, requestImageHandleText, requiredImageOffload, resolveImageAttachmentAccess, resolveRetryPolicy, runFirstTokenTime, runFirstVisibleTime, textOnlyImageText, userAgent };
@@ -68,7 +68,7 @@ export const TYPERT = {
68
68
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
69
69
  schema: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
70
70
  },
71
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":628,"column":9},
71
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":631,"column":9},
72
72
  },
73
73
  {
74
74
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -83,7 +83,7 @@ export const TYPERT = {
83
83
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
84
84
  schema: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
85
85
  },
86
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":541,"column":3},
86
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":544,"column":3},
87
87
  },
88
88
  {
89
89
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -98,7 +98,7 @@ export const TYPERT = {
98
98
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
99
99
  schema: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
100
100
  },
101
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":469,"column":3},
101
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":472,"column":3},
102
102
  },
103
103
  ],
104
104
  model: {
@@ -227,8 +227,8 @@ export const TYPERT = {
227
227
  "declaration": "export interface AgentMessageSource {\n readonly kind: 'agent-message';\n readonly form: 'relay';\n readonly senderSessionId: SessionId;\n}"
228
228
  },
229
229
  {
230
- "name": "AssistantProvenance",
231
- "declaration": "export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n}"
230
+ "name": "AssistantProviderMetadata",
231
+ "declaration": "export interface AssistantProviderMetadata {\n provider: string;\n model: string;\n replayState?: unknown;\n}"
232
232
  },
233
233
  {
234
234
  "name": "AttachmentId",
@@ -300,7 +300,7 @@ export const TYPERT = {
300
300
  },
301
301
  {
302
302
  "name": "ImageBlock",
303
- "declaration": "export interface ImageBlock {\n type: 'image';\n attachment: ImageAttachmentRef;\n}"
303
+ "declaration": "export interface ImageBlock {\n type: 'image';\n attachment: ImageAttachmentRef;\n offloaded?: true;\n}"
304
304
  },
305
305
  {
306
306
  "name": "ImageMediaType",
@@ -328,7 +328,7 @@ export const TYPERT = {
328
328
  },
329
329
  {
330
330
  "name": "LlmFailure",
331
- "declaration": "export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n}"
331
+ "declaration": "export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n readonly offloadImages?: number;\n}"
332
332
  },
333
333
  {
334
334
  "name": "LlmImageRequestPrice",
@@ -336,7 +336,7 @@ export const TYPERT = {
336
336
  },
337
337
  {
338
338
  "name": "LlmImageRequestPricing",
339
- "declaration": "export interface LlmImageRequestPricing {\n priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[];\n}"
339
+ "declaration": "export interface LlmImageRequestPricing {\n priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[];\n}"
340
340
  },
341
341
  {
342
342
  "name": "LlmModelContext",
@@ -384,7 +384,7 @@ export const TYPERT = {
384
384
  },
385
385
  {
386
386
  "name": "ModelMessageSource",
387
- "declaration": "export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n}"
387
+ "declaration": "export interface ModelMessageSource extends AssistantProviderMetadata {\n kind: 'model';\n}"
388
388
  },
389
389
  {
390
390
  "name": "ModelModality",
@@ -65,7 +65,7 @@ export const TYPERT_REMOTE = {
65
65
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
66
66
  schema: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
67
67
  },
68
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":628,"column":9},
68
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":631,"column":9},
69
69
  },
70
70
  {
71
71
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -80,7 +80,7 @@ export const TYPERT_REMOTE = {
80
80
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
81
81
  schema: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
82
82
  },
83
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":541,"column":3},
83
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":544,"column":3},
84
84
  },
85
85
  {
86
86
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -95,7 +95,7 @@ export const TYPERT_REMOTE = {
95
95
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
96
96
  schema: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
97
97
  },
98
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":469,"column":3},
98
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":472,"column":3},
99
99
  },
100
100
  ],
101
101
  }
@@ -67,12 +67,14 @@ function failureSnapshot(value) {
67
67
  const status = candidate.status;
68
68
  const providerRetryAfterMs = candidate.providerRetryAfterMs;
69
69
  const requestId = candidate.requestId;
70
+ const offloadImages = candidate.offloadImages;
70
71
  if (typeof message !== 'string' || message.length === 0
71
72
  || typeof code !== 'string' || code.length === 0
72
73
  || (status !== undefined && (!Number.isInteger(status) || status < 100 || status > 599))
73
74
  || (providerRetryAfterMs !== undefined
74
75
  && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0))
75
- || (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0)))
76
+ || (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0))
77
+ || (offloadImages !== undefined && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)))
76
78
  return undefined;
77
79
  return Object.freeze({
78
80
  message,
@@ -80,6 +82,7 @@ function failureSnapshot(value) {
80
82
  ...status === undefined ? {} : { status },
81
83
  ...providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs },
82
84
  ...requestId === undefined ? {} : { requestId },
85
+ ...offloadImages === undefined ? {} : { offloadImages },
83
86
  });
84
87
  }
85
88
  catch (_sdkFailureGetter) {
@@ -1,5 +1,5 @@
1
1
  /** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */
2
- import type { ContentBlock } from './types.ts';
2
+ import type { ContentBlock, ImageBlock, LlmImageRequestBudget } from './types.ts';
3
3
  import type { Message } from './message.ts';
4
4
  import type { AttachmentStore, FileAttachmentRef, ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment';
5
5
  /** Execution-world path that model tools can use to read one normalized attachment. */
@@ -83,49 +83,30 @@ export declare function fileHandleText(ref: FileAttachmentRef, readonlyPath: str
83
83
  * @returns the original list without files, otherwise shallow message copies with handle text.
84
84
  */
85
85
  export declare function projectFilesToText(messages: readonly Message[], resolvePath: (ref: FileAttachmentRef) => string | undefined): readonly Message[];
86
- /** Byte accounting and quantized removal policy for one request representation. */
87
- export interface RequestImageOffloadPolicy {
88
- /** Image count accepted by the route; omission leaves count unbounded. */
89
- maxImages?: number;
90
- /** Accumulated image bytes accepted by the route; omission leaves bytes unbounded. */
91
- maxBytes?: number;
92
- /** Number of excess images removed as one deterministic step. */
93
- countQuantum?: number;
94
- /** Number of excess bytes removed as one deterministic step. */
95
- byteQuantum?: number;
96
- /** Whether byte accounting uses raw file bytes or inline base64 length. */
97
- representation: 'raw' | 'base64';
98
- /** Resolve the encoded request-version length; omission uses normalized attachment bytes. */
99
- byteLength?: (ref: ImageAttachmentRef) => number;
100
- /** Build the model-visible replacement for each omitted attachment. */
101
- placeholder: (ref: ImageAttachmentRef) => string;
102
- }
103
86
  /**
104
- * Project durable image history into deterministic text for an exact text-only model.
105
- * @param messages - complete request history.
106
- * @returns the original list without images, otherwise shallow message copies with stable placeholders.
87
+ * Project the surface's offloaded occurrences into deterministic text for one
88
+ * request. The offloaded set is a durable surface fact, so every route sends
89
+ * the same set; only the placeholder text is route-owned.
90
+ * @param messages - derived request history.
91
+ * @param placeholder - build the model-visible replacement for one offloaded attachment.
92
+ * @returns the original list when nothing is offloaded, otherwise shallow message copies with placeholders.
107
93
  */
108
- export declare function projectImagesForTextModel(messages: readonly Message[]): readonly Message[];
94
+ export declare function projectOffloadedImages(messages: readonly Message[], placeholder: (ref: ImageAttachmentRef) => string): readonly Message[];
109
95
  /**
110
- * Number of oldest image occurrences one request projection removes, in whole
111
- * count and byte quanta, once a route budget is exceeded. The result depends
112
- * only on the represented lengths, so provider request pricing reproduces the
113
- * exact serialization decision without building the projected messages.
114
- * @param lengths - represented byte length of every occurrence, in request order.
115
- * @param policy - count/byte budgets and removal quanta; unbounded when absent.
116
- * @returns how many leading occurrences the projection replaces with placeholders.
96
+ * Number of oldest retained occurrences a route must still offload before a
97
+ * derived request fits its budget at the exact byte length the route sends;
98
+ * zero when the request fits. A route fails with `IMAGE_OFFLOAD_REQUIRED`
99
+ * carrying this count instead of offloading on its own.
100
+ * @param messages - derived request history carrying the surface's `offloaded` marks.
101
+ * @param budget - route representation, budgets, and removal quanta.
102
+ * @param versionBytes - exact request-version byte length of one retained occurrence.
103
+ * @returns how many more leading retained occurrences to offload.
117
104
  */
118
- export declare function offloadedImagePrefixCount(lengths: readonly number[], policy: Pick<RequestImageOffloadPolicy, 'maxImages' | 'maxBytes' | 'countQuantum' | 'byteQuantum'>): number;
105
+ export declare function requiredImageOffload(messages: readonly Message[], budget: Pick<LlmImageRequestBudget, 'representation' | 'maxBytes' | 'maxImages' | 'byteQuantum' | 'countQuantum'>, versionBytes: (block: ImageBlock) => number): number;
119
106
  /**
120
- * Return a deterministic transient projection whose oldest images are replaced
121
- * in whole count and byte quanta after a route budget is exceeded. The target
122
- * depends only on complete durable history: at 129 one-megabyte images under
123
- * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
124
- * 64 MiB remain; that removed prefix stays fixed until total history exceeds
125
- * 192 MiB.
126
- * @param messages - complete request history, oldest first.
127
- * @param policy - route representation, budgets, and removal quanta.
128
- * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
107
+ * Project durable image history into deterministic text for an exact text-only model.
108
+ * @param messages - complete request history.
109
+ * @returns the original list without images, otherwise shallow message copies with stable placeholders.
129
110
  */
130
- export declare function offloadRequestImagesWithPolicy(messages: readonly Message[], policy: RequestImageOffloadPolicy): readonly Message[];
111
+ export declare function projectImagesForTextModel(messages: readonly Message[]): readonly Message[];
131
112
  //# sourceMappingURL=content.d.ts.map
@@ -161,32 +161,31 @@ export function projectFilesToText(messages, resolvePath) {
161
161
  function base64Length(bytes) {
162
162
  return Math.ceil(bytes / 3) * 4;
163
163
  }
164
- /** Collect represented image lengths in request and nested-block order. */
165
- function collectImageLengths(blocks, lengths, policy) {
166
- for (const block of blocks) {
167
- if (block.type === 'image') {
168
- const bytes = policy.byteLength === undefined
169
- ? block.attachment.bytes
170
- : policy.byteLength(block.attachment);
171
- lengths.push(policy.representation === 'base64' ? base64Length(bytes) : bytes);
172
- }
173
- else if (block.type === 'tool-result') {
174
- collectImageLengths(block.content, lengths, policy);
175
- }
164
+ /**
165
+ * Visit every image occurrence of typed content in message order, including
166
+ * nested tool-result content.
167
+ * @param content - typed model content blocks.
168
+ * @param visit - called once per occurrence.
169
+ */
170
+ function visitImageBlocks(content, visit) {
171
+ for (const block of content) {
172
+ if (block.type === 'image')
173
+ visit(block);
174
+ else if (block.type === 'tool-result')
175
+ visitImageBlocks(block.content, visit);
176
176
  }
177
177
  }
178
- /** Replace the first `remaining.count` image occurrences without mutating durable messages. */
179
- function replaceOldestImages(blocks, remaining, placeholder) {
178
+ /** Replace every offloaded occurrence, including nested tool results, with its placeholder. */
179
+ function replaceOffloadedImages(blocks, placeholder) {
180
180
  let next;
181
181
  for (const [index, block] of blocks.entries()) {
182
- if (block.type === 'image' && remaining.count > 0) {
183
- remaining.count -= 1;
182
+ if (block.type === 'image' && block.offloaded === true) {
184
183
  next ??= blocks.slice(0, index);
185
184
  next.push({ type: 'text', text: placeholder(block.attachment) });
186
185
  continue;
187
186
  }
188
187
  if (block.type === 'tool-result') {
189
- const content = replaceOldestImages(block.content, remaining, placeholder);
188
+ const content = replaceOffloadedImages(block.content, placeholder);
190
189
  if (content !== block.content) {
191
190
  next ??= blocks.slice(0, index);
192
191
  next.push({ ...block, content });
@@ -197,6 +196,73 @@ function replaceOldestImages(blocks, remaining, placeholder) {
197
196
  }
198
197
  return next ?? blocks;
199
198
  }
199
+ /**
200
+ * Project the surface's offloaded occurrences into deterministic text for one
201
+ * request. The offloaded set is a durable surface fact, so every route sends
202
+ * the same set; only the placeholder text is route-owned.
203
+ * @param messages - derived request history.
204
+ * @param placeholder - build the model-visible replacement for one offloaded attachment.
205
+ * @returns the original list when nothing is offloaded, otherwise shallow message copies with placeholders.
206
+ */
207
+ export function projectOffloadedImages(messages, placeholder) {
208
+ return messages.map((message) => {
209
+ const content = replaceOffloadedImages(message.content, placeholder);
210
+ return content === message.content ? message : { ...message, content };
211
+ });
212
+ }
213
+ /**
214
+ * Number of oldest retained image occurrences one route budget removes, in
215
+ * whole count and byte quanta, once the budget is exceeded. The result depends
216
+ * only on the represented lengths, so every route names the count the same
217
+ * way.
218
+ * @param lengths - represented byte length of every retained occurrence, oldest first.
219
+ * @param budget - count/byte budgets and removal quanta; unbounded when absent.
220
+ * @returns how many leading occurrences to offload.
221
+ */
222
+ function offloadedImagePrefixCount(lengths, budget) {
223
+ const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
224
+ const excessCount = budget.maxImages === undefined ? 0 : Math.max(0, lengths.length - budget.maxImages);
225
+ const excessBytes = budget.maxBytes === undefined ? 0 : Math.max(0, total - budget.maxBytes);
226
+ if (excessCount === 0 && excessBytes === 0)
227
+ return 0;
228
+ const countQuantum = budget.countQuantum ?? 1;
229
+ const byteQuantum = budget.byteQuantum ?? 1;
230
+ const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
231
+ const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
232
+ let count = 0;
233
+ let removedBytes = 0;
234
+ for (const imageBytes of lengths) {
235
+ const byteTargetMet = removeBytes === 0
236
+ || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes);
237
+ if (count >= removeCount && byteTargetMet)
238
+ break;
239
+ removedBytes += imageBytes;
240
+ count += 1;
241
+ }
242
+ return count;
243
+ }
244
+ /**
245
+ * Number of oldest retained occurrences a route must still offload before a
246
+ * derived request fits its budget at the exact byte length the route sends;
247
+ * zero when the request fits. A route fails with `IMAGE_OFFLOAD_REQUIRED`
248
+ * carrying this count instead of offloading on its own.
249
+ * @param messages - derived request history carrying the surface's `offloaded` marks.
250
+ * @param budget - route representation, budgets, and removal quanta.
251
+ * @param versionBytes - exact request-version byte length of one retained occurrence.
252
+ * @returns how many more leading retained occurrences to offload.
253
+ */
254
+ export function requiredImageOffload(messages, budget, versionBytes) {
255
+ const lengths = [];
256
+ for (const message of messages) {
257
+ visitImageBlocks(message.content, (block) => {
258
+ if (block.offloaded === true)
259
+ return;
260
+ const bytes = versionBytes(block);
261
+ lengths.push(budget.representation === 'base64' ? base64Length(bytes) : bytes);
262
+ });
263
+ }
264
+ return offloadedImagePrefixCount(lengths, budget);
265
+ }
200
266
  /** Replace every image occurrence, including nested tool results, for a text-only model. */
201
267
  function replaceImagesForTextModel(blocks) {
202
268
  let next;
@@ -231,59 +297,4 @@ export function projectImagesForTextModel(messages) {
231
297
  return content === message.content ? message : { ...message, content };
232
298
  });
233
299
  }
234
- /**
235
- * Number of oldest image occurrences one request projection removes, in whole
236
- * count and byte quanta, once a route budget is exceeded. The result depends
237
- * only on the represented lengths, so provider request pricing reproduces the
238
- * exact serialization decision without building the projected messages.
239
- * @param lengths - represented byte length of every occurrence, in request order.
240
- * @param policy - count/byte budgets and removal quanta; unbounded when absent.
241
- * @returns how many leading occurrences the projection replaces with placeholders.
242
- */
243
- export function offloadedImagePrefixCount(lengths, policy) {
244
- const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
245
- const excessCount = policy.maxImages === undefined ? 0 : Math.max(0, lengths.length - policy.maxImages);
246
- const excessBytes = policy.maxBytes === undefined ? 0 : Math.max(0, total - policy.maxBytes);
247
- if (excessCount === 0 && excessBytes === 0)
248
- return 0;
249
- const countQuantum = policy.countQuantum ?? 1;
250
- const byteQuantum = policy.byteQuantum ?? 1;
251
- const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
252
- const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
253
- let count = 0;
254
- let removedBytes = 0;
255
- for (const imageBytes of lengths) {
256
- const byteTargetMet = removeBytes === 0
257
- || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes);
258
- if (count >= removeCount && byteTargetMet)
259
- break;
260
- removedBytes += imageBytes;
261
- count += 1;
262
- }
263
- return count;
264
- }
265
- /**
266
- * Return a deterministic transient projection whose oldest images are replaced
267
- * in whole count and byte quanta after a route budget is exceeded. The target
268
- * depends only on complete durable history: at 129 one-megabyte images under
269
- * a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
270
- * 64 MiB remain; that removed prefix stays fixed until total history exceeds
271
- * 192 MiB.
272
- * @param messages - complete request history, oldest first.
273
- * @param policy - route representation, budgets, and removal quanta.
274
- * @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
275
- */
276
- export function offloadRequestImagesWithPolicy(messages, policy) {
277
- const lengths = [];
278
- for (const message of messages)
279
- collectImageLengths(message.content, lengths, policy);
280
- const count = offloadedImagePrefixCount(lengths, policy);
281
- if (count === 0)
282
- return messages;
283
- const remaining = { count };
284
- return messages.map((message) => {
285
- const content = replaceOldestImages(message.content, remaining, policy.placeholder);
286
- return content === message.content ? message : { ...message, content };
287
- });
288
- }
289
300
  //# sourceMappingURL=content.js.map
@@ -70,4 +70,12 @@ export declare function errorChain(value: unknown): string;
70
70
  * @returns true only for real instances; duck-typed or cross-realm errors do not narrow.
71
71
  */
72
72
  export declare function isHarnessError(value: unknown): value is HarnessError;
73
+ /**
74
+ * Canonical code for a request an image-capable route cannot send until more
75
+ * of its images are offloaded. The failure's `offloadImages` names how many
76
+ * more of the oldest retained occurrences must be offloaded;
77
+ * `dsh-compaction-image-offload` records an `image/offload` selection before
78
+ * the agent or summarizer retries with freshly derived input.
79
+ */
80
+ export declare const IMAGE_OFFLOAD_REQUIRED_CODE = "IMAGE_OFFLOAD_REQUIRED";
73
81
  //# sourceMappingURL=error.d.ts.map
@@ -142,4 +142,12 @@ export function errorChain(value) {
142
142
  export function isHarnessError(value) {
143
143
  return value instanceof HarnessError;
144
144
  }
145
+ /**
146
+ * Canonical code for a request an image-capable route cannot send until more
147
+ * of its images are offloaded. The failure's `offloadImages` names how many
148
+ * more of the oldest retained occurrences must be offloaded;
149
+ * `dsh-compaction-image-offload` records an `image/offload` selection before
150
+ * the agent or summarizer retries with freshly derived input.
151
+ */
152
+ export const IMAGE_OFFLOAD_REQUIRED_CODE = 'IMAGE_OFFLOAD_REQUIRED';
145
153
  //# sourceMappingURL=error.js.map
@@ -53,6 +53,8 @@ export interface LlmErrorOptions extends ErrorOptions {
53
53
  providerRetryAfterMs?: number;
54
54
  /** Non-empty opaque provider request id. */
55
55
  requestId?: ProviderRequestId;
56
+ /** Positive count of additional oldest retained image occurrences to offload; only with `IMAGE_OFFLOAD_REQUIRED`. */
57
+ offloadImages?: number;
56
58
  }
57
59
  /**
58
60
  * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
@@ -96,6 +96,7 @@ export class LlmError extends HarnessError {
96
96
  ...options?.status === undefined ? {} : { status: options.status },
97
97
  ...options?.providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
98
98
  ...options?.requestId === undefined ? {} : { requestId: options.requestId },
99
+ ...options?.offloadImages === undefined ? {} : { offloadImages: options.offloadImages },
99
100
  });
100
101
  }
101
102
  }
@@ -2,7 +2,7 @@
2
2
  import type { MessageId, ToolCallId } from './brand.ts';
3
3
  import type { ContentBlock, ToolResultBlock } from './types.ts';
4
4
  /** Provider/model identity and adapter-private replay data for an assistant message. */
5
- export interface AssistantProvenance {
5
+ export interface AssistantProviderMetadata {
6
6
  /** Provider route that produced the message. */
7
7
  provider: string;
8
8
  /** Provider model id that produced the message. */
@@ -15,7 +15,7 @@ export interface AssistantProvenance {
15
15
  replayState?: unknown;
16
16
  }
17
17
  /** Required source of an assistant message produced by a routed model. */
18
- export interface ModelMessageSource extends AssistantProvenance {
18
+ export interface ModelMessageSource extends AssistantProviderMetadata {
19
19
  kind: 'model';
20
20
  }
21
21
  /** Required source of a user-role message carrying one tool result. */
@@ -25,7 +25,7 @@ export interface ToolMessageSource {
25
25
  }
26
26
  /**
27
27
  * The kind of information in producer-supplied context, declared by the
28
- * producer beside its provenance.
28
+ * producer in the same `MessageSource`.
29
29
  *
30
30
  * `MessageSource.kind` answers *who produced this*; `form` answers *what kind
31
31
  * of thing it is*, and the two axes are deliberately independent — several
@@ -21,7 +21,7 @@ declare module '@deepseek-ai/cordis' {
21
21
  'llm/adapters-updated'(): void;
22
22
  }
23
23
  }
24
- export type { AssistantMessage, AssistantProvenance, Message, MessageSource, MessageSourceMap, ModelMessageSource, ToolMessageSource, ToolResultMessage, UserMessage, } from './message.ts';
24
+ export type { AssistantMessage, AssistantProviderMetadata, Message, MessageSource, MessageSourceMap, ModelMessageSource, ToolMessageSource, ToolResultMessage, UserMessage, } from './message.ts';
25
25
  /** Serializable provider or transport failure facts; policy decides whether they are retryable. */
26
26
  export interface LlmFailure {
27
27
  /** Human-readable provider or transport failure. */
@@ -34,6 +34,13 @@ export interface LlmFailure {
34
34
  readonly providerRetryAfterMs?: number;
35
35
  /** Opaque provider-issued request identifier for diagnostics. */
36
36
  readonly requestId?: ProviderRequestId;
37
+ /**
38
+ * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained
39
+ * image occurrences the route needs offloaded before the same request fits
40
+ * its exact byte accounting. `dsh-compaction-image-offload` records the
41
+ * selected occurrences in an `image/offload` event and retries the step.
42
+ */
43
+ readonly offloadImages?: number;
37
44
  }
38
45
  /** Plain text visible to the end user. */
39
46
  export interface TextBlock {
@@ -55,6 +62,12 @@ export interface ImageBlock {
55
62
  type: 'image';
56
63
  /** Immutable bytes and intrinsic display metadata owned by the attachment service. */
57
64
  attachment: ImageAttachmentRef;
65
+ /**
66
+ * Derived from a durable image-offload decision or preserved by a message
67
+ * rewrite. Every route sends placeholder text naming the image and its
68
+ * available read-only path instead of image bytes.
69
+ */
70
+ offloaded?: true;
58
71
  }
59
72
  /**
60
73
  * A durable verbatim file reference, valid in user content. Files never reach
@@ -171,10 +184,11 @@ export interface LlmImageRequestPrice {
171
184
  export interface LlmImageRequestPricing {
172
185
  /**
173
186
  * Price every image occurrence of one request projection.
174
- * @param images - durable image references in request order, one entry per occurrence.
187
+ * @param images - surface image blocks in request order, one entry per occurrence; an `offloaded` block
188
+ * is priced as its placeholder text.
175
189
  * @returns one price per occurrence, aligned by index with `images`.
176
190
  */
177
- priceImages(images: readonly ImageAttachmentRef[]): readonly LlmImageRequestPrice[];
191
+ priceImages(images: readonly ImageBlock[]): readonly LlmImageRequestPrice[];
178
192
  }
179
193
  /** Display metadata for one registered provider route. */
180
194
  export interface LlmProviderInfo {
@@ -291,6 +305,25 @@ export interface LlmModelContext {
291
305
  /** Maximum combined request and response context in tokens. */
292
306
  contextWindow: number;
293
307
  }
308
+ /**
309
+ * Request-image budget one exact image-capable route enforces over the
310
+ * retained occurrences' exact request-version bytes; for the `base64`
311
+ * representation each byte count expands to its encoded length. A route whose
312
+ * retained occurrences exceed the budget fails the request with
313
+ * `IMAGE_OFFLOAD_REQUIRED` naming the additional occurrences to offload.
314
+ */
315
+ export interface LlmImageRequestBudget {
316
+ /** Whether the route accounts raw file bytes or inline base64 length. */
317
+ representation: 'raw' | 'base64';
318
+ /** Accumulated represented image bytes the route accepts; absent leaves bytes unbounded. */
319
+ maxBytes?: number;
320
+ /** Image occurrences the route accepts; absent leaves the count unbounded. */
321
+ maxImages?: number;
322
+ /** Represented bytes removed as one deterministic advance step; absent removes the minimum. */
323
+ byteQuantum?: number;
324
+ /** Occurrences removed as one deterministic advance step; absent removes the minimum. */
325
+ countQuantum?: number;
326
+ }
294
327
  /** Display metadata for one adapter-owned reasoning effort. */
295
328
  export interface LlmReasoningEffortInfo {
296
329
  /** Opaque stable value accepted by {@link GenerateOptions.reasoningEffort}. */
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.1.5-rc.1",
4
+ "version": "0.1.6-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -65,16 +65,16 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "zod": "^4.4.3",
68
- "@deepseek-ai/dsh-util-crypto": "^0.1.5-rc.1",
69
68
  "@deepseek-ai/schemastery": "^3.18.2",
70
- "@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
71
- "@deepseek-ai/dsh-brand": "^0.1.5-rc.1",
72
- "@deepseek-ai/dsh-util-values": "^0.1.5-rc.1",
73
- "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.1"
69
+ "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.1",
70
+ "@deepseek-ai/dsh-brand": "^0.1.6-alpha.1",
71
+ "@deepseek-ai/dsh-util-values": "^0.1.6-alpha.1",
72
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.1",
73
+ "@deepseek-ai/dsh-util-crypto": "^0.1.6-alpha.1"
74
74
  },
75
75
  "devDependencies": {
76
- "@deepseek-ai/cordis": "^4.0.2",
77
- "@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
78
- "@deepseek-ai/dsh-attachment": "^0.1.5-rc.1"
76
+ "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.1",
77
+ "@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
78
+ "@deepseek-ai/cordis": "^4.0.2"
79
79
  }
80
80
  }