@deepseek-ai/dsh-llm 0.1.3-alpha.2 → 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -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: 0f84af8418f916a77589907656ed310ae2bb73f7
6
- README.zh.md: 2c306940af912176a87d80a2552808cc2b644554
5
+ README.md: 75a5d3e1d51ada12338a41e54b42ad8d6d21c279
6
+ README.zh.md: e10706108d86f337c333b1f80a15c731fd2a00cf
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `@deepseek-ai/dsh-llm` is the provider-neutral model-call service at the center of the harness's LLM capability. Every composition that streams a request to a model provider goes through it, and it owns the shared vocabulary — messages, content blocks, raw stream chunks, and compact Assistant stream records — that the agent loop, session log, and every plugin speak. With it you can register provider adapters, stream one model call, list and discover models, resolve exact-model metadata and call defaults, and capture each provider's retry policy; every request is logged so it stays reconstructable from the session log. It executes no retries and owns no provider wire logic: adapters translate their provider's format, and the optional `dsh-llm-retry` package re-runs failed requests at durable step boundaries. Requests are deep-frozen before dispatch, so middleware and adapters can read them but never rewrite them.
12
+ Use `@deepseek-ai/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 `@deepseek-ai/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
 
@@ -60,8 +60,8 @@ After a successful mount, `ctx.llm.listProviders()` reports the registered route
60
60
 
61
61
  - **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
62
  - **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
- - **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.
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, and input modalities.
63
+ - **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`.
65
65
  - **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
66
  - **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
67
 
@@ -95,13 +95,15 @@ 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 image-content helpers, including request-image offloading |
98
+ | [`src/content.ts`](src/content.ts) | Shared file and image projection helpers, including request-image offloading |
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, and input modalities — 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. `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.
105
+
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.
105
107
 
106
108
  ### Invariants
107
109
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `@deepseek-ai/dsh-llm` 是位于 harness LLM 能力核心的提供方无关模型调用服务。任何向模型提供方发起流式请求的组合都会经过它,它拥有 agent loop(智能体循环)、会话日志和所有插件共同使用的共享词汇——消息、内容块、原始流式分片与紧凑 Assistant stream record。借助它,你可以注册提供方适配器、流式发起一次模型调用、列出与发现模型、解析精确模型元数据与调用默认值,并捕获每个提供方的重试策略;每个请求都会被记录,因此始终可以从会话日志重建。它不执行重试,也不拥有任何提供方协议逻辑:适配器翻译各自提供方的格式,可选包 `dsh-llm-retry` 在持久步骤边界上重跑失败的请求。请求在分发前会被深度冻结,因此 middleware 与适配器只能读取,绝不能改写。
12
+ 使用 `@deepseek-ai/dsh-llm` 可通过已配置的提供方适配器流式调用模型、发现模型,并解析模型能力与调用默认值。每个已分发请求都可以从会话日志重建。请求在分发前会被深度冻结,因此扩展与适配器可以读取但不能改写。每个流只尝试调用提供方一次:提供方特定的转换由对应适配器完成,可选包 `@deepseek-ai/dsh-llm-retry` 负责重跑失败的请求。流始终以终止结果结束,因此调用方可以一致地处理成功、失败与取消。
13
13
 
14
14
  ## 目录
15
15
 
@@ -60,8 +60,8 @@ 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,配置界面因此可以激活休眠提供方并编辑连接事实,无需重启。
64
- - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型,并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度与输入模态。
63
+ - **通过配置暴露并激活提供方**——适配器声明可配置提供方路由与 settings namespace,配置界面因此可以激活休眠提供方并编辑连接事实,无需重启。`LlmConfigurableProvider.error` 报告供修复的配置诊断;未受影响的模型仍可提供服务。
64
+ - **发现与解析模型**——列出适配器公布的模型、询问端点它提供哪些模型,并解析某个精确模型的上下文窗口、输出默认值、推理(reasoning)强度、输入模态与系统提示词更新模式:当模型把任意位置最新的 `system` 消息读作有效系统提示词时,`LlmResolvedModelInfo.systemPromptUpdate` 为 `'in-history'`;只读取开头 system 消息时该字段缺失;`normalizeModelInfo` 以 `INVALID_MODEL_INFO` 拒绝任何其他值。
65
65
  - **校验调用配置**——显式或配置的推理强度会在任何提供方 I/O 之前对照精确模型校验;请求省略输出上限时,会填入适配器配置的输出上限。
