dsh-router-core 0.3.21 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -63
- package/cordis.patch.yml +22 -0
- package/docs/ext.md +28 -5
- package/docs/pool-sticky-block.md +158 -0
- package/docs/suppliers.md +28 -5
- package/lib/client-registry.js +1300 -549
- package/lib/client.js +1300 -549
- package/lib/ext-test/index.js +47 -0
- package/lib/ext-test/locale/en.json +6 -0
- package/lib/ext-test/locale/zh.json +6 -0
- package/lib/ext-test/package.json +5 -0
- package/lib/http-state-C1vb3gW2.js +48 -0
- package/lib/index.js +1169 -254
- package/lib/suppliers/{nvidia.js → nvidia/index.js} +14 -6
- package/lib/suppliers/nvidia/locale/en.json +6 -0
- package/lib/suppliers/nvidia/locale/zh.json +6 -0
- package/lib/suppliers/nvidia/package.json +5 -0
- package/lib/suppliers/{opencode.js → opencode/index.js} +14 -6
- package/lib/suppliers/opencode/locale/en.json +6 -0
- package/lib/suppliers/opencode/locale/zh.json +6 -0
- package/lib/suppliers/opencode/package.json +5 -0
- package/lib/suppliers/{openrouter.js → openrouter/index.js} +14 -6
- package/lib/suppliers/openrouter/locale/en.json +6 -0
- package/lib/suppliers/openrouter/locale/zh.json +6 -0
- package/lib/suppliers/openrouter/package.json +5 -0
- package/lib/types/ext/contract.d.ts +9 -7
- package/lib/types/ext-test/index.d.ts +21 -0
- package/lib/types/ext-test/plugin.d.ts +23 -0
- package/lib/types/host-version.d.ts +15 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/llm/adapter.d.ts +35 -4
- package/lib/types/router/account-pool.d.ts +53 -18
- package/lib/types/router/index.d.ts +65 -15
- package/lib/types/router/prefix-affinity.d.ts +18 -1
- package/lib/types/router/types.d.ts +7 -4
- package/lib/types/router/usage-store.d.ts +12 -0
- package/lib/types/shared.d.ts +25 -21
- package/lib/types/supplier-config.d.ts +15 -3
- package/lib/types/suppliers/contract.d.ts +37 -4
- package/lib/types/suppliers/http-state.d.ts +23 -0
- package/lib/types/suppliers/loader.d.ts +6 -4
- package/lib/types/suppliers/nvidia/index.d.ts +18 -0
- package/lib/types/suppliers/nvidia/plugin.d.ts +13 -0
- package/lib/types/suppliers/opencode/index.d.ts +18 -0
- package/lib/types/suppliers/opencode/plugin.d.ts +7 -0
- package/lib/types/suppliers/openrouter/index.d.ts +18 -0
- package/lib/types/suppliers/openrouter/plugin.d.ts +11 -0
- package/lib/types/suppliers/probe.d.ts +160 -0
- package/lib/types/suppliers/registry.d.ts +3 -2
- package/package.json +50 -16
- package/src/client/CombosTab.tsx +8 -53
- package/src/client/ExtDetail.tsx +32 -196
- package/src/client/ExtTab.tsx +25 -42
- package/src/client/ExtTestPanel.tsx +392 -0
- package/src/client/LastHitDock.tsx +141 -0
- package/src/client/RouterComponentsSection.tsx +241 -0
- package/src/client/RouterView.tsx +12 -5
- package/src/client/StatsTab.tsx +8 -6
- package/src/client/SupplierDetail.tsx +2 -34
- package/src/client/comp-button-scope.test.ts +58 -0
- package/src/client/comp-panel-layout.test.ts +58 -0
- package/src/client/env.d.ts +38 -1
- package/src/client/ext-panels.test.ts +44 -0
- package/src/client/ext-panels.ts +49 -0
- package/src/client/index.tsx +86 -0
- package/src/client/last-hit-store.ts +89 -0
- package/src/client/model-choice.test.ts +41 -0
- package/src/client/model-choice.ts +37 -0
- package/src/client/model-hint-copy.test.ts +7 -2
- package/src/client/model-hint-copy.ts +2 -2
- package/src/client/router-components.test.ts +114 -0
- package/src/client/router-components.ts +104 -0
- package/src/client/router.css +2592 -1635
- package/src/client/supplier-visibility.test.ts +57 -0
- package/src/client/supplier-visibility.ts +63 -0
- package/src/ext/contract.ts +9 -7
- package/src/ext-test/ext-test.test.ts +60 -0
- package/src/ext-test/index.ts +35 -0
- package/src/ext-test/locale/en.json +6 -0
- package/src/ext-test/locale/zh.json +6 -0
- package/src/ext-test/plugin.ts +34 -0
- package/src/ext-test/row.json +5 -0
- package/src/host-version.test.ts +61 -0
- package/src/host-version.ts +101 -0
- package/src/index.ts +165 -68
- package/src/llm/adapter.test.ts +175 -4
- package/src/llm/adapter.ts +109 -16
- package/src/router/account-pool.test.ts +189 -83
- package/src/router/account-pool.ts +120 -31
- package/src/router/disconnect-leak.test.ts +7 -12
- package/src/router/first-byte-budget.test.ts +160 -0
- package/src/router/index.ts +276 -62
- package/src/router/models-cache.test.ts +54 -9
- package/src/router/precise-dispatch.test.ts +6 -6
- package/src/router/prefix-affinity.ts +22 -1
- package/src/router/supplier-switch.test.ts +220 -0
- package/src/router/types.ts +7 -5
- package/src/router/usage-store.ts +16 -0
- package/src/router/usage.test.ts +1 -1
- package/src/shared.ts +25 -24
- package/src/supplier-config.ts +33 -13
- package/src/suppliers/contract.ts +56 -4
- package/src/suppliers/loader.test.ts +38 -4
- package/src/suppliers/loader.ts +33 -18
- package/src/suppliers/nvidia/index.ts +27 -0
- package/src/suppliers/nvidia/locale/en.json +6 -0
- package/src/suppliers/nvidia/locale/zh.json +6 -0
- package/src/suppliers/nvidia/row.json +5 -0
- package/src/suppliers/opencode/index.ts +27 -0
- package/src/suppliers/opencode/locale/en.json +6 -0
- package/src/suppliers/opencode/locale/zh.json +6 -0
- package/src/suppliers/opencode/row.json +5 -0
- package/src/suppliers/openrouter/index.ts +27 -0
- package/src/suppliers/openrouter/locale/en.json +6 -0
- package/src/suppliers/openrouter/locale/zh.json +6 -0
- package/src/suppliers/openrouter/row.json +5 -0
- package/src/suppliers/probe.test.ts +266 -0
- package/src/suppliers/probe.ts +475 -0
- package/src/suppliers/registry.ts +103 -23
- package/src/types.d.ts +2 -1
package/src/llm/adapter.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* `image` 块序列化成 OpenAI 标准的 `image_url` base64 part,经 /v1 原样透传给
|
|
9
9
|
* 命中的上游供应商。最终能否看图取决于**命中的那个上游模型**——组合里是异构
|
|
10
10
|
* 供应商,可能在网关端声明了图片能力、某条上游却只收文本(此时由上游自行拒收)。
|
|
11
|
-
* 图片字节从 `ctx.attachments`
|
|
11
|
+
* 图片字节从 `ctx.attachments` 读取。读不到时按稳定占位文本降级(不让整条消息失败),
|
|
12
|
+
* 但**必须留痕**:降级原因走 adapter 的 `log` 出口打到宿主 logger。曾经这里是
|
|
13
|
+
* 一个空 `catch {}`,字段改名导致的读图失败静默了跨两个内测版本(issue #8)。
|
|
12
14
|
*/
|
|
13
15
|
import {
|
|
14
16
|
EMPTY_RESPONSE_CODE,
|
|
@@ -31,15 +33,58 @@ import { mergeUsage, normalizeUsage, toTokenUsage, type UsageTokens } from '../r
|
|
|
31
33
|
* 这一行为,不引入对 dsh-attachment 包编译期/运行期的依赖(它是宿主注入的
|
|
32
34
|
* devDependency)。本地声明与原包 `AttachmentStore.readImageRequest` 形状一致,
|
|
33
35
|
* 宿主注入真实 store 时天然满足。
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ **第二个参数是 `ImageRequestTarget`(`width` + `height` + `maxBytes`),
|
|
38
|
+
* 不是 0.1.5 时代的 `ImageRequestPolicy`(`maxPixels` + `maxBytes`)**。宿主从
|
|
39
|
+
* 0.1.6 起在 `validateTarget()` 里校验这三个字段,缺一个就抛
|
|
40
|
+
* `Image request width must be a positive integer`,读图整体失败(issue #8)。
|
|
41
|
+
* 这两个字段不是摆设:宿主的 `pipeline()` 按 **target 的长边**把图缩到该尺寸,
|
|
42
|
+
* 短边由它自己推 —— 所以这里传投影后的尺寸,就是「按预算给我这张图」。
|
|
34
43
|
*/
|
|
35
44
|
export interface RouterAttachmentStore {
|
|
36
45
|
readImageRequest(
|
|
37
46
|
ref: { attachmentId: string; mediaType: string; bytes: number; width: number; height: number },
|
|
38
|
-
|
|
47
|
+
target: { width: number; height: number; maxBytes: number },
|
|
39
48
|
signal?: AbortSignal,
|
|
40
49
|
): Promise<{ mediaType: string; data: Uint8Array }>
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/** 送上游的请求图的像素预算(宽 × 高),沿用 0.1.5 时代 `maxPixels` 的意图。 */
|
|
53
|
+
const MAX_REQUEST_PIXELS = 64e4
|
|
54
|
+
/** 送上游的请求图的字节上限。 */
|
|
55
|
+
const MAX_REQUEST_BYTES = 1024 * 1024
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 把源图尺寸投影到「总像素 ≤ maxPixels」的请求目标(等比、只缩不放)。
|
|
59
|
+
*
|
|
60
|
+
* 为什么需要它:宿主 0.1.6 起要的是 `ImageRequestTarget`(width/height/maxBytes),
|
|
61
|
+
* 而「别把一张 12MP 手机照原样塞进对话」这个意图原本靠 `maxPixels` 表达。
|
|
62
|
+
* 宿主自己**只按 target 的长边**缩放、短边由它推,所以我们要做的只是给出一组
|
|
63
|
+
* 落在预算内的正整数;短边取 floor 即可(它只参与校验与缓存键,不决定出图尺寸)。
|
|
64
|
+
*
|
|
65
|
+
* 没有直接复用官方的 `requestImageDimensions`(`@deepseek-ai/dsh-attachment` 有导出):
|
|
66
|
+
* 本文件刻意不依赖那个包(见 `RouterAttachmentStore` 的说明 —— 它是宿主注入的
|
|
67
|
+
* devDependency,引入它等于给插件加一条编译期/运行期依赖)。这 8 行是纯函数、
|
|
68
|
+
* 有测试钉住;真要换成官方实现时,语义等价。
|
|
69
|
+
*/
|
|
70
|
+
export function projectImageDimensions(
|
|
71
|
+
width: number,
|
|
72
|
+
height: number,
|
|
73
|
+
maxPixels: number,
|
|
74
|
+
): { width: number; height: number } {
|
|
75
|
+
const pixels = width * height
|
|
76
|
+
// 尺寸缺失/非法时不投影:让宿主自己的校验去报错,别在这里造一个假尺寸掩盖问题
|
|
77
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
78
|
+
return { width, height }
|
|
79
|
+
}
|
|
80
|
+
if (pixels <= maxPixels) return { width, height }
|
|
81
|
+
const scale = Math.sqrt(maxPixels / pixels)
|
|
82
|
+
return {
|
|
83
|
+
width: Math.max(1, Math.floor(width * scale)),
|
|
84
|
+
height: Math.max(1, Math.floor(height * scale)),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
/** 一张 image 块的 attachment ref 的最小形状(value 类型,够用即可)。 */
|
|
44
89
|
interface RouterImgLike {
|
|
45
90
|
attachmentId: string
|
|
@@ -77,6 +122,10 @@ export interface RouterAdapterSource {
|
|
|
77
122
|
* 图片:`image` 块被序列化成 OpenAI `content` 数组里的 `image_url` part(data URI
|
|
78
123
|
* base64)。图片字节经 `resolveAttachments` 读取;读不到时回退为稳定的占位文本,
|
|
79
124
|
* **绝不静默丢图**(丢图 = 模型看到一条没有图的消息却毫无提示)。
|
|
125
|
+
*
|
|
126
|
+
* 工具结果双模型兼容:0.1.5/0.1.6 里 tool result 是 user 消息里的 'tool-result' 块
|
|
127
|
+
* (旧路径);0.1.7+ 改成了独立 role:'tool' 消息、toolCallId 挂消息级(新分支)。
|
|
128
|
+
* 两者在 wire 上统一序列化为 role:'tool',产物一致。见主循环里的 role==='tool' 分支。
|
|
80
129
|
*/
|
|
81
130
|
/** tool-result 里图片挂到 user 消息时用的说明文字(与官方适配器一致)。 */
|
|
82
131
|
const TOOL_RESULT_IMAGE_TEXT = 'Tool result images'
|
|
@@ -92,7 +141,7 @@ const TOOL_RESULT_IMAGE_TEXT = 'Tool result images'
|
|
|
92
141
|
*/
|
|
93
142
|
const STREAM_IDLE_TIMEOUT_MS = 300_000
|
|
94
143
|
|
|
95
|
-
async function wireMessages(options: GenerateOptions, system: string | undefined, attachments: RouterAttachmentStore | undefined): Promise<Array<Record<string, unknown>>> {
|
|
144
|
+
async function wireMessages(options: GenerateOptions, system: string | undefined, attachments: RouterAttachmentStore | undefined, log?: (msg: string) => void): Promise<Array<Record<string, unknown>>> {
|
|
96
145
|
const out: Array<Record<string, unknown>> = []
|
|
97
146
|
/**
|
|
98
147
|
* 攒着「tool-result 里带出来的图片」,等这一串 tool 消息**发完**再合并成一条
|
|
@@ -139,6 +188,32 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
|
|
|
139
188
|
})
|
|
140
189
|
continue
|
|
141
190
|
}
|
|
191
|
+
// tool 结果的消息形态:**同一宿主内两种形态都合法**,按消息实际形状分派。
|
|
192
|
+
// 新形态:独立 role:'tool' 消息、toolCallId 挂消息级(0.1.7 loop-built);
|
|
193
|
+
// 旧形态:user 消息里的 'tool-result' 块(0.1.5/0.1.6,以及 0.1.7 下经
|
|
194
|
+
// session-format 迁移/重放的老会话历史——实测 0.1.7 仍会出现)。
|
|
195
|
+
// 所以**不能**用宿主版本号判断形态对错(0.1.7-alpha.2 也是 0.1.7,同样会出现
|
|
196
|
+
// 旧形态)——形状才是事实,按形状分派这一份 adapter 就同时吃两版。
|
|
197
|
+
const isToolRoleMessage = (message.role as string) === 'tool'
|
|
198
|
+
if (isToolRoleMessage) {
|
|
199
|
+
// tool 消息必须紧跟 assistant(tool_calls):中间插进任何 user 消息,上游都会判定
|
|
200
|
+
// tool_call 与 tool_result 失配 → codebuddy 直接 400 网关码 11148(bad_request
|
|
201
|
+
// 按核心设计不罚账号,面板上看不到异常,易误判成额度/风控)。
|
|
202
|
+
// 图片不能放进 tool 消息(实测模型会把两张不同图认成同一张),也不能插在两条 tool
|
|
203
|
+
// 之间(同样 400),所以先攒进 pending,等整串 tool 发完再补一条 user(图)。
|
|
204
|
+
const toolCallId = (message as { toolCallId?: unknown }).toolCallId
|
|
205
|
+
const parts = await contentParts(message.content, attachments, options.signal, log)
|
|
206
|
+
const imageParts = parts.filter((p) => p.type !== 'text')
|
|
207
|
+
const text = parts
|
|
208
|
+
.filter((p) => p.type === 'text')
|
|
209
|
+
.map((p) => (p as { text?: string }).text ?? '')
|
|
210
|
+
.join('')
|
|
211
|
+
// 空结果就发空串:不要替换成 '(no output)' 之类字面量——模型会以为工具真的打印了
|
|
212
|
+
// 那句话。上游接受 content:""(0.1.7 探测矩阵第 6 项确认)。
|
|
213
|
+
out.push({ role: 'tool', tool_call_id: String(toolCallId ?? ''), content: text })
|
|
214
|
+
pendingToolImages.push(...imageParts)
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
142
217
|
// user / tool-result
|
|
143
218
|
const toolResults = message.content.filter((b) => b.type === 'tool-result')
|
|
144
219
|
// 这条消息自己的(非 tool-result 的)user 部分:文本 + 图片。
|
|
@@ -146,6 +221,7 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
|
|
|
146
221
|
message.content.filter((b) => b.type !== 'tool-result'),
|
|
147
222
|
attachments,
|
|
148
223
|
options.signal,
|
|
224
|
+
log,
|
|
149
225
|
)
|
|
150
226
|
// 有实质 user 内容(或压根没有 tool 结果)就先发这条 user 消息;
|
|
151
227
|
// 只有 tool 结果的「纯工具」消息不发空 user,图片留到 pending 里合并。
|
|
@@ -159,7 +235,7 @@ async function wireMessages(options: GenerateOptions, system: string | undefined
|
|
|
159
235
|
// 11148「tool calls and tool results do not match」。
|
|
160
236
|
// 图片不能放进 tool 消息(实测模型会把两张不同的图认成同一张),
|
|
161
237
|
// 也不能插在两条 tool 之间(同样 400),所以先攒起来、发完整串再补一条 user。
|
|
162
|
-
const parts = await contentParts(result.content, attachments, options.signal)
|
|
238
|
+
const parts = await contentParts(result.content, attachments, options.signal, log)
|
|
163
239
|
const imageParts = parts.filter((p) => p.type !== 'text')
|
|
164
240
|
const text = parts.filter((p) => p.type === 'text').map((p) => (p as { text?: string }).text ?? '').join('')
|
|
165
241
|
// 空结果就发空串,不要替换成 '(no output)' 之类的字面量 —— 模型会
|
|
@@ -194,6 +270,7 @@ async function contentParts(
|
|
|
194
270
|
blocks: ReadonlyArray<{ type: string }>,
|
|
195
271
|
attachments: RouterAttachmentStore | undefined,
|
|
196
272
|
signal: AbortSignal | undefined,
|
|
273
|
+
log?: (msg: string) => void,
|
|
197
274
|
): Promise<Array<Record<string, unknown>>> {
|
|
198
275
|
const parts: Array<Record<string, unknown>> = []
|
|
199
276
|
for (const block of blocks) {
|
|
@@ -204,11 +281,11 @@ async function contentParts(
|
|
|
204
281
|
}
|
|
205
282
|
if (block.type === 'image') {
|
|
206
283
|
const ref = (block as unknown as { attachment: RouterImgLike }).attachment
|
|
207
|
-
parts.push(...(await imageParts(ref, attachments, signal)))
|
|
284
|
+
parts.push(...(await imageParts(ref, attachments, signal, log)))
|
|
208
285
|
continue
|
|
209
286
|
}
|
|
210
287
|
if (block.type === 'tool-result') {
|
|
211
|
-
parts.push(...(await contentParts((block as unknown as { content: ReadonlyArray<{ type: string }> }).content, attachments, signal)))
|
|
288
|
+
parts.push(...(await contentParts((block as unknown as { content: ReadonlyArray<{ type: string }> }).content, attachments, signal, log)))
|
|
212
289
|
}
|
|
213
290
|
// 其它块(reasoning/tool-call)不属于 user content,忽略
|
|
214
291
|
}
|
|
@@ -220,16 +297,23 @@ async function imageParts(
|
|
|
220
297
|
ref: { attachmentId: string; mediaType: string; bytes: number; width: number; height: number },
|
|
221
298
|
attachments: RouterAttachmentStore | undefined,
|
|
222
299
|
signal: AbortSignal | undefined,
|
|
300
|
+
log?: (msg: string) => void,
|
|
223
301
|
): Promise<Array<Record<string, unknown>>> {
|
|
224
|
-
if (attachments === undefined)
|
|
302
|
+
if (attachments === undefined) {
|
|
303
|
+
log?.('图片附件 store 不可用(ctx.attachments 未注入),该图降级为占位文本')
|
|
304
|
+
return [{ type: 'text', text: imagePlaceholder(ref.attachmentId) }]
|
|
305
|
+
}
|
|
306
|
+
const ref0 = { attachmentId: ref.attachmentId, mediaType: ref.mediaType, bytes: ref.bytes, width: ref.width, height: ref.height }
|
|
307
|
+
// 目标 = 源尺寸按 64e4 像素预算等比投影后的宽高 + 字节上限(0.1.6+ 的 target 形状)
|
|
308
|
+
const target = { ...projectImageDimensions(ref.width, ref.height, MAX_REQUEST_PIXELS), maxBytes: MAX_REQUEST_BYTES }
|
|
225
309
|
try {
|
|
226
|
-
const img = await attachments.readImageRequest(
|
|
227
|
-
{ attachmentId: ref.attachmentId, mediaType: ref.mediaType, bytes: ref.bytes, width: ref.width, height: ref.height },
|
|
228
|
-
{ maxPixels: 64e4, maxBytes: 1024 * 1024 },
|
|
229
|
-
signal,
|
|
230
|
-
)
|
|
310
|
+
const img = await attachments.readImageRequest(ref0, target, signal)
|
|
231
311
|
return [{ type: 'image_url', image_url: { url: `data:${img.mediaType};base64,${bytesToBase64(img.data)}` } }]
|
|
232
|
-
} catch {
|
|
312
|
+
} catch (err) {
|
|
313
|
+
// 曾经这里是空 catch:宿主把请求目标从 maxPixels 换成 width/height 之后读图
|
|
314
|
+
// 全失败,图被静默换成占位文本,跨两个内测版本没被发现(issue #8)。降级仍然
|
|
315
|
+
// 保留(不能让整条消息失败),但原因必须出口。
|
|
316
|
+
log?.(`读取图片 ${ref.attachmentId} 失败,已降级为占位文本:${(err as Error).message}`)
|
|
233
317
|
return [{ type: 'text', text: imagePlaceholder(ref.attachmentId) }]
|
|
234
318
|
}
|
|
235
319
|
}
|
|
@@ -250,8 +334,8 @@ function imagePlaceholder(attachmentId: string): string {
|
|
|
250
334
|
}
|
|
251
335
|
|
|
252
336
|
/** 组装 wire 请求体。图片序列化需要读 attachments,故为异步。 */
|
|
253
|
-
async function wireRequest(options: GenerateOptions, attachments: RouterAttachmentStore | undefined): Promise<Record<string, unknown>> {
|
|
254
|
-
const messages = await wireMessages(options, options.system, attachments)
|
|
337
|
+
async function wireRequest(options: GenerateOptions, attachments: RouterAttachmentStore | undefined, log?: (msg: string) => void): Promise<Record<string, unknown>> {
|
|
338
|
+
const messages = await wireMessages(options, options.system, attachments, log)
|
|
255
339
|
const body: Record<string, unknown> = {
|
|
256
340
|
model: options.model,
|
|
257
341
|
messages,
|
|
@@ -555,22 +639,28 @@ export class RouterAdapter extends LlmAdapter {
|
|
|
555
639
|
private readonly resolveAttachments: () => RouterAttachmentStore | undefined
|
|
556
640
|
/** 流空闲超时(ms):上游这么久不吐字节就判 TIMEOUT。 */
|
|
557
641
|
private readonly idleTimeoutMs: number
|
|
642
|
+
/** 降级/失败诊断的出口(宿主接 ctx.logger.warn)。缺省丢弃 —— 仅为测试可注入。 */
|
|
643
|
+
private readonly log: ((msg: string) => void) | undefined
|
|
558
644
|
|
|
559
645
|
/**
|
|
560
646
|
* @param idleTimeoutMs 流空闲超时;缺省用生产值(300s,对齐 pi-ai)。
|
|
561
647
|
* 可注入只为测试能压到几百毫秒,生产不传。
|
|
648
|
+
* @param log 降级诊断出口。图片读不到时会降级成占位文本,原因走这里 ——
|
|
649
|
+
* 曾经那条路径是个空 `catch`,出问题跨版本都没人发现(issue #8)。
|
|
562
650
|
*/
|
|
563
651
|
constructor(
|
|
564
652
|
baseURL: string,
|
|
565
653
|
source: RouterAdapterSource,
|
|
566
654
|
resolveAttachments?: () => RouterAttachmentStore | undefined,
|
|
567
655
|
idleTimeoutMs: number = STREAM_IDLE_TIMEOUT_MS,
|
|
656
|
+
log?: (msg: string) => void,
|
|
568
657
|
) {
|
|
569
658
|
super()
|
|
570
659
|
this.baseURL = baseURL
|
|
571
660
|
this.source = source
|
|
572
661
|
this.resolveAttachments = resolveAttachments ?? (() => undefined)
|
|
573
662
|
this.idleTimeoutMs = idleTimeoutMs
|
|
663
|
+
this.log = log
|
|
574
664
|
}
|
|
575
665
|
|
|
576
666
|
providerInfo(provider: string): { id: string; name: string } {
|
|
@@ -617,7 +707,7 @@ export class RouterAdapter extends LlmAdapter {
|
|
|
617
707
|
}
|
|
618
708
|
|
|
619
709
|
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
620
|
-
const body = await wireRequest(options, this.resolveAttachments())
|
|
710
|
+
const body = await wireRequest(options, this.resolveAttachments(), this.log)
|
|
621
711
|
const controller = new AbortController()
|
|
622
712
|
const onAbort = (): void => controller.abort()
|
|
623
713
|
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
@@ -632,6 +722,9 @@ export class RouterAdapter extends LlmAdapter {
|
|
|
632
722
|
...attributionHeaders(),
|
|
633
723
|
'content-type': 'application/json',
|
|
634
724
|
accept: 'text/event-stream',
|
|
725
|
+
// 宿主盖的会话身份转成内部头:核心据此把用量按会话归因,
|
|
726
|
+
// 供「最近命中」徽章按当前会话显示。宿主没给就不带(外部调用)。
|
|
727
|
+
...options.sessionId === undefined ? {} : { 'x-dsh-router-session': String(options.sessionId) },
|
|
635
728
|
},
|
|
636
729
|
body: JSON.stringify(body),
|
|
637
730
|
signal: controller.signal,
|