@deepseek-ai/dsh-llm 0.1.0-rc.8 → 0.1.1-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +5 -3
- package/README.zh.md +11 -5
- package/lib/index.js +211 -79
- package/lib/types/content.d.ts +46 -0
- package/lib/types/content.js +92 -13
- package/lib/types/index.d.ts +24 -1
- package/lib/types/index.js +61 -8
- package/package.json +10 -10
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 59c5303bdae8d6391f3bb1595a527d677e2378bc
|
|
6
|
+
README.zh.md: 313fc23590de2119bf07dd5c4c4fafe9daa94c56
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Each provider adapter supplies its resolved route policy. Omitting provider conf
|
|
|
25
25
|
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` Discover the models one registered provider currently advertises.
|
|
26
26
|
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` Resolve validated exact-model identity plus available context, output-default, and reasoning metadata from the owning adapter, with optional cancellation for asynchronous adapters.
|
|
27
27
|
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` Validate an explicit effort and materialize adapter-configured call defaults without clamping.
|
|
28
|
-
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` Resolve a config plus detached context metadata and markers for fields supplied by adapter defaults in one exact-model lookup, then capture
|
|
28
|
+
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` Resolve a config plus detached context and modality metadata and markers for fields supplied by adapter defaults in one exact-model lookup, then capture the adapter's matching dispatch generation and immutable retry policy as one cancellable, one-shot call.
|
|
29
29
|
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` Stream one model call as raw chunks (token-level deltas). Consumers assemble the chunks into blocks/messages with `BlockAssembler`.
|
|
30
30
|
|
|
31
31
|
`LlmRuntime` normalizes failures from final adapter selection, synchronous dispatch, iterator construction, and iteration into the stream protocol's single terminal form: `finish { kind: 'error' | 'aborted', failure }`. A failure after partial deltas may leave content blocks open; consumers discard that incomplete output. Errors from `llm/stream` middleware, nested calls, adapter cleanup, and downstream consumers remain thrown because they are plugin or consumer failures rather than model-request outcomes. A prepared call exposes the immutable retry policy captured with its exact adapter registration; a route handled entirely by middleware has no serving policy.
|
|
@@ -38,7 +38,7 @@ Every topology commit point — adapter routes registering or disposing, directo
|
|
|
38
38
|
|
|
39
39
|
Exact-model metadata is a separate correctness query, not a catalog decoration or global LLM setting. `resolveModelInfo()` asks the adapter that owns the exact provider/model route once; an adapter can describe an unlisted dynamic model, and absent `context`, `defaultMaxTokens`, or `reasoning` fields preserve unknown capacity, provider-owned output defaults, or unavailable reasoning capability. Invalid identity, context, output default, or reasoning metadata fails with `INVALID_MODEL_INFO`, `INVALID_MODEL_CONTEXT`, `INVALID_MODEL_MAX_TOKENS`, or `INVALID_MODEL_REASONING`.
|
|
40
40
|
|
|
41
|
-
`defaultMaxTokens` is an adapter-configured per-request output cap, not a model hard limit. `resolveCallConfig()` materializes it only when the request omits `maxTokens`; an explicit cap wins. Reasoning identifiers are opaque adapter-owned strings rather than a core enum: the same resolution accepts only an exact advertised identifier, materializes `defaultEffort` when present, and otherwise preserves the provider default. Asynchronous model resolvers receive the caller's signal and must settle promptly after cancellation. `prepareCall()` additionally exposes detached context
|
|
41
|
+
`defaultMaxTokens` is an adapter-configured per-request output cap, not a model hard limit. `resolveCallConfig()` materializes it only when the request omits `maxTokens`; an explicit cap wins. Reasoning identifiers are opaque adapter-owned strings rather than a core enum: the same resolution accepts only an exact advertised identifier, materializes `defaultEffort` when present, and otherwise preserves the provider default. Asynchronous model resolvers receive the caller's signal and must settle promptly after cancellation. `prepareCall()` additionally exposes detached context and input-modality metadata, reports which `maxTokens` and `reasoningEffort` fields it materialized in `adapterDefaults`, and binds those facts to the adapter generation that performs terminal dispatch. HMR or dynamic settings therefore cannot combine one generation's image capability with another generation's endpoint; reusing the one-shot handle or changing its call-config fields fails with `INVALID_PREPARED_CALL`. An unsupported explicit or configured effort fails with `UNSUPPORTED_REASONING_EFFORT` before provider I/O.
|
|
42
42
|
|
|
43
43
|
### Events
|
|
44
44
|
|
|
@@ -55,7 +55,9 @@ Exact-model metadata is a separate correctness query, not a catalog decoration o
|
|
|
55
55
|
|
|
56
56
|
`Message` is the shared immutable value used by delivery, durable history, and model requests. Every message has a required `MessageId`, role, content, and typed source from creation onward. `createMessage(input)` mints the identity and returns a detached deep-frozen value; `createUserMessage({ content, source })` fixes the user role; `createAssistantMessage({ content, source })` fixes the assistant role and model source kind; `createToolResultMessage({ callId, content, isError })` fixes the user role and couples the tool source to its result block; `freezeMessage(message)` imports an identity that already exists and never replaces it. Message rewrites preserve the identity and produce another frozen value. Browser code imports these value constructors from the dependency-minimal `@deepseek-ai/dsh-llm/message` entry instead of the service-bearing package root.
|
|
57
57
|
|
|
58
|
-
Message content is an array of typed blocks: `text`, `reasoning`, `tool-call`, `tool-result`.
|
|
58
|
+
Message content is an array of typed blocks: `text`, `reasoning`, `image`, `tool-call`, `tool-result`. An `ImageBlock` carries only a durable `ImageAttachmentRef`; provider bytes and request dimensions are resolved later. The union remains merge-extensible through `ContentBlockMap`, so plugins can add further block types via declaration merging. Assistant messages use a model source carrying the provider and model that produced them plus optional adapter-private replay state. Before dispatch, `LlmRuntime` retains that state only when the historical provider route and target provider route are currently owned by the exact same adapter instance; the adapter then decides whether it can restore or convert the state across models/providers.
|
|
59
|
+
|
|
60
|
+
Every dispatch uses the exact model modalities captured with its adapter generation. An image-capable adapter projects durable image references into route-specific request versions. A text-only route instead receives deterministic attachment placeholders, including nested tool-result images, without changing append-only session history. `offloadRequestImagesWithPolicy()` provides deterministic oldest-first image removal with raw or base64 accounting and count or byte quanta; adapters supply the exact derived-version byte length.
|
|
59
61
|
|
|
60
62
|
Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`). Every adapter outcome reaches consumers as one terminal `finish`; operational failure uses its `error` or `aborted` reason rather than throwing across the stream API. `BlockAssembler` is the single shared implementation that assembles chunks into blocks/messages. A successful `finish` may carry a `ReplayEnvelope` — opaque response-level replay metadata plus optional per-block entries aligned with the emitted block sequence. Assembly makes one keep/drop decision for content and metadata together: a `max-tokens` finish drops tool calls that may have been truncated, and the envelope loses the entry at each dropped position, so stored metadata always describes stored content.
|
|
61
63
|
|
package/README.zh.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` 发现某个已注册提供方当前公布的模型。
|
|
26
26
|
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` 从拥有该精确路由的适配器中,解析并校验确切模型身份,以及可用上下文、输出默认值和推理(reasoning)元数据;异步适配器可选地支持取消。
|
|
27
27
|
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` 校验显式推理强度,并填入适配器配置的调用默认值,但不自动调整。
|
|
28
|
-
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>`
|
|
28
|
+
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` 在一次精确模型查询中解析配置、脱耦的上下文与模态元数据以及标明哪些字段由适配器默认值填入的标记,再把适配器匹配的分发世代和不可变重试策略捕获为一次可取消、一次性调用。
|
|
29
29
|
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` 将一次模型调用流式输出为原始分片(token 级增量)。消费方使用 `BlockAssembler` 将分片组装为块/消息。
|
|
30
30
|
|
|
31
31
|
`LlmRuntime` 将最终适配器选择、同步分发、迭代器构造和迭代期间的失败,统一转换为流协议唯一的终止形式:`finish { kind: 'error' | 'aborted', failure }`。部分增量输出后发生失败时,内容块可能仍未闭合;消费方会丢弃这些不完整输出。`llm/stream` middleware、嵌套调用、适配器清理和下游消费方的错误仍会抛出,因为它们属于插件或消费方失败,而非模型请求结果。已准备调用会暴露随其确切适配器注册一同捕获的不可变重试策略;完全由 middleware 处理的路由没有服务策略。
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
确切模型元数据是独立的正确性查询,不是 catalog 装饰或全局 LLM 设置。`resolveModelInfo()` 会向拥有精确提供方/模型路由的适配器查询一次;适配器可以描述未列出的动态模型。缺少 `context` 表示模型容量未知;缺少 `defaultMaxTokens` 表示继续沿用提供方自身的输出默认值;缺少 `reasoning` 则表示推理能力不可用。无效的身份、上下文、输出默认值或推理元数据会以 `INVALID_MODEL_INFO`、`INVALID_MODEL_CONTEXT`、`INVALID_MODEL_MAX_TOKENS` 或 `INVALID_MODEL_REASONING` 失败。
|
|
40
40
|
|
|
41
|
-
`defaultMaxTokens` 是适配器配置的单次请求输出上限,不是模型硬上限。仅当请求省略 `maxTokens` 时,`resolveCallConfig()` 才会填入该值;显式上限优先。推理标识符是由适配器定义的不透明字符串,而非核心枚举:同一次解析只接受与已公布标识符完全一致的值,在存在 `defaultEffort` 时填入它,否则保留提供方默认值。异步模型解析器会接收调用方信号,并且必须在取消后尽快结算。`prepareCall()`
|
|
41
|
+
`defaultMaxTokens` 是适配器配置的单次请求输出上限,不是模型硬上限。仅当请求省略 `maxTokens` 时,`resolveCallConfig()` 才会填入该值;显式上限优先。推理标识符是由适配器定义的不透明字符串,而非核心枚举:同一次解析只接受与已公布标识符完全一致的值,在存在 `defaultEffort` 时填入它,否则保留提供方默认值。异步模型解析器会接收调用方信号,并且必须在取消后尽快结算。`prepareCall()` 还会公开脱离内部状态的上下文和输入模态元数据,通过 `adapterDefaults` 标明填入了哪些 `maxTokens` 和 `reasoningEffort` 字段,并把这些事实绑定到执行最终分发的适配器世代。因此,HMR(热模块替换)或动态 settings 不会把一个世代的图片能力与另一个世代的端点组合;复用一次性句柄或更改调用配置字段会以 `INVALID_PREPARED_CALL` 失败。不支持的显式或配置推理强度会在提供方 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
|
42
42
|
|
|
43
43
|
### 事件
|
|
44
44
|
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
|
|
56
56
|
`Message` 是投递、持久历史和模型请求共享的不可变值。每条消息从创建起都必须具有 `MessageId`、角色、内容和带类型的来源。`createMessage(input)` 生成标识,并返回与输入分离且深度冻结的值;`createUserMessage({ content, source })` 固定 user 角色;`createAssistantMessage({ content, source })` 固定 assistant 角色与模型来源类别;`createToolResultMessage({ callId, content, isError })` 固定 user 角色,并将工具来源与其结果块耦合;`freezeMessage(message)` 导入已有标识,绝不将其替换。改写消息时会保留标识,并产生另一个冻结值。浏览器端代码会从依赖最少的 `@deepseek-ai/dsh-llm/message` 入口导入这些值构造函数,而不是从包含服务的包根入口导入。
|
|
57
57
|
|
|
58
|
-
消息内容是类型化内容块数组:`text`、`reasoning`、`tool-call`、`tool-result
|
|
58
|
+
消息内容是类型化内容块数组:`text`、`reasoning`、`image`、`tool-call`、`tool-result`。`ImageBlock` 只携带持久 `ImageAttachmentRef`;提供方字节和请求尺寸之后再解析。联合仍从可合并扩展的 `ContentBlockMap` 派生,因此插件可以通过 declaration merging 添加其他块类型。assistant 消息使用模型来源,其中携带生成该消息的提供方和模型,以及可选的适配器私有回放状态。dispatch 前,`LlmRuntime` 只在历史提供方路由与目标提供方路由当前由完全相同的适配器实例拥有时才保留该状态;随后由适配器判定能否在模型或提供方间恢复或转换该状态。
|
|
59
|
+
|
|
60
|
+
每次分发都使用随适配器世代捕获的确切模型模态。支持图片的适配器把持久图片引用投影为路由专用请求版本。纯文本路由则收到确定性的附件占位文本,其中也包括嵌套工具结果图片,追加式会话历史不会改变。`offloadRequestImagesWithPolicy()` 提供确定性的从旧到新图片移除,支持按原始字节或 base64 计数,也支持图片数量或字节量步长;适配器提供确切派生版本的字节长度。
|
|
59
61
|
|
|
60
62
|
流式输出是原始分片协议(`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`)。每个适配器结果都以一个终止 `finish` 到达消费方;运行故障使用 `error` 或 `aborted` 作为结束原因,而不会跨流 API 抛出。`BlockAssembler` 是将分片组装为块/消息的唯一共享实现。成功的 `finish` 可以携带 `ReplayEnvelope`——不透明的响应级回放元数据,加上与发射块序列对齐的可选逐块条目。组装对内容与元数据只做一次保留/丢弃决定:`max-tokens` 结束会丢弃可能被截断的工具调用,数据在每个被丢弃的位置同步失去对应条目,因此存储的元数据始终描述存储的内容。
|
|
61
63
|
|
|
@@ -63,9 +65,11 @@
|
|
|
63
65
|
|
|
64
66
|
`LlmCallConfig` 记录一个会话的模型请求所使用的提供方、模型、由适配器定义的可选推理强度,以及采样参数(`provider`、`model`、`reasoningEffort`、`temperature`、`maxTokens`、`stop`,分别与同名 `GenerateOptions` 字段一一对应)。它是作为请求标头一部分记录在会话日志中的每会话状态(见 dsh-session `request/header` 事件),绝不是可静默调整的每次调用旋钮:`agent/request` waterfall 会提议替换,`prepareCall()` 在轮次 signal 控制下校验它并填入适配器默认值,loop 随后记录生效值以及标明哪些字段由适配器默认值填入的标记,再使用已准备调用中与注册绑定的流。下一次提议会省略带标记的默认值,使变更后的路由解析自身的值;未带标记的显式字段会保留。`callConfigEquals(a, b)` 是逐字段真实变更检测器;`deepFreeze(value)` 是 loop 使用的请求所有权辅助函数:每个构造完成的请求都会在分发前深度冻结;`llm/stream` 监听器和适配器只能读取,绝不能改写。`markAgentLoopRequest()` 将该精确对象标记为由进程本地 agent loop 创建,`isAgentLoopRequest()` 让观测方可以将其与同样可能冻结并关联会话、但独立记录的辅助调用区分。`GenerateOptions.purpose` 会对已记录的辅助压缩和会话标题调用进行分类,使适配器可以按调用目的应用不同的传输策略,而不改变普通会话请求。
|
|
65
67
|
|
|
68
|
+
<a id="app-attribution-attributionts"></a>
|
|
69
|
+
|
|
66
70
|
### 应用归因(`attribution.ts`)
|
|
67
71
|
|
|
68
|
-
每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
|
72
|
+
每个产品适配器都会在提供方 HTTP 请求上发送应用身份。`attributionHeaders(identity?)` 构建标准 `User-Agent`,默认为公开 `APP_IDENTITY`;白标部署可以替换它,但不能抑制它。适配器会直接验证 wire 标头,或通过自身库 hook 验证。详见 [归因 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md)。
|
|
69
73
|
|
|
70
74
|
### API 密钥校验(`api-key.ts`)
|
|
71
75
|
|
|
@@ -85,7 +89,7 @@
|
|
|
85
89
|
|
|
86
90
|
### 真实适配器
|
|
87
91
|
|
|
88
|
-
两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSE(Server-Sent Events)分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定:usage 先于 finish,工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish;`LlmRuntime` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md)。
|
|
92
|
+
两个适配器使用不同内部机制实现 `LlmAdapter`:[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSE(Server-Sent Events)分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `types.ts` 中的 `StreamChunk` 约定:usage 先于 finish,工具参数保持原始字符串。适配器实现在内部可以抛出异常或发出失败 finish;`LlmRuntime` 会将两者都暴露为终止失败 finish。适配器理由见[双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md),服务边界见[终止失败决策](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.zh.md)。
|
|
89
93
|
|
|
90
94
|
## 模型体验
|
|
91
95
|
|
|
@@ -95,6 +99,8 @@
|
|
|
95
99
|
|
|
96
100
|
透传;注册表保留已组装请求前缀,cache 复用与路由边界属于所选适配器和提供方。
|
|
97
101
|
|
|
102
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
103
|
+
|
|
98
104
|
## 已知限制与暂缓事项
|
|
99
105
|
|
|
100
106
|
- **本服务不执行重试、缓存或速率限制**:提供方注册会存储重试策略,但 `llm/stream` 仍是单次尝试调用包装层。agent loop 会将已验证模型请求失败单独提供给 `agent/request-error`,其默认行为是保留原始失败;`@deepseek-ai/dsh-llm-retry` 是共享示例主干加载的可选执行器。
|
package/lib/index.js
CHANGED
|
@@ -580,72 +580,26 @@ function normalizeApiKey(raw) {
|
|
|
580
580
|
};
|
|
581
581
|
}
|
|
582
582
|
//#endregion
|
|
583
|
-
//#region lib/types/
|
|
584
|
-
/**
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
588
|
-
*
|
|
589
|
-
* App-attribution vocabulary for provider requests.
|
|
590
|
-
* @module @deepseek-ai/dsh-llm/attribution
|
|
591
|
-
*/
|
|
592
|
-
const { version } = createRequire(import.meta.url)("../package.json");
|
|
593
|
-
/**
|
|
594
|
-
* The harness's own identity: the default every adapter sends. Deployments
|
|
595
|
-
* that need a white-label identity pass their own {@link AppIdentity} to
|
|
596
|
-
* {@link attributionHeaders} — omission falls back to this default; nothing
|
|
597
|
-
* can suppress attribution entirely.
|
|
598
|
-
*/
|
|
599
|
-
const APP_IDENTITY = {
|
|
600
|
-
product: "deepseek-harness",
|
|
601
|
-
version,
|
|
602
|
-
url: "https://github.com/deepseek-ai/deepseek-harness"
|
|
603
|
-
};
|
|
583
|
+
//#region lib/types/content.js
|
|
584
|
+
/** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */
|
|
585
|
+
/** Model-facing stand-in for an image removed to fit a provider request bound. */
|
|
586
|
+
const OFFLOADED_IMAGE_TEXT = "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]";
|
|
604
587
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
* @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
|
|
609
|
-
* @returns the ready-to-send header value.
|
|
588
|
+
* Stable text shown to a model that cannot accept one durable image reference.
|
|
589
|
+
* @param ref - durable master reference omitted from the request.
|
|
590
|
+
* @returns deterministic text-only placeholder.
|
|
610
591
|
*/
|
|
611
|
-
function
|
|
612
|
-
return
|
|
592
|
+
function textOnlyImageText(ref) {
|
|
593
|
+
return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
|
|
613
594
|
}
|
|
614
595
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
* @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
|
|
619
|
-
* @returns headers to merge into the provider request (currently just `user-agent`).
|
|
596
|
+
* Stable model-facing handle for one exact request image.
|
|
597
|
+
* @param version - exact request image shown beside the text.
|
|
598
|
+
* @returns attachment handle and request-image dimensions.
|
|
620
599
|
*/
|
|
621
|
-
function
|
|
622
|
-
return {
|
|
600
|
+
function requestImageHandleText(version) {
|
|
601
|
+
return `Image ${version.attachment.attachmentId}; request image ${version.width}x${version.height}px.`;
|
|
623
602
|
}
|
|
624
|
-
//#endregion
|
|
625
|
-
//#region lib/types/never.js
|
|
626
|
-
/**
|
|
627
|
-
* Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
|
|
628
|
-
* new variant fails compilation at every required handler. Do not use it for declaration-merged
|
|
629
|
-
* unions such as session events or content blocks: handle known variants and explicitly fall
|
|
630
|
-
* through because plugins may add valid unknown cases.
|
|
631
|
-
* @module @deepseek-ai/dsh-llm/never
|
|
632
|
-
*/
|
|
633
|
-
/**
|
|
634
|
-
* Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
|
|
635
|
-
* a value that escaped its type throws with diagnostics at runtime.
|
|
636
|
-
* @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
|
|
637
|
-
* @param context - optional label (e.g. the switch site) prefixed into the throw message.
|
|
638
|
-
* @returns never — it always throws, with the offending value JSON-rendered in the message.
|
|
639
|
-
*/
|
|
640
|
-
function assertNever(value, context) {
|
|
641
|
-
const rendered = JSON.stringify(value) ?? String(value);
|
|
642
|
-
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
643
|
-
}
|
|
644
|
-
//#endregion
|
|
645
|
-
//#region lib/types/content.js
|
|
646
|
-
/** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */
|
|
647
|
-
/** Model-facing stand-in for an image removed to fit a provider request bound. */
|
|
648
|
-
const OFFLOADED_IMAGE_TEXT = "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]";
|
|
649
603
|
/**
|
|
650
604
|
* True when typed model content contains an image block, walking nested
|
|
651
605
|
* tool-result content. This is the one recursive image walk shared by every
|
|
@@ -661,10 +615,12 @@ function contentHasImage(content) {
|
|
|
661
615
|
function base64Length(bytes) {
|
|
662
616
|
return Math.ceil(bytes / 3) * 4;
|
|
663
617
|
}
|
|
664
|
-
/** Collect
|
|
665
|
-
function collectImageLengths(blocks, lengths) {
|
|
666
|
-
for (const block of blocks) if (block.type === "image")
|
|
667
|
-
|
|
618
|
+
/** Collect represented image lengths in request and nested-block order. */
|
|
619
|
+
function collectImageLengths(blocks, lengths, policy) {
|
|
620
|
+
for (const block of blocks) if (block.type === "image") {
|
|
621
|
+
const bytes = policy.byteLength === void 0 ? block.attachment.bytes : policy.byteLength(block.attachment);
|
|
622
|
+
lengths.push(policy.representation === "base64" ? base64Length(bytes) : bytes);
|
|
623
|
+
} else if (block.type === "tool-result") collectImageLengths(block.content, lengths, policy);
|
|
668
624
|
}
|
|
669
625
|
/** Replace the first `remaining.count` image occurrences without mutating durable messages. */
|
|
670
626
|
function replaceOldestImages(blocks, remaining) {
|
|
@@ -694,6 +650,48 @@ function replaceOldestImages(blocks, remaining) {
|
|
|
694
650
|
}
|
|
695
651
|
return next ?? blocks;
|
|
696
652
|
}
|
|
653
|
+
/** Replace every image occurrence, including nested tool results, for a text-only model. */
|
|
654
|
+
function replaceImagesForTextModel(blocks) {
|
|
655
|
+
let next;
|
|
656
|
+
for (const [index, block] of blocks.entries()) {
|
|
657
|
+
if (block.type === "image") {
|
|
658
|
+
next ??= blocks.slice(0, index);
|
|
659
|
+
next.push({
|
|
660
|
+
type: "text",
|
|
661
|
+
text: textOnlyImageText(block.attachment)
|
|
662
|
+
});
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (block.type === "tool-result") {
|
|
666
|
+
const content = replaceImagesForTextModel(block.content);
|
|
667
|
+
if (content !== block.content) {
|
|
668
|
+
next ??= blocks.slice(0, index);
|
|
669
|
+
next.push({
|
|
670
|
+
...block,
|
|
671
|
+
content
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
next?.push(block);
|
|
677
|
+
}
|
|
678
|
+
return next ?? blocks;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Project durable image history into deterministic text for an exact text-only model.
|
|
682
|
+
* @param messages - complete request history.
|
|
683
|
+
* @returns the original list without images, otherwise shallow message copies with stable placeholders.
|
|
684
|
+
*/
|
|
685
|
+
function projectImagesForTextModel(messages) {
|
|
686
|
+
if (!messages.some((message) => contentHasImage(message.content))) return messages;
|
|
687
|
+
return messages.map((message) => {
|
|
688
|
+
const content = replaceImagesForTextModel(message.content);
|
|
689
|
+
return content === message.content ? message : {
|
|
690
|
+
...message,
|
|
691
|
+
content
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
}
|
|
697
695
|
/**
|
|
698
696
|
* Return transient request messages whose oldest images are replaced until
|
|
699
697
|
* their accumulated base64 payload fits the configured bound. The selection
|
|
@@ -704,17 +702,41 @@ function replaceOldestImages(blocks, remaining) {
|
|
|
704
702
|
* @returns the original messages when they already fit, otherwise shallow message copies with replaced content trees.
|
|
705
703
|
*/
|
|
706
704
|
function offloadRequestImages(messages, maxRequestImageBytes) {
|
|
707
|
-
|
|
705
|
+
return offloadRequestImagesWithPolicy(messages, {
|
|
706
|
+
representation: "base64",
|
|
707
|
+
...maxRequestImageBytes === void 0 ? {} : { maxBytes: maxRequestImageBytes },
|
|
708
|
+
byteQuantum: 1
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Return a deterministic transient projection whose oldest images are replaced
|
|
713
|
+
* in whole count and byte quanta after a route budget is exceeded. The target
|
|
714
|
+
* depends only on complete durable history: at 129 one-megabyte images under
|
|
715
|
+
* a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
|
|
716
|
+
* 64 MiB remain; that removed prefix stays fixed until total history exceeds
|
|
717
|
+
* 192 MiB.
|
|
718
|
+
* @param messages - complete request history, oldest first.
|
|
719
|
+
* @param policy - route representation, budgets, and removal quanta.
|
|
720
|
+
* @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
|
|
721
|
+
*/
|
|
722
|
+
function offloadRequestImagesWithPolicy(messages, policy) {
|
|
708
723
|
const lengths = [];
|
|
709
|
-
for (const message of messages) collectImageLengths(message.content, lengths);
|
|
710
|
-
|
|
724
|
+
for (const message of messages) collectImageLengths(message.content, lengths, policy);
|
|
725
|
+
const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
|
|
726
|
+
const excessCount = policy.maxImages === void 0 ? 0 : Math.max(0, lengths.length - policy.maxImages);
|
|
727
|
+
const excessBytes = policy.maxBytes === void 0 ? 0 : Math.max(0, total - policy.maxBytes);
|
|
728
|
+
if (excessCount === 0 && excessBytes === 0) return messages;
|
|
729
|
+
const countQuantum = policy.countQuantum ?? 1;
|
|
730
|
+
const byteQuantum = policy.byteQuantum ?? 1;
|
|
731
|
+
const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
|
|
732
|
+
const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
|
|
711
733
|
let count = 0;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
734
|
+
let removedBytes = 0;
|
|
735
|
+
for (const imageBytes of lengths) {
|
|
736
|
+
if (count >= removeCount && (removeBytes === 0 || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes))) break;
|
|
737
|
+
removedBytes += imageBytes;
|
|
715
738
|
count += 1;
|
|
716
739
|
}
|
|
717
|
-
if (count === 0) return messages;
|
|
718
740
|
const remaining = { count };
|
|
719
741
|
return messages.map((message) => {
|
|
720
742
|
const content = replaceOldestImages(message.content, remaining);
|
|
@@ -725,6 +747,68 @@ function offloadRequestImages(messages, maxRequestImageBytes) {
|
|
|
725
747
|
});
|
|
726
748
|
}
|
|
727
749
|
//#endregion
|
|
750
|
+
//#region lib/types/attribution.js
|
|
751
|
+
/**
|
|
752
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
753
|
+
* adapters from drifting. See
|
|
754
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
755
|
+
*
|
|
756
|
+
* App-attribution vocabulary for provider requests.
|
|
757
|
+
* @module @deepseek-ai/dsh-llm/attribution
|
|
758
|
+
*/
|
|
759
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
760
|
+
/**
|
|
761
|
+
* The harness's own identity: the default every adapter sends. Deployments
|
|
762
|
+
* that need a white-label identity pass their own {@link AppIdentity} to
|
|
763
|
+
* {@link attributionHeaders} — omission falls back to this default; nothing
|
|
764
|
+
* can suppress attribution entirely.
|
|
765
|
+
*/
|
|
766
|
+
const APP_IDENTITY = {
|
|
767
|
+
product: "deepseek-harness",
|
|
768
|
+
version,
|
|
769
|
+
url: "https://github.com/deepseek-ai/deepseek-harness"
|
|
770
|
+
};
|
|
771
|
+
/**
|
|
772
|
+
* The standard `User-Agent` value: `product/version (+url)`. The
|
|
773
|
+
* parenthesized `+url` comment is the conventional self-identification form
|
|
774
|
+
* (RFC 9110 §10.1.5 product + comment syntax).
|
|
775
|
+
* @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
|
|
776
|
+
* @returns the ready-to-send header value.
|
|
777
|
+
*/
|
|
778
|
+
function userAgent(identity = APP_IDENTITY) {
|
|
779
|
+
return `${identity.product}/${identity.version} (+${identity.url})`;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Build the attribution headers an adapter must send on every provider
|
|
783
|
+
* request. Header names are lowercase (HTTP field names are case-insensitive
|
|
784
|
+
* on the wire).
|
|
785
|
+
* @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
|
|
786
|
+
* @returns headers to merge into the provider request (currently just `user-agent`).
|
|
787
|
+
*/
|
|
788
|
+
function attributionHeaders(identity = APP_IDENTITY) {
|
|
789
|
+
return { "user-agent": userAgent(identity) };
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region lib/types/never.js
|
|
793
|
+
/**
|
|
794
|
+
* Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
|
|
795
|
+
* new variant fails compilation at every required handler. Do not use it for declaration-merged
|
|
796
|
+
* unions such as session events or content blocks: handle known variants and explicitly fall
|
|
797
|
+
* through because plugins may add valid unknown cases.
|
|
798
|
+
* @module @deepseek-ai/dsh-llm/never
|
|
799
|
+
*/
|
|
800
|
+
/**
|
|
801
|
+
* Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
|
|
802
|
+
* a value that escaped its type throws with diagnostics at runtime.
|
|
803
|
+
* @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
|
|
804
|
+
* @param context - optional label (e.g. the switch site) prefixed into the throw message.
|
|
805
|
+
* @returns never — it always throws, with the offending value JSON-rendered in the message.
|
|
806
|
+
*/
|
|
807
|
+
function assertNever(value, context) {
|
|
808
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
809
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
728
812
|
//#region lib/types/assembler.js
|
|
729
813
|
/**
|
|
730
814
|
* Incremental chunk-to-message assembler. This is the single canonical assembly
|
|
@@ -1030,6 +1114,21 @@ var LlmAdapter = class {
|
|
|
1030
1114
|
name: model
|
|
1031
1115
|
});
|
|
1032
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Bind exact model metadata and the eventual request dispatch to one adapter generation.
|
|
1119
|
+
* Dynamic adapters override this so settings changes between preparation and
|
|
1120
|
+
* dispatch cannot combine one generation's capabilities with another's endpoint.
|
|
1121
|
+
* @param provider - registered provider route.
|
|
1122
|
+
* @param model - exact model id.
|
|
1123
|
+
* @param signal - cancellation for model resolution.
|
|
1124
|
+
* @returns model metadata and a one-generation stream entry point.
|
|
1125
|
+
*/
|
|
1126
|
+
async prepareCall(provider, model, signal) {
|
|
1127
|
+
return {
|
|
1128
|
+
model: await this.resolveModel(provider, model, signal),
|
|
1129
|
+
stream: (options) => this.stream(options)
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1033
1132
|
};
|
|
1034
1133
|
/**
|
|
1035
1134
|
* The abstract `llm` service: an adapter registry plus a streaming model-call
|
|
@@ -1299,8 +1398,12 @@ var LlmRuntime = class extends Service {
|
|
|
1299
1398
|
return this.resolveModelInfoFor(this.registration(provider), model, signal);
|
|
1300
1399
|
}
|
|
1301
1400
|
async resolveModelInfoFor(registration, model, signal) {
|
|
1401
|
+
const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
|
|
1402
|
+
return this.normalizeModelInfo(registration, model, resolved);
|
|
1403
|
+
}
|
|
1404
|
+
/** Validate and detach one adapter-returned exact model result. */
|
|
1405
|
+
normalizeModelInfo(registration, model, resolved) {
|
|
1302
1406
|
const provider = registration.provider.id;
|
|
1303
|
-
const resolved = await registration.adapter.resolveModel(provider, model, signal);
|
|
1304
1407
|
if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
1305
1408
|
const context = resolved.context;
|
|
1306
1409
|
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");
|
|
@@ -1353,6 +1456,10 @@ var LlmRuntime = class extends Service {
|
|
|
1353
1456
|
}
|
|
1354
1457
|
async resolveCallFor(registration, config, signal) {
|
|
1355
1458
|
const info = await this.resolveModelInfoFor(registration, config.model, signal);
|
|
1459
|
+
return this.resolveCallWithInfo(config, info);
|
|
1460
|
+
}
|
|
1461
|
+
/** Validate request controls against one already-bound exact model result. */
|
|
1462
|
+
resolveCallWithInfo(config, info) {
|
|
1356
1463
|
const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
|
|
1357
1464
|
...config,
|
|
1358
1465
|
maxTokens: info.defaultMaxTokens
|
|
@@ -1374,7 +1481,8 @@ var LlmRuntime = class extends Service {
|
|
|
1374
1481
|
}
|
|
1375
1482
|
return {
|
|
1376
1483
|
config: resolvedConfig,
|
|
1377
|
-
...info.context === void 0 ? {} : { context: info.context }
|
|
1484
|
+
...info.context === void 0 ? {} : { context: info.context },
|
|
1485
|
+
modelInfo: info
|
|
1378
1486
|
};
|
|
1379
1487
|
}
|
|
1380
1488
|
/**
|
|
@@ -1387,7 +1495,9 @@ var LlmRuntime = class extends Service {
|
|
|
1387
1495
|
*/
|
|
1388
1496
|
async prepareCall(config, signal) {
|
|
1389
1497
|
const registration = this.registration(config.provider);
|
|
1390
|
-
const
|
|
1498
|
+
const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
|
|
1499
|
+
const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
|
|
1500
|
+
const resolved = this.resolveCallWithInfo(config, modelInfo);
|
|
1391
1501
|
const resolvedConfig = deepFreeze(structuredClone(resolved.config));
|
|
1392
1502
|
const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
|
|
1393
1503
|
const adapterDefaults = deepFreeze({
|
|
@@ -1400,13 +1510,16 @@ var LlmRuntime = class extends Service {
|
|
|
1400
1510
|
retryPolicy: registration.retryPolicy,
|
|
1401
1511
|
adapterDefaults,
|
|
1402
1512
|
...context === void 0 ? {} : { context },
|
|
1513
|
+
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
1403
1514
|
stream: (options) => {
|
|
1404
1515
|
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
1405
1516
|
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
1406
1517
|
dispatched = true;
|
|
1407
1518
|
return this.streamWithRegistration(options, {
|
|
1408
1519
|
registration,
|
|
1409
|
-
config: resolvedConfig
|
|
1520
|
+
config: resolvedConfig,
|
|
1521
|
+
modelInfo,
|
|
1522
|
+
dispatch: (options) => adapterCall.stream(options)
|
|
1410
1523
|
});
|
|
1411
1524
|
}
|
|
1412
1525
|
});
|
|
@@ -1447,7 +1560,20 @@ var LlmRuntime = class extends Service {
|
|
|
1447
1560
|
let iterator;
|
|
1448
1561
|
try {
|
|
1449
1562
|
const registration = prepared?.registration ?? this.registration(options.provider);
|
|
1450
|
-
const
|
|
1563
|
+
const adapter = registration.adapter;
|
|
1564
|
+
let modelInfo;
|
|
1565
|
+
let resolvedConfig;
|
|
1566
|
+
let dispatch;
|
|
1567
|
+
if (prepared === void 0) {
|
|
1568
|
+
const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
|
|
1569
|
+
modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
|
|
1570
|
+
resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
|
|
1571
|
+
dispatch = (options) => adapterCall.stream(options);
|
|
1572
|
+
} else {
|
|
1573
|
+
modelInfo = prepared.modelInfo;
|
|
1574
|
+
resolvedConfig = prepared.config;
|
|
1575
|
+
dispatch = prepared.dispatch;
|
|
1576
|
+
}
|
|
1451
1577
|
if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
1452
1578
|
const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
|
|
1453
1579
|
...options,
|
|
@@ -1456,8 +1582,14 @@ var LlmRuntime = class extends Service {
|
|
|
1456
1582
|
...options,
|
|
1457
1583
|
...resolvedConfig
|
|
1458
1584
|
};
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1585
|
+
const projectedOptions = modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && resolvedOptions.messages.some((message) => contentHasImage(message.content)) ? Object.isFrozen(resolvedOptions) ? deepFreeze({
|
|
1586
|
+
...resolvedOptions,
|
|
1587
|
+
messages: projectImagesForTextModel(resolvedOptions.messages)
|
|
1588
|
+
}) : {
|
|
1589
|
+
...resolvedOptions,
|
|
1590
|
+
messages: projectImagesForTextModel(resolvedOptions.messages)
|
|
1591
|
+
} : resolvedOptions;
|
|
1592
|
+
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
1461
1593
|
} catch (error) {
|
|
1462
1594
|
yield adapterFailureChunk(error, options.signal);
|
|
1463
1595
|
return;
|
|
@@ -1523,4 +1655,4 @@ function adapterFailureChunk(error, signal) {
|
|
|
1523
1655
|
};
|
|
1524
1656
|
}
|
|
1525
1657
|
//#endregion
|
|
1526
|
-
export { APP_IDENTITY, BlockAssembler, CONTEXT_SUMMARY_MAX_CHARS, CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, HarnessError, INVALID_CREDENTIAL_CODE, LlmAdapter, LlmError, LlmRuntime, LlmRuntime as default, MessageId, OFFLOADED_IMAGE_TEXT, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, assertNever, assertUsableApiKey, attributionHeaders, boundContextSummary, callConfigEquals, contentHasImage, createAssistantMessage, createMessage, createToolResultMessage, createUserMessage, deepFreeze, errorChain, freezeMessage, isAgentLoopRequest, isContextWindowExceededError, isHarnessError, isQuotaExceededError, isTokenDelta, markAgentLoopRequest, normalizeApiKey, offloadRequestImages, resolveRetryPolicy, userAgent };
|
|
1658
|
+
export { APP_IDENTITY, BlockAssembler, CONTEXT_SUMMARY_MAX_CHARS, CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, HarnessError, INVALID_CREDENTIAL_CODE, LlmAdapter, LlmError, LlmRuntime, LlmRuntime as default, MessageId, OFFLOADED_IMAGE_TEXT, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, assertNever, assertUsableApiKey, attributionHeaders, boundContextSummary, callConfigEquals, contentHasImage, createAssistantMessage, createMessage, createToolResultMessage, createUserMessage, deepFreeze, errorChain, freezeMessage, isAgentLoopRequest, isContextWindowExceededError, isHarnessError, isQuotaExceededError, isTokenDelta, markAgentLoopRequest, normalizeApiKey, offloadRequestImages, offloadRequestImagesWithPolicy, projectImagesForTextModel, requestImageHandleText, resolveRetryPolicy, textOnlyImageText, userAgent };
|
package/lib/types/content.d.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */
|
|
2
2
|
import type { ContentBlock } from './types.ts';
|
|
3
3
|
import type { Message } from './message.ts';
|
|
4
|
+
import type { ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment';
|
|
4
5
|
/** Model-facing stand-in for an image removed to fit a provider request bound. */
|
|
5
6
|
export declare const OFFLOADED_IMAGE_TEXT = "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]";
|
|
7
|
+
/**
|
|
8
|
+
* Stable text shown to a model that cannot accept one durable image reference.
|
|
9
|
+
* @param ref - durable master reference omitted from the request.
|
|
10
|
+
* @returns deterministic text-only placeholder.
|
|
11
|
+
*/
|
|
12
|
+
export declare function textOnlyImageText(ref: ImageAttachmentRef): string;
|
|
13
|
+
/**
|
|
14
|
+
* Stable model-facing handle for one exact request image.
|
|
15
|
+
* @param version - exact request image shown beside the text.
|
|
16
|
+
* @returns attachment handle and request-image dimensions.
|
|
17
|
+
*/
|
|
18
|
+
export declare function requestImageHandleText(version: RequestImageAttachment): string;
|
|
6
19
|
/**
|
|
7
20
|
* True when typed model content contains an image block, walking nested
|
|
8
21
|
* tool-result content. This is the one recursive image walk shared by every
|
|
@@ -12,6 +25,27 @@ export declare const OFFLOADED_IMAGE_TEXT = "[image omitted to keep the request
|
|
|
12
25
|
* @returns whether any nested block is an image.
|
|
13
26
|
*/
|
|
14
27
|
export declare function contentHasImage(content: readonly ContentBlock[]): boolean;
|
|
28
|
+
/** Byte accounting and quantized removal policy for one request representation. */
|
|
29
|
+
export interface RequestImageOffloadPolicy {
|
|
30
|
+
/** Image count accepted by the route; omission leaves count unbounded. */
|
|
31
|
+
maxImages?: number;
|
|
32
|
+
/** Accumulated image bytes accepted by the route; omission leaves bytes unbounded. */
|
|
33
|
+
maxBytes?: number;
|
|
34
|
+
/** Number of excess images removed as one deterministic step. */
|
|
35
|
+
countQuantum?: number;
|
|
36
|
+
/** Number of excess bytes removed as one deterministic step. */
|
|
37
|
+
byteQuantum?: number;
|
|
38
|
+
/** Whether byte accounting uses raw file bytes or inline base64 length. */
|
|
39
|
+
representation: 'raw' | 'base64';
|
|
40
|
+
/** Resolve the encoded request-version length; omission uses master attachment bytes. */
|
|
41
|
+
byteLength?: (ref: ImageAttachmentRef) => number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Project durable image history into deterministic text for an exact text-only model.
|
|
45
|
+
* @param messages - complete request history.
|
|
46
|
+
* @returns the original list without images, otherwise shallow message copies with stable placeholders.
|
|
47
|
+
*/
|
|
48
|
+
export declare function projectImagesForTextModel(messages: readonly Message[]): readonly Message[];
|
|
15
49
|
/**
|
|
16
50
|
* Return transient request messages whose oldest images are replaced until
|
|
17
51
|
* their accumulated base64 payload fits the configured bound. The selection
|
|
@@ -22,4 +56,16 @@ export declare function contentHasImage(content: readonly ContentBlock[]): boole
|
|
|
22
56
|
* @returns the original messages when they already fit, otherwise shallow message copies with replaced content trees.
|
|
23
57
|
*/
|
|
24
58
|
export declare function offloadRequestImages(messages: readonly Message[], maxRequestImageBytes: number | undefined): readonly Message[];
|
|
59
|
+
/**
|
|
60
|
+
* Return a deterministic transient projection whose oldest images are replaced
|
|
61
|
+
* in whole count and byte quanta after a route budget is exceeded. The target
|
|
62
|
+
* depends only on complete durable history: at 129 one-megabyte images under
|
|
63
|
+
* a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
|
|
64
|
+
* 64 MiB remain; that removed prefix stays fixed until total history exceeds
|
|
65
|
+
* 192 MiB.
|
|
66
|
+
* @param messages - complete request history, oldest first.
|
|
67
|
+
* @param policy - route representation, budgets, and removal quanta.
|
|
68
|
+
* @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
|
|
69
|
+
*/
|
|
70
|
+
export declare function offloadRequestImagesWithPolicy(messages: readonly Message[], policy: RequestImageOffloadPolicy): readonly Message[];
|
|
25
71
|
//# sourceMappingURL=content.d.ts.map
|
package/lib/types/content.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
/** Content-block structure helpers. @module @deepseek-ai/dsh-llm/content */
|
|
2
2
|
/** Model-facing stand-in for an image removed to fit a provider request bound. */
|
|
3
3
|
export const OFFLOADED_IMAGE_TEXT = '[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]';
|
|
4
|
+
/**
|
|
5
|
+
* Stable text shown to a model that cannot accept one durable image reference.
|
|
6
|
+
* @param ref - durable master reference omitted from the request.
|
|
7
|
+
* @returns deterministic text-only placeholder.
|
|
8
|
+
*/
|
|
9
|
+
export function textOnlyImageText(ref) {
|
|
10
|
+
const digest = String(ref.attachmentId).slice('sha256:'.length, 'sha256:'.length + 8);
|
|
11
|
+
return `[image omitted because this model accepts text only; attachment sha256:${digest}]`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Stable model-facing handle for one exact request image.
|
|
15
|
+
* @param version - exact request image shown beside the text.
|
|
16
|
+
* @returns attachment handle and request-image dimensions.
|
|
17
|
+
*/
|
|
18
|
+
export function requestImageHandleText(version) {
|
|
19
|
+
return `Image ${version.attachment.attachmentId}; request image ${version.width}x${version.height}px.`;
|
|
20
|
+
}
|
|
4
21
|
/**
|
|
5
22
|
* True when typed model content contains an image block, walking nested
|
|
6
23
|
* tool-result content. This is the one recursive image walk shared by every
|
|
@@ -17,14 +34,17 @@ export function contentHasImage(content) {
|
|
|
17
34
|
function base64Length(bytes) {
|
|
18
35
|
return Math.ceil(bytes / 3) * 4;
|
|
19
36
|
}
|
|
20
|
-
/** Collect
|
|
21
|
-
function collectImageLengths(blocks, lengths) {
|
|
37
|
+
/** Collect represented image lengths in request and nested-block order. */
|
|
38
|
+
function collectImageLengths(blocks, lengths, policy) {
|
|
22
39
|
for (const block of blocks) {
|
|
23
40
|
if (block.type === 'image') {
|
|
24
|
-
|
|
41
|
+
const bytes = policy.byteLength === undefined
|
|
42
|
+
? block.attachment.bytes
|
|
43
|
+
: policy.byteLength(block.attachment);
|
|
44
|
+
lengths.push(policy.representation === 'base64' ? base64Length(bytes) : bytes);
|
|
25
45
|
}
|
|
26
46
|
else if (block.type === 'tool-result') {
|
|
27
|
-
collectImageLengths(block.content, lengths);
|
|
47
|
+
collectImageLengths(block.content, lengths, policy);
|
|
28
48
|
}
|
|
29
49
|
}
|
|
30
50
|
}
|
|
@@ -50,6 +70,40 @@ function replaceOldestImages(blocks, remaining) {
|
|
|
50
70
|
}
|
|
51
71
|
return next ?? blocks;
|
|
52
72
|
}
|
|
73
|
+
/** Replace every image occurrence, including nested tool results, for a text-only model. */
|
|
74
|
+
function replaceImagesForTextModel(blocks) {
|
|
75
|
+
let next;
|
|
76
|
+
for (const [index, block] of blocks.entries()) {
|
|
77
|
+
if (block.type === 'image') {
|
|
78
|
+
next ??= blocks.slice(0, index);
|
|
79
|
+
next.push({ type: 'text', text: textOnlyImageText(block.attachment) });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (block.type === 'tool-result') {
|
|
83
|
+
const content = replaceImagesForTextModel(block.content);
|
|
84
|
+
if (content !== block.content) {
|
|
85
|
+
next ??= blocks.slice(0, index);
|
|
86
|
+
next.push({ ...block, content });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
next?.push(block);
|
|
91
|
+
}
|
|
92
|
+
return next ?? blocks;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Project durable image history into deterministic text for an exact text-only model.
|
|
96
|
+
* @param messages - complete request history.
|
|
97
|
+
* @returns the original list without images, otherwise shallow message copies with stable placeholders.
|
|
98
|
+
*/
|
|
99
|
+
export function projectImagesForTextModel(messages) {
|
|
100
|
+
if (!messages.some(message => contentHasImage(message.content)))
|
|
101
|
+
return messages;
|
|
102
|
+
return messages.map((message) => {
|
|
103
|
+
const content = replaceImagesForTextModel(message.content);
|
|
104
|
+
return content === message.content ? message : { ...message, content };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
53
107
|
/**
|
|
54
108
|
* Return transient request messages whose oldest images are replaced until
|
|
55
109
|
* their accumulated base64 payload fits the configured bound. The selection
|
|
@@ -60,21 +114,46 @@ function replaceOldestImages(blocks, remaining) {
|
|
|
60
114
|
* @returns the original messages when they already fit, otherwise shallow message copies with replaced content trees.
|
|
61
115
|
*/
|
|
62
116
|
export function offloadRequestImages(messages, maxRequestImageBytes) {
|
|
63
|
-
|
|
64
|
-
|
|
117
|
+
return offloadRequestImagesWithPolicy(messages, {
|
|
118
|
+
representation: 'base64',
|
|
119
|
+
...maxRequestImageBytes === undefined ? {} : { maxBytes: maxRequestImageBytes },
|
|
120
|
+
byteQuantum: 1,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Return a deterministic transient projection whose oldest images are replaced
|
|
125
|
+
* in whole count and byte quanta after a route budget is exceeded. The target
|
|
126
|
+
* depends only on complete durable history: at 129 one-megabyte images under
|
|
127
|
+
* a 128 MiB bound with a 64 MiB quantum, the oldest 65 images are removed so
|
|
128
|
+
* 64 MiB remain; that removed prefix stays fixed until total history exceeds
|
|
129
|
+
* 192 MiB.
|
|
130
|
+
* @param messages - complete request history, oldest first.
|
|
131
|
+
* @param policy - route representation, budgets, and removal quanta.
|
|
132
|
+
* @returns original messages below both bounds, otherwise shallow copies with deterministic placeholders.
|
|
133
|
+
*/
|
|
134
|
+
export function offloadRequestImagesWithPolicy(messages, policy) {
|
|
65
135
|
const lengths = [];
|
|
66
136
|
for (const message of messages)
|
|
67
|
-
collectImageLengths(message.content, lengths);
|
|
68
|
-
|
|
137
|
+
collectImageLengths(message.content, lengths, policy);
|
|
138
|
+
const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
|
|
139
|
+
const excessCount = policy.maxImages === undefined ? 0 : Math.max(0, lengths.length - policy.maxImages);
|
|
140
|
+
const excessBytes = policy.maxBytes === undefined ? 0 : Math.max(0, total - policy.maxBytes);
|
|
141
|
+
if (excessCount === 0 && excessBytes === 0)
|
|
142
|
+
return messages;
|
|
143
|
+
const countQuantum = policy.countQuantum ?? 1;
|
|
144
|
+
const byteQuantum = policy.byteQuantum ?? 1;
|
|
145
|
+
const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
|
|
146
|
+
const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
|
|
69
147
|
let count = 0;
|
|
70
|
-
|
|
71
|
-
|
|
148
|
+
let removedBytes = 0;
|
|
149
|
+
for (const imageBytes of lengths) {
|
|
150
|
+
const byteTargetMet = removeBytes === 0
|
|
151
|
+
|| (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes);
|
|
152
|
+
if (count >= removeCount && byteTargetMet)
|
|
72
153
|
break;
|
|
73
|
-
|
|
154
|
+
removedBytes += imageBytes;
|
|
74
155
|
count += 1;
|
|
75
156
|
}
|
|
76
|
-
if (count === 0)
|
|
77
|
-
return messages;
|
|
78
157
|
const remaining = { count };
|
|
79
158
|
return messages.map((message) => {
|
|
80
159
|
const content = replaceOldestImages(message.content, remaining);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module @deepseek-ai/dsh-llm
|
|
7
7
|
*/
|
|
8
8
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
9
|
-
import type { GenerateOptions, LlmConfigurableProvider, LlmDiscoveredModel, LlmFailure, LlmModelContext, LlmModelDiscoveryRequest, LlmModelInfo, LlmResolvedModelInfo, LlmProviderInfo, StreamChunk } from './types.ts';
|
|
9
|
+
import type { GenerateOptions, LlmConfigurableProvider, LlmDiscoveredModel, LlmFailure, LlmModelContext, LlmModelDiscoveryRequest, LlmModelInfo, LlmResolvedModelInfo, LlmProviderInfo, ModelModality, StreamChunk } from './types.ts';
|
|
10
10
|
import type { ResolvedRetryPolicy } from './retry-policy.ts';
|
|
11
11
|
import type { ProviderRequestId } from './brand.ts';
|
|
12
12
|
import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts';
|
|
@@ -93,6 +93,8 @@ export interface PreparedLlmCall {
|
|
|
93
93
|
readonly retryPolicy: ResolvedRetryPolicy;
|
|
94
94
|
/** Detached context metadata resolved with the registration-bound call. */
|
|
95
95
|
readonly context?: LlmModelContext;
|
|
96
|
+
/** Exact model modalities captured with the adapter dispatch generation. */
|
|
97
|
+
readonly inputModalities?: readonly ModelModality[];
|
|
96
98
|
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
|
97
99
|
readonly adapterDefaults: LlmCallConfigAdapterDefaults;
|
|
98
100
|
/**
|
|
@@ -104,6 +106,13 @@ export interface PreparedLlmCall {
|
|
|
104
106
|
*/
|
|
105
107
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
106
108
|
}
|
|
109
|
+
/** One adapter-owned model-resolution generation bound to its eventual stream call. */
|
|
110
|
+
export interface PreparedAdapterCall {
|
|
111
|
+
/** Exact model metadata from the same adapter generation as {@link stream}. */
|
|
112
|
+
readonly model: LlmResolvedModelInfo;
|
|
113
|
+
/** Dispatch through that generation without re-reading dynamic connection facts. */
|
|
114
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
115
|
+
}
|
|
107
116
|
/**
|
|
108
117
|
* Provider-wire adapter for the harness message and stream vocabulary. Register implementations
|
|
109
118
|
* with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
|
|
@@ -141,6 +150,16 @@ export declare abstract class LlmAdapter {
|
|
|
141
150
|
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
|
142
151
|
*/
|
|
143
152
|
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
153
|
+
/**
|
|
154
|
+
* Bind exact model metadata and the eventual request dispatch to one adapter generation.
|
|
155
|
+
* Dynamic adapters override this so settings changes between preparation and
|
|
156
|
+
* dispatch cannot combine one generation's capabilities with another's endpoint.
|
|
157
|
+
* @param provider - registered provider route.
|
|
158
|
+
* @param model - exact model id.
|
|
159
|
+
* @param signal - cancellation for model resolution.
|
|
160
|
+
* @returns model metadata and a one-generation stream entry point.
|
|
161
|
+
*/
|
|
162
|
+
prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
|
|
144
163
|
/**
|
|
145
164
|
* Stream one model call as raw chunks. The only required method.
|
|
146
165
|
* @param options - the fully-assembled request; implementations must honor `options.signal`.
|
|
@@ -293,6 +312,8 @@ export declare class LlmRuntime extends Service {
|
|
|
293
312
|
*/
|
|
294
313
|
resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
295
314
|
private resolveModelInfoFor;
|
|
315
|
+
/** Validate and detach one adapter-returned exact model result. */
|
|
316
|
+
private normalizeModelInfo;
|
|
296
317
|
/**
|
|
297
318
|
* Validate a conversation call config against its exact model capability and
|
|
298
319
|
* materialize adapter-configured defaults. Unsupported explicit efforts
|
|
@@ -305,6 +326,8 @@ export declare class LlmRuntime extends Service {
|
|
|
305
326
|
*/
|
|
306
327
|
resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>;
|
|
307
328
|
private resolveCallFor;
|
|
329
|
+
/** Validate request controls against one already-bound exact model result. */
|
|
330
|
+
private resolveCallWithInfo;
|
|
308
331
|
/**
|
|
309
332
|
* Resolve one call under its current adapter registration. The returned
|
|
310
333
|
* one-shot handle keeps that registration across header logging and dispatch,
|
package/lib/types/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { callConfigEquals, deepFreeze } from "./call-config.js";
|
|
|
12
12
|
import { HarnessError, INVALID_CREDENTIAL_CODE } from "./error.js";
|
|
13
13
|
import { normalizeLlmFailure } from "./adapter-failure.js";
|
|
14
14
|
import { normalizeApiKey } from "./api-key.js";
|
|
15
|
+
import { contentHasImage, projectImagesForTextModel } from "./content.js";
|
|
15
16
|
export * from "./attribution.js";
|
|
16
17
|
export * from "./brand.js";
|
|
17
18
|
export * from "./never.js";
|
|
@@ -140,6 +141,21 @@ export class LlmAdapter {
|
|
|
140
141
|
resolveModel(provider, model, _signal) {
|
|
141
142
|
return Promise.resolve({ provider, id: model, name: model });
|
|
142
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Bind exact model metadata and the eventual request dispatch to one adapter generation.
|
|
146
|
+
* Dynamic adapters override this so settings changes between preparation and
|
|
147
|
+
* dispatch cannot combine one generation's capabilities with another's endpoint.
|
|
148
|
+
* @param provider - registered provider route.
|
|
149
|
+
* @param model - exact model id.
|
|
150
|
+
* @param signal - cancellation for model resolution.
|
|
151
|
+
* @returns model metadata and a one-generation stream entry point.
|
|
152
|
+
*/
|
|
153
|
+
async prepareCall(provider, model, signal) {
|
|
154
|
+
return {
|
|
155
|
+
model: await this.resolveModel(provider, model, signal),
|
|
156
|
+
stream: options => this.stream(options),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
160
|
/**
|
|
145
161
|
* The abstract `llm` service: an adapter registry plus a streaming model-call
|
|
@@ -466,8 +482,12 @@ export class LlmRuntime extends Service {
|
|
|
466
482
|
return this.resolveModelInfoFor(this.registration(provider), model, signal);
|
|
467
483
|
}
|
|
468
484
|
async resolveModelInfoFor(registration, model, signal) {
|
|
485
|
+
const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
|
|
486
|
+
return this.normalizeModelInfo(registration, model, resolved);
|
|
487
|
+
}
|
|
488
|
+
/** Validate and detach one adapter-returned exact model result. */
|
|
489
|
+
normalizeModelInfo(registration, model, resolved) {
|
|
469
490
|
const provider = registration.provider.id;
|
|
470
|
-
const resolved = await registration.adapter.resolveModel(provider, model, signal);
|
|
471
491
|
if (typeof resolved.provider !== 'string'
|
|
472
492
|
|| resolved.provider !== provider
|
|
473
493
|
|| typeof resolved.id !== 'string'
|
|
@@ -547,6 +567,10 @@ export class LlmRuntime extends Service {
|
|
|
547
567
|
}
|
|
548
568
|
async resolveCallFor(registration, config, signal) {
|
|
549
569
|
const info = await this.resolveModelInfoFor(registration, config.model, signal);
|
|
570
|
+
return this.resolveCallWithInfo(config, info);
|
|
571
|
+
}
|
|
572
|
+
/** Validate request controls against one already-bound exact model result. */
|
|
573
|
+
resolveCallWithInfo(config, info) {
|
|
550
574
|
const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
|
|
551
575
|
? { ...config, maxTokens: info.defaultMaxTokens }
|
|
552
576
|
: config;
|
|
@@ -571,6 +595,7 @@ export class LlmRuntime extends Service {
|
|
|
571
595
|
return {
|
|
572
596
|
config: resolvedConfig,
|
|
573
597
|
...info.context === undefined ? {} : { context: info.context },
|
|
598
|
+
modelInfo: info,
|
|
574
599
|
};
|
|
575
600
|
}
|
|
576
601
|
/**
|
|
@@ -583,7 +608,9 @@ export class LlmRuntime extends Service {
|
|
|
583
608
|
*/
|
|
584
609
|
async prepareCall(config, signal) {
|
|
585
610
|
const registration = this.registration(config.provider);
|
|
586
|
-
const
|
|
611
|
+
const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
|
|
612
|
+
const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
|
|
613
|
+
const resolved = this.resolveCallWithInfo(config, modelInfo);
|
|
587
614
|
const resolvedConfig = deepFreeze(structuredClone(resolved.config));
|
|
588
615
|
const context = resolved.context === undefined
|
|
589
616
|
? undefined
|
|
@@ -602,6 +629,9 @@ export class LlmRuntime extends Service {
|
|
|
602
629
|
retryPolicy: registration.retryPolicy,
|
|
603
630
|
adapterDefaults,
|
|
604
631
|
...context === undefined ? {} : { context },
|
|
632
|
+
...modelInfo.inputModalities === undefined
|
|
633
|
+
? {}
|
|
634
|
+
: { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
605
635
|
stream: (options) => {
|
|
606
636
|
if (dispatched) {
|
|
607
637
|
throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
|
|
@@ -610,7 +640,12 @@ export class LlmRuntime extends Service {
|
|
|
610
640
|
throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
|
|
611
641
|
}
|
|
612
642
|
dispatched = true;
|
|
613
|
-
return this.streamWithRegistration(options, {
|
|
643
|
+
return this.streamWithRegistration(options, {
|
|
644
|
+
registration,
|
|
645
|
+
config: resolvedConfig,
|
|
646
|
+
modelInfo,
|
|
647
|
+
dispatch: options => adapterCall.stream(options),
|
|
648
|
+
});
|
|
614
649
|
},
|
|
615
650
|
});
|
|
616
651
|
}
|
|
@@ -647,9 +682,21 @@ export class LlmRuntime extends Service {
|
|
|
647
682
|
let iterator;
|
|
648
683
|
try {
|
|
649
684
|
const registration = prepared?.registration ?? this.registration(options.provider);
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
685
|
+
const adapter = registration.adapter;
|
|
686
|
+
let modelInfo;
|
|
687
|
+
let resolvedConfig;
|
|
688
|
+
let dispatch;
|
|
689
|
+
if (prepared === undefined) {
|
|
690
|
+
const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
|
|
691
|
+
modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
|
|
692
|
+
resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
|
|
693
|
+
dispatch = options => adapterCall.stream(options);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
modelInfo = prepared.modelInfo;
|
|
697
|
+
resolvedConfig = prepared.config;
|
|
698
|
+
dispatch = prepared.dispatch;
|
|
699
|
+
}
|
|
653
700
|
if (prepared !== undefined && !callConfigEquals(options, resolvedConfig)) {
|
|
654
701
|
throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
|
|
655
702
|
}
|
|
@@ -658,8 +705,14 @@ export class LlmRuntime extends Service {
|
|
|
658
705
|
: Object.isFrozen(options)
|
|
659
706
|
? deepFreeze({ ...options, ...resolvedConfig })
|
|
660
707
|
: { ...options, ...resolvedConfig };
|
|
661
|
-
const
|
|
662
|
-
|
|
708
|
+
const projectedOptions = modelInfo.inputModalities !== undefined
|
|
709
|
+
&& !modelInfo.inputModalities.includes('image')
|
|
710
|
+
&& resolvedOptions.messages.some(message => contentHasImage(message.content))
|
|
711
|
+
? Object.isFrozen(resolvedOptions)
|
|
712
|
+
? deepFreeze({ ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) })
|
|
713
|
+
: { ...resolvedOptions, messages: projectImagesForTextModel(resolvedOptions.messages) }
|
|
714
|
+
: resolvedOptions;
|
|
715
|
+
const stream = dispatch(this.forAdapter(projectedOptions, adapter));
|
|
663
716
|
iterator = stream[Symbol.asyncIterator]();
|
|
664
717
|
}
|
|
665
718
|
catch (error) {
|
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.
|
|
4
|
+
"version": "0.1.1-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -45,20 +45,20 @@
|
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
48
|
+
"@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-timeout": "^0.1.1-rc.2",
|
|
52
52
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/
|
|
58
|
+
"@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-timeout": "^0.1.1-rc.2",
|
|
60
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
+
"@deepseek-ai/dsh-attachment": "^0.1.1-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
|
|
63
63
|
}
|
|
64
64
|
}
|