66
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
 
@@ -95,13 +95,15 @@ for await (const chunk of ctx.llm.stream({
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
- 请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度与输入模态——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`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` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 handle 文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。`offloadRequestImagesWithPolicy()` 按原始字节或 base64 大小以及图片数或字节步长,确定性地从最旧图片开始移除;纯函数 `offloadedImagePrefixCount()` 公开同一决策,使路由所属的请求定价无需构建投影即可复现它。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall,随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
105
+
106
+ 文件检测在每次请求时读取当前内容,包括嵌套工具结果,不缓存消息身份或冻结状态。[文件扫描决策](../../../.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md)记录了实测遍历成本。
105
107
 
106
108
  ### 不变式
107
109
 
package/lib/index.js CHANGED
@@ -67,6 +67,26 @@ function createAssistantMessage(input) {
67
67
  });
68
68
  }
69
69
  /**
70
+ * Create and freeze one identified system-role message holding a rendered
71
+ * system prompt.
72
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
73
+ * @param plugin - the plugin that assembled the prompt.
74
+ * @returns an immutable system message with a fresh stable identity.
75
+ */
76
+ function createSystemMessage(text, plugin) {
77
+ return createMessage({
78
+ role: "system",
79
+ content: text.length === 0 ? [] : [{
80
+ type: "text",
81
+ text
82
+ }],
83
+ source: {
84
+ kind: "plugin",
85
+ plugin
86
+ }
87
+ });
88
+ }
89
+ /**
70
90
  * Create and freeze one identified tool-result message.
71
91
  * @param input - call identity, raw result blocks, and outcome.
72
92
  * @returns an immutable user-role tool-result message.
@@ -560,11 +580,13 @@ function contentHasImage(content) {
560
580
  /**
561
581
  * True when typed model content contains a file block, walking nested
562
582
  * tool-result content on the same recursion every file policy shares.
583
+ * Reads current content on every call without retaining scan results.
563
584
  * @param content - typed model content blocks.
564
585
  * @returns whether any nested block is a file.
565
586
  */
566
587
  function contentHasFile(content) {
567
- return content.some((block) => block.type === "file" || block.type === "tool-result" && contentHasFile(block.content));
588
+ for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
589
+ return false;
568
590
  }
569
591
  /**
570
592
  * Stable model-facing handle for one durable file reference: the address of
@@ -2032,6 +2054,8 @@ let LlmRuntime = (() => {
2032
2054
  const context = resolved.context;
2033
2055
  if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
2034
2056
  const inputModalities = this.detachedModalities(resolved.inputModalities);
2057
+ const systemPromptUpdate = resolved.systemPromptUpdate;
2058
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2035
2059
  const defaultMaxTokens = resolved.defaultMaxTokens;
2036
2060
  if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
2037
2061
  const info = {
@@ -2041,7 +2065,8 @@ let LlmRuntime = (() => {
2041
2065
  ...resolved.description === void 0 ? {} : { description: resolved.description },
2042
2066
  ...inputModalities === void 0 ? {} : { inputModalities },
2043
2067
  ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
2044
- ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens }
2068
+ ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
2069
+ ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
2045
2070
  };
2046
2071
  const reasoning = resolved.reasoning;
2047
2072
  if (reasoning === void 0) return info;
@@ -2135,6 +2160,7 @@ let LlmRuntime = (() => {
2135
2160
  adapterDefaults,
2136
2161
  ...context === void 0 ? {} : { context },
2137
2162
  ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
2163
+ ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
2138
2164
  stream: (options) => {
2139
2165
  if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
2140
2166
  if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
@@ -2297,4 +2323,4 @@ function adapterFailureChunk(error, signal) {
2297
2323
  };
2298
2324
  }
2299
2325
  //#endregion
2300
- 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, 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 };
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 };
@@ -20,6 +20,7 @@ const _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema = z.array
20
20
  'settingsNs': z.string(),
21
21
  'settingsPath': z.array(z.string()),
22
22
  'declared': z.boolean().optional(),
23
+ 'error': z.string().optional(),
23
24
  }))
24
25
  const _deepseek_ai_dsh_llm_llm_listProviders_result$schema = z.array(z.object({
25
26
  'id': z.string(),
@@ -67,7 +68,7 @@ export const TYPERT = {
67
68
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
68
69
  schema: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
69
70
  },
70
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":625,"column":9},
71
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":628,"column":9},
71
72
  },
72
73
  {
73
74
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -82,7 +83,7 @@ export const TYPERT = {
82
83
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
83
84
  schema: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
84
85
  },
85
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":538,"column":3},
86
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":541,"column":3},
86
87
  },
87
88
  {
88
89
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -97,7 +98,7 @@ export const TYPERT = {
97
98
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
98
99
  schema: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
99
100
  },
100
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":466,"column":3},
101
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":469,"column":3},
101
102
  },
102
103
  ],
103
104
  model: {
@@ -319,7 +320,7 @@ export const TYPERT = {
319
320
  },
320
321
  {
321
322
  "name": "LlmConfigurableProvider",
322
- "declaration": "export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n}"
323
+ "declaration": "export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n declared?: boolean;\n error?: string;\n}"
323
324
  },
324
325
  {
325
326
  "name": "LlmDiscoveredModel",
@@ -363,7 +364,7 @@ export const TYPERT = {
363
364
  },
364
365
  {
365
366
  "name": "LlmResolvedModelInfo",
366
- "declaration": "export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n defaultMaxTokens?: number;\n reasoning?: LlmModelReasoningInfo;\n}"
367
+ "declaration": "export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n defaultMaxTokens?: number;\n reasoning?: LlmModelReasoningInfo;\n systemPromptUpdate?: SystemPromptUpdate;\n}"
367
368
  },
368
369
  {
369
370
  "name": "Message",
@@ -403,7 +404,7 @@ export const TYPERT = {
403
404
  },
404
405
  {
405
406
  "name": "PreparedLlmCall",
406
- "declaration": "export interface PreparedLlmCall {\n readonly config: LlmCallConfig;\n readonly retryPolicy: ResolvedRetryPolicy;\n readonly context?: LlmModelContext;\n readonly inputModalities?: readonly ModelModality[];\n readonly adapterDefaults: LlmCallConfigAdapterDefaults;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}"
407
+ "declaration": "export interface PreparedLlmCall {\n readonly config: LlmCallConfig;\n readonly retryPolicy: ResolvedRetryPolicy;\n readonly context?: LlmModelContext;\n readonly inputModalities?: readonly ModelModality[];\n readonly systemPromptUpdate?: SystemPromptUpdate;\n readonly adapterDefaults: LlmCallConfigAdapterDefaults;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}"
407
408
  },
408
409
  {
409
410
  "name": "ProviderRequestId",
@@ -465,6 +466,10 @@ export const TYPERT = {
465
466
  "name": "SubagentSettledMessageSource",
466
467
  "declaration": "export interface SubagentSettledMessageSource {\n readonly kind: 'subagent-settled';\n readonly form: 'notice';\n readonly summary: string;\n readonly senderSessionId: SessionId;\n}"
467
468
  },
469
+ {
470
+ "name": "SystemPromptUpdate",
471
+ "declaration": "export type SystemPromptUpdate = 'in-history';"
472
+ },
468
473
  {
469
474
  "name": "TeamId",
470
475
  "declaration": "export type TeamId = Branded<'TeamId'>;"
@@ -20,6 +20,7 @@ const _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema = z.array
20
20
  'settingsNs': z.string(),
21
21
  'settingsPath': z.array(z.string()),
22
22
  'declared': z.boolean().optional(),
23
+ 'error': z.string().optional(),
23
24
  }))
24
25
  const _deepseek_ai_dsh_llm_llm_listProviders_result$schema = z.array(z.object({
25
26
  'id': z.string(),
@@ -64,7 +65,7 @@ export const TYPERT_REMOTE = {
64
65
  typeSymbol: '@deepseek-ai/dsh-llm#llm/discoverModels:result',
65
66
  schema: _deepseek_ai_dsh_llm_llm_discoverModels_result$schema,
66
67
  },
67
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":625,"column":9},
68
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":628,"column":9},
68
69
  },
69
70
  {
70
71
  id: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders',
@@ -79,7 +80,7 @@ export const TYPERT_REMOTE = {
79
80
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listConfigurableProviders:result',
80
81
  schema: _deepseek_ai_dsh_llm_llm_listConfigurableProviders_result$schema,
81
82
  },
82
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":538,"column":3},
83
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":541,"column":3},
83
84
  },
84
85
  {
85
86
  id: '@deepseek-ai/dsh-llm#llm/listProviders',
@@ -94,7 +95,7 @@ export const TYPERT_REMOTE = {
94
95
  typeSymbol: '@deepseek-ai/dsh-llm#llm/listProviders:result',
95
96
  schema: _deepseek_ai_dsh_llm_llm_listProviders_result$schema,
96
97
  },
97
- sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":466,"column":3},
98
+ sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":469,"column":3},
98
99
  },
99
100
  ],
100
101
  }
@@ -60,6 +60,7 @@ export declare function contentHasImage(content: readonly ContentBlock[]): boole
60
60
  /**
61
61
  * True when typed model content contains a file block, walking nested
62
62
  * tool-result content on the same recursion every file policy shares.
63
+ * Reads current content on every call without retaining scan results.
63
64
  * @param content - typed model content blocks.
64
65
  * @returns whether any nested block is a file.
65
66
  */
@@ -92,12 +92,17 @@ export function contentHasImage(content) {
92
92
  /**
93
93
  * True when typed model content contains a file block, walking nested
94
94
  * tool-result content on the same recursion every file policy shares.
95
+ * Reads current content on every call without retaining scan results.
95
96
  * @param content - typed model content blocks.
96
97
  * @returns whether any nested block is a file.
97
98
  */
98
99
  export function contentHasFile(content) {
99
- return content.some(block => block.type === 'file'
100
- || (block.type === 'tool-result' && contentHasFile(block.content)));
100
+ for (const block of content) {
101
+ if (block.type === 'file'
102
+ || (block.type === 'tool-result' && contentHasFile(block.content)))
103
+ return true;
104
+ }
105
+ return false;
101
106
  }
102
107
  /**
103
108
  * Stable model-facing handle for one durable file reference: the address of
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Context } from '@deepseek-ai/cordis';
9
9
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
10
- import type { GenerateOptions, LlmConfigurableProvider, LlmDiscoveredModel, LlmFailure, LlmImageRequestPricing, LlmModelContext, LlmModelDiscoveryRequest, LlmModelInfo, LlmResolvedModelInfo, LlmProviderInfo, ModelModality, StreamChunk } from './types.ts';
10
+ import type { GenerateOptions, LlmConfigurableProvider, LlmDiscoveredModel, LlmFailure, LlmImageRequestPricing, LlmModelContext, LlmModelDiscoveryRequest, LlmModelInfo, LlmResolvedModelInfo, LlmProviderInfo, ModelModality, StreamChunk, SystemPromptUpdate } from './types.ts';
11
11
  import type { ResolvedRetryPolicy } from './retry-policy.ts';
12
12
  import type { ProviderRequestId } from './brand.ts';
13
13
  import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts';
@@ -97,6 +97,8 @@ export interface PreparedLlmCall {
97
97
  readonly context?: LlmModelContext;
98
98
  /** Exact model modalities captured with the adapter dispatch generation. */
99
99
  readonly inputModalities?: readonly ModelModality[];
100
+ /** Exact model system prompt update mode captured with the adapter dispatch generation. */
101
+ readonly systemPromptUpdate?: SystemPromptUpdate;
100
102
  /** Config fields materialized by the captured adapter rather than proposed by the caller. */
101
103
  readonly adapterDefaults: LlmCallConfigAdapterDefaults;
102
104
  /**
@@ -610,6 +610,11 @@ let LlmRuntime = (() => {
610
610
  // Capability metadata rides through: an explicit modality omission is
611
611
  // negative capability downstream preflights act on (image admission).
612
612
  const inputModalities = this.detachedModalities(resolved.inputModalities);
613
+ // Widened: adapters derive this mode from catalog config, so the value is checked as a string.
614
+ const systemPromptUpdate = resolved.systemPromptUpdate;
615
+ if (systemPromptUpdate !== undefined && systemPromptUpdate !== 'in-history') {
616
+ throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, 'INVALID_MODEL_INFO');
617
+ }
613
618
  const defaultMaxTokens = resolved.defaultMaxTokens;
614
619
  if (defaultMaxTokens !== undefined
615
620
  && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) {
@@ -623,6 +628,7 @@ let LlmRuntime = (() => {
623
628
  ...inputModalities === undefined ? {} : { inputModalities },
624
629
  ...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
625
630
  ...defaultMaxTokens === undefined ? {} : { defaultMaxTokens },
631
+ ...resolved.systemPromptUpdate === undefined ? {} : { systemPromptUpdate: resolved.systemPromptUpdate },
626
632
  };
627
633
  const reasoning = resolved.reasoning;
628
634
  if (reasoning === undefined)
@@ -738,6 +744,7 @@ let LlmRuntime = (() => {
738
744
  ...modelInfo.inputModalities === undefined
739
745
  ? {}
740
746
  : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
747
+ ...modelInfo.systemPromptUpdate === undefined ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
741
748
  stream: (options) => {
742
749
  if (dispatched) {
743
750
  throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
@@ -136,6 +136,15 @@ export interface AssistantMessage extends Message {
136
136
  readonly role: 'assistant';
137
137
  readonly source: ModelMessageSource;
138
138
  }
139
+ /**
140
+ * A system-role specialization of the shared message representation: one
141
+ * rendered system prompt attributed to the plugin that assembled it. Empty
142
+ * `content` means "no system prompt" and projects to no wire message.
143
+ */
144
+ export interface SystemMessage extends Message {
145
+ readonly role: 'system';
146
+ readonly source: MessageSourceMap['plugin'];
147
+ }
139
148
  /** A tool-result specialization whose model-facing block retains call correlation. */
140
149
  export interface ToolResultMessage extends Message {
141
150
  readonly role: 'user';
@@ -181,6 +190,14 @@ export declare function createAssistantMessage(input: NewAssistantMessage & {
181
190
  readonly id?: never;
182
191
  readonly role?: never;
183
192
  }): AssistantMessage;
193
+ /**
194
+ * Create and freeze one identified system-role message holding a rendered
195
+ * system prompt.
196
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
197
+ * @param plugin - the plugin that assembled the prompt.
198
+ * @returns an immutable system message with a fresh stable identity.
199
+ */
200
+ export declare function createSystemMessage(text: string, plugin: string): SystemMessage;
184
201
  /** Input whose acceptance creates one tool-result message. */
185
202
  export interface ToolResultMessageInput {
186
203
  readonly callId: ToolCallId;
@@ -63,6 +63,20 @@ export function createAssistantMessage(input) {
63
63
  },
64
64
  });
65
65
  }
66
+ /**
67
+ * Create and freeze one identified system-role message holding a rendered
68
+ * system prompt.
69
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
70
+ * @param plugin - the plugin that assembled the prompt.
71
+ * @returns an immutable system message with a fresh stable identity.
72
+ */
73
+ export function createSystemMessage(text, plugin) {
74
+ return createMessage({
75
+ role: 'system',
76
+ content: text.length === 0 ? [] : [{ type: 'text', text }],
77
+ source: { kind: 'plugin', plugin },
78
+ });
79
+ }
66
80
  /**
67
81
  * Create and freeze one identified tool-result message.
68
82
  * @param input - call identity, raw result blocks, and outcome.
@@ -217,6 +217,8 @@ export interface LlmConfigurableProvider {
217
217
  * from outside.
218
218
  */
219
219
  declared?: boolean;
220
+ /** Configuration diagnostic for repair; unaffected models may remain serviceable. */
221
+ error?: string;
220
222
  }
221
223
  /**
222
224
  * One interrogation of a provider endpoint that configuration has not stored
@@ -308,6 +310,13 @@ export interface LlmModelReasoningInfo {
308
310
  */
309
311
  defaultEffort?: ReasoningEffortId;
310
312
  }
313
+ /**
314
+ * How a model applies a system prompt that changes mid-conversation.
315
+ * `'in-history'`: the model reads the latest `system` message at any position
316
+ * of `messages` as the complete effective system prompt, so a changed prompt
317
+ * can follow the cached history instead of rewriting message 0.
318
+ */
319
+ export type SystemPromptUpdate = 'in-history';
311
320
  /** Exact-route model metadata resolved by its owning adapter. */
312
321
  export interface LlmResolvedModelInfo extends LlmModelInfo {
313
322
  /** Provider-owned context capacity when known. */
@@ -316,6 +325,8 @@ export interface LlmResolvedModelInfo extends LlmModelInfo {
316
325
  defaultMaxTokens?: number;
317
326
  /** Adapter-owned selectable reasoning levels when exposed. */
318
327
  reasoning?: LlmModelReasoningInfo;
328
+ /** Declared mid-conversation system prompt handling; absent means only a leading system message is read. */
329
+ systemPromptUpdate?: SystemPromptUpdate;
319
330
  }
320
331
  /**
321
332
  * Adapter-private lossless-JSON state for replaying a successful response,
@@ -397,12 +408,16 @@ export interface GenerateOptions {
397
408
  /** Adapter-owned reasoning effort selected for this exact model. */
398
409
  reasoningEffort?: ReasoningEffortId;
399
410
  /**
400
- * Ordered conversation messages, exactly as the provider sees them (after
401
- * the `system` slot). A loop-built request assembles them as
402
- * the derived history (dsh-agent-loop); a hand-built one-shot passes any list.
411
+ * Ordered conversation messages, exactly as the provider sees them. A
412
+ * loop-built request passes the derived history (dsh-agent-loop), whose
413
+ * leading system-role message carries the system prompt; a hand-built
414
+ * one-shot passes any list.
403
415
  */
404
416
  messages: Message[];
405
- /** System prompt text (adapters map to the provider's system slot). */
417
+ /**
418
+ * System prompt text for one-shot callers; adapters map it to the provider's
419
+ * system slot ahead of `messages`. Loop-built requests leave it undefined.
420
+ */
406
421
  system?: string;
407
422
  /** Tool schemas (adapters map to the provider's `tools` field). */
408
423
  tools?: ToolSchema[];
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.3-alpha.2",
4
+ "version": "0.1.5-alpha.2",
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.3-alpha.2",
69
- "@deepseek-ai/dsh-util-values": "^0.1.3-alpha.2",
70
- "@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2",
71
68
  "@deepseek-ai/schemastery": "^3.18.2",
72
- "@deepseek-ai/dsh-brand": "^0.1.3-alpha.2",
73
- "@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2"
69
+ "@deepseek-ai/dsh-timeout": "^0.1.5-alpha.2",
70
+ "@deepseek-ai/dsh-brand": "^0.1.5-alpha.2",
71
+ "@deepseek-ai/dsh-util-values": "^0.1.5-alpha.2",
72
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.5-alpha.2",
73
+ "@deepseek-ai/dsh-util-crypto": "^0.1.5-alpha.2"
74
74
  },
75
75
  "devDependencies": {
76
- "@deepseek-ai/dsh-attachment": "^0.1.3-alpha.2",
77
- "@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
76
+ "@deepseek-ai/dsh-attachment": "^0.1.5-alpha.2",
77
+ "@deepseek-ai/dsh-invariants": "^0.1.5-alpha.2",
78
78
  "@deepseek-ai/cordis": "^4.0.2"
79
79
  }
80
80
  }