dsh-router-core 0.3.18 → 0.3.20

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.
@@ -140,6 +140,7 @@ async function aggregateSSE(stream: ReadableStream<Uint8Array>): Promise<string>
140
140
 
141
141
  const reader = stream.getReader()
142
142
  let buf = ''
143
+ try {
143
144
  for (;;) {
144
145
  const { done, value } = await reader.read()
145
146
  if (done) break
@@ -189,6 +190,12 @@ async function aggregateSSE(stream: ReadableStream<Uint8Array>): Promise<string>
189
190
  }
190
191
  }
191
192
  }
193
+ } finally {
194
+ // 无论正常读完还是中途抛错,都要**解绑** reader:调用方(writeChatResult)
195
+ // 在失败路径里要对同一流 cancel(),而流被锁住时 cancel 会抛
196
+ // `Invalid state: ReadableStream is locked` —— 那样这次的连接就漏了。
197
+ try { reader.releaseLock() } catch { /* 有待定 read 时会抛,忽略 */ }
198
+ }
192
199
 
193
200
  const toolCalls = [...calls.entries()]
194
201
  .sort((a, b) => a[0] - b[0])
@@ -242,9 +249,11 @@ function normalizeSSEStream(stream: ReadableStream<Uint8Array>): {
242
249
  const dec = new TextDecoder()
243
250
  let buf = ''
244
251
  let sawDone = false
252
+ /** 上游 reader:cancel 时要靠它把取消传下去(见下方 cancel 分支)。 */
253
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
245
254
  const out = new ReadableStream<Uint8Array>({
246
255
  start: async (ctrl) => {
247
- const reader = stream.getReader()
256
+ reader = stream.getReader()
248
257
  try {
249
258
  for (;;) {
250
259
  const { done, value } = await reader.read()
@@ -263,11 +272,21 @@ function normalizeSSEStream(stream: ReadableStream<Uint8Array>): {
263
272
  if (isDoneFrame(buf)) sawDone = true
264
273
  ctrl.enqueue(enc.encode(fixFrame(buf)))
265
274
  }
275
+ } catch {
276
+ // 消费方取消(客户端断开)会让挂起的 read 抛错/返回 done —— 静默收尾
266
277
  } finally {
267
- reader.releaseLock()
278
+ try { reader.releaseLock() } catch { /* 有待定 read 时会抛,忽略 */ }
268
279
  }
269
- ctrl.close()
280
+ try { ctrl.close() } catch { /* 已取消 */ }
270
281
  },
282
+ /**
283
+ * **取消必须往下传**:ReadableStream 的取消不会自动穿过 wrapper。
284
+ * 不实现这个分支,客户端断开的取消就停在这里,上游 fetch body 永不 cancel
285
+ * ——那条 TCP 连接和 in-flight 请求原样挂着(长跑一天就会累积到耗尽)。
286
+ * 用 reader.cancel() 而不是 stream.cancel():流已被 getReader() 锁住,
287
+ * 直接 cancel 会抛 `Invalid state: ReadableStream is locked`。
288
+ */
289
+ cancel: (reason) => reader?.cancel(reason),
271
290
  })
272
291
  return { stream: out, sawDone: () => sawDone }
273
292
  }
@@ -338,7 +357,12 @@ async function writeChatResult(res: ServerResponse, r: ChatOnceResult, wantsStre
338
357
  try {
339
358
  body = await aggregateSSE(r.stream)
340
359
  } catch {
341
- return false // 聚合失败 = 一个字节都没写,调用方可以换号重试
360
+ // 聚合失败 = 一个字节都没写,调用方可以换号重试。
361
+ // **但底层流必须 cancel**:aggregateSSE 内部 getReader() 消费到一半抛错时,
362
+ // reader 只是 releaseLock(),传输层毫无察觉 —— 那条上游连接会一直挂着。
363
+ // 不 cancel 就退化成「每次聚合失败漏一条连接」(issue #6 的路径 1)。
364
+ await r.stream.cancel().catch(() => {})
365
+ return false
342
366
  }
343
367
  if (probe !== undefined) probe.tokens = usageFromJsonBody(body)
344
368
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
@@ -351,6 +375,18 @@ async function writeChatResult(res: ServerResponse, r: ChatOnceResult, wantsStre
351
375
  let wroteAny = false
352
376
  /** 响应已死(客户端断开/res 已销毁):等 drain 的写入要立刻放弃,别吊死。 */
353
377
  let dead = false
378
+ /**
379
+ * 唤醒「等 drain 的挂起 write」。客户端断开时必须调它:那时 'drain' 永不到来,
380
+ * 不唤醒就永远卡在 write 里,pipeTo 不 settle,finally 不执行,
381
+ * **上游流也就永不 cancel**(= 每次客户端放弃漏一条连接)。
382
+ */
383
+ let releaseWait: (() => void) | undefined
384
+ /**
385
+ * 中止 pipeTo 的信号源(客户端断开时 abort)。
386
+ * 单靠 `dead` 标志只能让**下一次** write 立刻返回,唤醒不了**已经挂起**的那次;
387
+ * abort 才能让 pipeTo 立刻收尾并走它的取消路径(进而 cancel 上游)。
388
+ */
389
+ const abort = new AbortController()
354
390
  const writeChunk = (chunk: Uint8Array): Promise<void> =>
355
391
  new Promise<void>((resolve) => {
356
392
  if (dead) { resolve(); return }
@@ -370,25 +406,44 @@ async function writeChatResult(res: ServerResponse, r: ChatOnceResult, wantsStre
370
406
  if (res.write(chunk)) resolve()
371
407
  else {
372
408
  // 背压:等 drain。但连接可能在这期间断掉 —— 那时 'drain' 永不到来,
373
- // 得靠 dead 标志(onError 里置位)兜住,否则这次请求会永远挂着。
409
+ // 得靠 release() 兜住(客户端断开时唤醒),否则这次请求会永远挂着。
374
410
  const onDrain = (): void => { res.removeListener('error', onDrain); resolve() }
375
411
  res.once('drain', onDrain)
412
+ /** 让这个挂起的 write 立刻收尾(客户端断开时调用)。 */
413
+ releaseWait = () => { res.removeListener('drain', onDrain); resolve() }
376
414
  }
377
415
  })
378
416
 
379
- // 流中途出错(客户端断开/上游断流)也要结束响应,否则连接悬挂
417
+ // 客户端断开 / 流中途出错都要停手,否则连接悬挂
380
418
  const onError = (): void => {
381
419
  dead = true
382
420
  res.destroy()
383
421
  }
384
422
  res.once('error', onError)
423
+ /**
424
+ * **客户端断开必须被当成取消信号**:只监听 'error' 是不够的 —— 对端关闭
425
+ * (pi-ai 的流空闲超时、用户取消、消费方提前退出)触发的是 'close',
426
+ * 不是 'error'。此时若不收手,pipeTo 会一直等着 drain(永不到来),
427
+ * finally 永不执行,上游 fetch body **永不 cancel** → 那条 TCP 连接和
428
+ * in-flight 请求原样挂着,长跑累积到耗尽("用久了就报 connection error")。
429
+ */
430
+ const onClose = (): void => {
431
+ if (res.writableEnded) return // 正常写完的 close,不是取消
432
+ dead = true
433
+ releaseWait?.()
434
+ abort.abort()
435
+ }
436
+ res.once('close', onClose)
385
437
  const norm = normalizeSSEStream(tapped?.stream ?? r.stream)
386
438
  try {
387
- await norm.stream.pipeTo(new WritableStream<Uint8Array>({ write: writeChunk }))
439
+ // signal 让 abort 时 pipeTo 立刻收尾(而不是等 drain);其收尾路径会
440
+ // cancel 源流,顺着 wrapper 的 cancel 一路传到上游 fetch body。
441
+ await norm.stream.pipeTo(new WritableStream<Uint8Array>({ write: writeChunk }), { signal: abort.signal })
388
442
  } catch {
389
- // 流断了:一个字节都没写的话,调用方还能换号重试——交给上层决定
443
+ // 断流/取消:一个字节都没写的话,调用方还能换号重试——交给上层决定
390
444
  } finally {
391
445
  res.removeListener('error', onError)
446
+ res.removeListener('close', onClose)
392
447
  // 断流也要给客户端一个终止帧。客户端(dsh-llm adapter)严格等
393
448
  // `[DONE]`,缺了就整轮判失败(`SSE payload stream ended without
394
449
  // [DONE]`),已经流式吐出去的内容全部作废。上游自己发了就不重复发
@@ -396,10 +451,11 @@ async function writeChatResult(res: ServerResponse, r: ChatOnceResult, wantsStre
396
451
  //
397
452
  // 只在**已提交响应**时补:一个字节都没写 = 还能换号重试,补了就等于
398
453
  // 把这次失败坐实成一个空响应,组合回退就没了。
454
+ // dead(客户端已走)时也不补:对端没了,写了也没人收。
399
455
  if (wroteAny && !norm.sawDone() && !dead && !res.writableEnded && !res.destroyed) {
400
456
  await writeChunk(DONE_FRAME)
401
457
  }
402
- if (wroteAny) res.end()
458
+ if (wroteAny && !dead) res.end()
403
459
  // 流走完才出得了 usage(上游多在最后一帧才发 usage)
404
460
  if (probe !== undefined && tapped !== undefined) {
405
461
  const got = await tapped.done()
@@ -280,9 +280,11 @@ export function tapStreamUsage(
280
280
  }
281
281
  }
282
282
 
283
+ /** 源 reader:cancel 时要靠它把取消传下去(见下方 cancel 分支)。 */
284
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
283
285
  const stream = new ReadableStream<Uint8Array>({
284
286
  start: async (ctrl) => {
285
- const reader = source.getReader()
287
+ reader = source.getReader()
286
288
  try {
287
289
  for (;;) {
288
290
  const { done, value } = await reader.read()
@@ -300,11 +302,19 @@ export function tapStreamUsage(
300
302
  ctrl.enqueue(value as Uint8Array)
301
303
  }
302
304
  if (buf !== '') parseFrame(buf)
305
+ } catch {
306
+ // 消费方取消会让挂起的 read 收尾 —— 静默退出
303
307
  } finally {
304
- reader.releaseLock()
308
+ try { reader.releaseLock() } catch { /* 有待定 read 时会抛,忽略 */ }
305
309
  }
306
- ctrl.close()
310
+ try { ctrl.close() } catch { /* 已取消 */ }
307
311
  },
312
+ /**
313
+ * 取消要往下传(同 normalizeSSEStream 的理由):不传 → 客户端断开时
314
+ * 上游 fetch body 永不 cancel,连接与 in-flight 请求一直挂着。
315
+ * 必须用 reader.cancel():流已被 getReader() 锁住,stream.cancel() 会抛。
316
+ */
317
+ cancel: (reason) => reader?.cancel(reason),
308
318
  })
309
319
 
310
320
  return {
package/src/shared.ts CHANGED
@@ -143,6 +143,8 @@ export interface RouterExtItem {
143
143
  id: string
144
144
  name: string
145
145
  description?: string
146
+ /** 卡片/详情图标 logo URL(可选)。 */
147
+ icon?: string
146
148
  /** 是否启用改写(面板开关)。 */
147
149
  enabled?: boolean
148
150
  /** 运行时是否就绪(如 RTK 二进制是否可用)。 */
@@ -158,6 +160,25 @@ export interface RouterExtResponse {
158
160
  enhancers?: RouterExtItem[]
159
161
  }
160
162
 
163
+ /** 一行 watch 进程(`/router/api/ext/watch/processes`)。 */
164
+ export interface RouterWatchProcess {
165
+ pid: number
166
+ ppid: number
167
+ /** %cpu 快照 */ cpu: number
168
+ /** %mem 快照 */ mem: number
169
+ command: string
170
+ /** 命中的已记录 agent 命令。 */
171
+ match: string
172
+ recordedAt: number
173
+ }
174
+
175
+ /** `/router/api/ext/{id}/processes` response。 */
176
+ export interface RouterWatchResponse {
177
+ ok: boolean
178
+ error?: string
179
+ processes?: RouterWatchProcess[]
180
+ }
181
+
161
182
  /** 供应商模型(含启用状态)。 */
162
183
  export interface RouterSupplierModel {
163
184
  id: string
@@ -0,0 +1,176 @@
1
+ /**
2
+ * opencode(Zen 免费档)握手契约测试 —— 锁住 2026-09-22 实测的那组硬门。
3
+ *
4
+ * 为什么值得固化:这四条**都不是可选优化,缺一即 403**
5
+ * `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
6
+ * 它们全是「看不出来」的那种约定——代码删掉一行 headers、或把 UA 改回
7
+ * `opencode`,单测本该立刻变红,否则下次只能在真机对话里发现整个供应商死了。
8
+ *
9
+ * 实测依据(2026-09-22,逐项消融验证):
10
+ * - UA 必须带版本且 ≥1.18:裸 `opencode`→403;`opencode/1.17.x`→426;1.18.0 起 200
11
+ * - 必须同时有 `x-opencode-session`/`x-opencode-request`,且形状是
12
+ * `ses_<12hex><14base62>` / `msg_<同形状>`——格式错也 403
13
+ * - tools 里必须**同时**有 bash 和 read,只给一个也 403
14
+ * - 非流式(`stream:false`)即使三样齐全也 403 → 必须恒发 stream:true
15
+ *
16
+ * 用 node --test 跑(Node 原生剥 TS 类型):
17
+ * node --test src/suppliers/opencode/plugin.test.ts
18
+ */
19
+ import test from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+ import factory from './plugin.ts'
22
+ import { SupplierConfigStore } from '../../supplier-config.ts'
23
+ import type { CredentialStore } from '../../credential-store.ts'
24
+ import type { SupplierEnv, SupplierModule } from '../contract.ts'
25
+ import type { ChatRequest } from '../../router/types.ts'
26
+
27
+ /** 上游认的 session/request id 形状:前缀 + 12 位小写 hex + 14 位 base62。 */
28
+ const SES_RE = /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/
29
+ const MSG_RE = /^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/
30
+
31
+ interface Captured {
32
+ url: string
33
+ headers: Record<string, string>
34
+ body: Record<string, unknown>
35
+ }
36
+
37
+ /** 打桩 globalThis.fetch,接住一次请求(上游返回空 SSE 流即可)。 */
38
+ function stubFetch(): { captured: Captured[]; restore: () => void } {
39
+ const captured: Captured[] = []
40
+ const real = globalThis.fetch
41
+ globalThis.fetch = (async (url: string, init?: { headers?: Record<string, string>; body?: string }) => {
42
+ captured.push({
43
+ url: String(url),
44
+ headers: init?.headers ?? {},
45
+ body: init?.body !== undefined ? (JSON.parse(init.body) as Record<string, unknown>) : {},
46
+ })
47
+ return new Response(new ReadableStream<Uint8Array>({ start: (c) => c.close() }), { status: 200 })
48
+ }) as unknown as typeof fetch
49
+ return { captured, restore: () => { globalThis.fetch = real } }
50
+ }
51
+
52
+ /** 造一个最小 env(store 空文件路径 = 纯内存,credentials 用不着的空壳)。 */
53
+ function makePlugin(): SupplierModule {
54
+ const store = new SupplierConfigStore('')
55
+ const env: SupplierEnv = {
56
+ dataDir: '/tmp',
57
+ log: () => {},
58
+ store,
59
+ credentials: {} as CredentialStore,
60
+ }
61
+ return factory(env)
62
+ }
63
+
64
+ function request(over: Partial<ChatRequest> = {}): ChatRequest {
65
+ return {
66
+ model: 'big-pickle',
67
+ stream: true,
68
+ rawBody: JSON.stringify({ model: 'big-pickle', messages: [{ role: 'user', content: 'hi' }] }),
69
+ ...over,
70
+ }
71
+ }
72
+
73
+ test('chatOnce 必须带版本化 UA + 合法形状的 session/request 头', async () => {
74
+ const stub = stubFetch()
75
+ try {
76
+ const r = await makePlugin().chatOnce('', 'auto', request())
77
+ assert.equal(r.ok, true, '握手齐全时应当成功')
78
+ const h = stub.captured[0]!.headers
79
+
80
+ // UA 必须带版本号(裸 'opencode' 会被上游按非 CLI 拒掉)
81
+ assert.match(h['User-Agent'] ?? '', /^opencode\/\d+\.\d+/, 'UA 必须带 opencode/ 版本号')
82
+
83
+ // session/request 形状不对上游是**拒绝**而不是忽略 —— 形状必须严格
84
+ assert.match(h['x-opencode-session'] ?? '', SES_RE, 'x-opencode-session 形状不对会被 403')
85
+ assert.match(h['x-opencode-request'] ?? '', MSG_RE, 'x-opencode-request 形状不对会被 403')
86
+ } finally {
87
+ stub.restore()
88
+ }
89
+ })
90
+
91
+ test('请求体恒为流式,且 tools 同时含 bash 与 read', async () => {
92
+ const stub = stubFetch()
93
+ try {
94
+ // 调用方明确要非流式 —— 上游免费档对非流式一律 403,插件必须翻成流式
95
+ await makePlugin().chatOnce('', 'auto', request({ stream: false }))
96
+ const body = stub.captured[0]!.body
97
+ assert.equal(body.stream, true, '免费档只支持流式,非流式必须翻成 stream:true')
98
+
99
+ const names = (body.tools as Array<{ function?: { name?: string } }>).map((t) => t.function?.name)
100
+ assert.ok(names.includes('bash'), 'tools 缺 bash 会被 403')
101
+ assert.ok(names.includes('read'), 'tools 缺 read 会被 403')
102
+ } finally {
103
+ stub.restore()
104
+ }
105
+ })
106
+
107
+ test('调用方自带的同名工具不被占位工具覆盖(bash/read 只补缺)', async () => {
108
+ const stub = stubFetch()
109
+ try {
110
+ const realBash = {
111
+ type: 'function',
112
+ function: { name: 'bash', description: 'REAL BASH', parameters: { type: 'object', properties: { command: { type: 'string' } } } },
113
+ }
114
+ await makePlugin().chatOnce('', 'auto', request({
115
+ rawBody: JSON.stringify({ model: 'big-pickle', messages: [], stream: true, tools: [realBash] }),
116
+ }))
117
+ const tools = stub.captured[0]!.body.tools as Array<{ function: { name: string; description?: string } }>
118
+
119
+ const bash = tools.filter((t) => t.function.name === 'bash')
120
+ assert.equal(bash.length, 1, 'bash 不能被注入成两条(否则真实工具签名被挤掉)')
121
+ assert.equal(bash[0]!.function.description, 'REAL BASH', '调用方自己的定义必须原样保留')
122
+
123
+ // read 缺失 → 补上;bash 已有 → 不重复
124
+ assert.ok(tools.some((t) => t.function.name === 'read'), '缺的 read 要补上')
125
+ } finally {
126
+ stub.restore()
127
+ }
128
+ })
129
+
130
+ test("effort='off' 必须删字段而不是下发(上游没有这个值,会 400)", async () => {
131
+ const stub = stubFetch()
132
+ try {
133
+ // 关键:rawBody 里**本来就带** reasoning_effort/reasoning_summary。
134
+ // 不种这两个字段的话,「删」与「什么都不做」结果一样,闸门形同虚设
135
+ // (实测过:把 else-if 的 'off' 分支去掉,测试照样全绿)。
136
+ await makePlugin().chatOnce('', 'off', request({
137
+ rawBody: JSON.stringify({
138
+ model: 'big-pickle',
139
+ messages: [],
140
+ stream: true,
141
+ reasoning_effort: 'high',
142
+ reasoning_summary: 'auto',
143
+ }),
144
+ }))
145
+ const body = stub.captured[0]!.body
146
+ assert.equal('reasoning_effort' in body, false, "off 要删字段:上游收到 'off' 直接 400 invalid_request_error")
147
+ assert.equal('reasoning_summary' in body, false, 'reasoning_summary 一并删掉')
148
+ } finally {
149
+ stub.restore()
150
+ }
151
+ })
152
+
153
+ test("effort='auto' 不动调用方自己的 reasoning_effort(不误删)", async () => {
154
+ const stub = stubFetch()
155
+ try {
156
+ await makePlugin().chatOnce('', 'auto', request({
157
+ rawBody: JSON.stringify({ model: 'big-pickle', messages: [], stream: true, reasoning_effort: 'high' }),
158
+ }))
159
+ const body = stub.captured[0]!.body
160
+ assert.equal(body.reasoning_effort, 'high', 'auto = 不显式下发,调用方原值必须留着')
161
+ } finally {
162
+ stub.restore()
163
+ }
164
+ })
165
+
166
+ test('非 free 模型报 no_such_model(不记账、不冷号)', async () => {
167
+ const stub = stubFetch()
168
+ try {
169
+ const r = await makePlugin().chatOnce('', 'auto', request({ model: 'gpt-5.5' }))
170
+ assert.equal(r.ok, false)
171
+ assert.equal(r.ok === false ? r.state : '', 'no_such_model', '模型不属于本供应商 → 核心换供应商而不是冷号')
172
+ assert.equal(stub.captured.length, 0, '不属于自己的模型不该打上游')
173
+ } finally {
174
+ stub.restore()
175
+ }
176
+ })
@@ -1,20 +1,24 @@
1
1
  /**
2
2
  * opencode 供应商插件 —— 参考 9Router(open-sse) 的 opencode free 实现。
3
3
  *
4
- * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,公开免费,无鉴权)
5
- * - chat: POST /zen/v1/chat/completions(SSE 流式 / 非流式)
4
+ * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,免费档)
5
+ * - chat: POST /zen/v1/chat/completions(**只能流式**)
6
6
  * - models: GET /zen/v1/models(透传,只取免费模型)
7
7
  *
8
- * noAuth 直连:无账号/凭证/连接池/签到 —— 通用层对它是空的,
9
- * 面板只显示模型卡片(连接池、签到由能力检测自动隐藏)。
8
+ * 「免费」不等于「无鉴权」:Zen 免费档仍要求**模拟真 CLI 的握手**,否则一律
9
+ * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`
10
+ * (2026-09-22 实测)。三个条件缺一即 403,见下方 `UA` / `sessionId` / `decoyTools`。
11
+ * 免费档也没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
12
+ * (连接池、签到由能力检测自动隐藏)。
10
13
  */
14
+ import { randomBytes } from 'node:crypto'
11
15
  import type { ChatRequest, ModelInfo } from '../../router/types.ts'
12
16
  import type { ChatOnceResult, SupplierEnv, SupplierModule, SupplierStatusNow } from '../contract.ts'
13
17
  import { stateFromHttpStatus } from '../http-state.ts'
14
18
 
15
19
 
16
20
  export const id = 'opencode'
17
- export const name = 'OpenCode Free'
21
+ export const name = 'OpenCode'
18
22
  export const priority = 0
19
23
  /** 面板图标。 */
20
24
  export const icon = 'data:image/png;base64,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'
@@ -22,11 +26,86 @@ export const icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA
22
26
  const BASE = 'https://opencode.ai/zen/v1'
23
27
  const MODELS_URL = `${BASE}/models`
24
28
  const CHAT_URL = `${BASE}/chat/completions`
25
- const UA = 'opencode'
29
+
30
+ /**
31
+ * User-Agent 必须带**版本号**且 ≥ 1.18。
32
+ *
33
+ * 裸 `opencode` → 403 FreeTierError;`opencode/1.17.x` → **426 UpgradeRequired**
34
+ * (上游明确要求升级客户端);`opencode/1.18.0` 起通过。别去掉版本号,
35
+ * 也别写死成能过的最高版本——取一个已实测通过的稳定值即可。
36
+ */
37
+ const UA = 'opencode/1.18.31'
38
+
39
+ /**
40
+ * 免费档按 session 记账(每次生成一个新的即可——见 `chatOnce` 的实测说明),
41
+ * `x-opencode-session` / `x-opencode-request` **必须**是上游认的形状,格式不对
42
+ * 同样 403(不是忽略,是拒绝):
43
+ * ses_<12 位小写 hex><14 位 base62>、msg_<同形状>
44
+ */
45
+ function newSessionId(prefix: 'ses_' | 'msg_'): string {
46
+ const B62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
47
+ const bytes = randomBytes(20)
48
+ let time = ''
49
+ for (let i = 0; i < 6; i++) time += bytes[i]!.toString(16).padStart(2, '0')
50
+ let rand = ''
51
+ for (let i = 6; i < 20; i++) rand += B62[bytes[i]! % 62] ?? ''
52
+ return `${prefix}${time}${rand}`
53
+ }
54
+
55
+ /**
56
+ * 每次请求一对新的 session/request id。
57
+ *
58
+ * 不做会话池的实测依据(2026-09-22):每次新 session 与固定 session 各打 40 次
59
+ * **全部 200**,且带大前缀时上游缓存命中数完全相同(都 2560 cached)——
60
+ * 9router 的 `stableSessionId` 在这里没有可测收益,属未经验证的复杂度。
61
+ */
62
+ function sessionHeaders(): Record<string, string> {
63
+ return {
64
+ 'x-opencode-session': newSessionId('ses_'),
65
+ 'x-opencode-request': newSessionId('msg_'),
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 免费档要求 tools 里**同时**存在 `bash` 和 `read`,否则 403。
71
+ *
72
+ * 为什么是「注入」而不是「覆盖」:上游只是**校验这两个名字在不在**,真正的
73
+ * 工具照常工作(实测真实工具在前、占位在后,混合下发 200)。所以调用方已经
74
+ * 带了同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的定义,
75
+ * 否则会把真实工具签名糊掉。
76
+ */
77
+ const DECOY_TOOLS: ReadonlyArray<{ name: string; description: string }> = [
78
+ { name: 'bash', description: 'This tool is currently unavailable and must not be used.' },
79
+ { name: 'read', description: 'This tool is currently unavailable and must not be used.' },
80
+ ]
26
81
 
27
82
  /** 免费模型识别:id 以 -free 结尾,或已知不带后缀的免费模型。 */
28
83
  const KNOWN_FREE = new Set(['big-pickle'])
29
84
 
85
+ /**
86
+ * 补上上游免费档要求的占位工具(幂等:已存在同名工具就跳过)。
87
+ *
88
+ * 只认 `function.name`(OpenAI chat 形态)——Zen 免费档是 chat/completions,
89
+ * 不走 responses 的扁平 `name` 形态。
90
+ */
91
+ function ensureDecoyTools(obj: Record<string, unknown>): void {
92
+ const tools = Array.isArray(obj.tools) ? (obj.tools as Array<Record<string, unknown>>) : []
93
+ const names = new Set<string>()
94
+ for (const t of tools) {
95
+ const fn = t?.function as Record<string, unknown> | undefined
96
+ const n = typeof fn?.name === 'string' ? fn.name : typeof t?.name === 'string' ? t.name : ''
97
+ if (n !== '') names.add(n)
98
+ }
99
+ for (const d of DECOY_TOOLS) {
100
+ if (names.has(d.name)) continue
101
+ tools.push({
102
+ type: 'function',
103
+ function: { name: d.name, description: d.description, parameters: { type: 'object', properties: {} } },
104
+ })
105
+ }
106
+ obj.tools = tools
107
+ }
108
+
30
109
  export default function factory(env: SupplierEnv): SupplierModule {
31
110
  // 模型缓存由 dsh-router 核心统一管;插件每次拉取,失败回退上次成功结果
32
111
  let modelsCache: ModelInfo[] | undefined
@@ -34,6 +113,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
34
113
 
35
114
  async function refreshModels(): Promise<ModelInfo[]> {
36
115
  try {
116
+ // /models 无握手要求(实测裸 curl 也 200),不必带 session 头
37
117
  const resp = await fetch(MODELS_URL, {
38
118
  headers: { 'User-Agent': UA, 'x-opencode-client': 'desktop' },
39
119
  signal: AbortSignal.timeout(15000),
@@ -89,11 +169,20 @@ export default function factory(env: SupplierEnv): SupplierModule {
89
169
  const msg = `unknown free model ${JSON.stringify(req.model)}`
90
170
  return { ok: false, state: 'no_such_model', message: msg }
91
171
  }
92
- // 模型名规范化:剥 alias 前缀,body.model 用裸 id
172
+ // 模型名规范化 + 握手补齐。解析失败也要继续发(宁可不改,不能不发):
173
+ // 上游只认 JSON,坏 body 由它按 400 拒,那正是我们要如实上报的状态。
93
174
  let body = req.rawBody
94
175
  try {
95
176
  const obj = JSON.parse(body) as Record<string, unknown>
96
177
  obj.model = stripAlias(String(obj.model ?? req.model), currentAlias())
178
+ // 免费档**只支持流式**:`stream:false` 即使握手齐全也 403(实测)。
179
+ // 客户端要非流式时代理层会聚合 SSE,所以这里恒为 true。
180
+ obj.stream = true
181
+ // 上游要求 tools 里同时有 bash/read,否则 403(缺哪个补哪个)
182
+ ensureDecoyTools(obj)
183
+ // effort 白名单:'off'/'none' 必须**删字段**(上游没有这两个值,
184
+ // 传 'off' 直接 400 invalid_request_error);low/high/max 实测可过。
185
+ // 'auto' 保持调用方原样不动(可能带了自己的 reasoning_effort)。
97
186
  if (lv !== 'auto' && lv !== '' && lv !== 'none' && lv !== 'off') obj.reasoning_effort = lv
98
187
  else if (lv === 'none' || lv === 'off') {
99
188
  delete obj.reasoning_effort
@@ -110,9 +199,11 @@ export default function factory(env: SupplierEnv): SupplierModule {
110
199
  method: 'POST',
111
200
  headers: {
112
201
  'Content-Type': 'application/json',
202
+ // 免费档固定字面量 'public';实测缺这行也 200,但保留以免上游收紧
113
203
  Authorization: 'Bearer public',
114
204
  'User-Agent': UA,
115
205
  'x-opencode-client': 'desktop',
206
+ ...sessionHeaders(),
116
207
  },
117
208
  body,
118
209
  signal: AbortSignal.timeout(120000),
@@ -129,22 +220,13 @@ export default function factory(env: SupplierEnv): SupplierModule {
129
220
  return { ok: false, state: stateFromHttpStatus(upstream.status), message: msg }
130
221
  }
131
222
 
132
- // 流式:上游已是 OpenAI SSE,原样交回核心(核心负责写响应)
133
- if (req.stream) {
134
- if (!upstream.body) {
135
- const msg = 'opencode upstream: empty stream body'
136
- return { ok: false, state: 'transport', message: msg }
137
- }
138
- return { ok: true, stream: upstream.body }
139
- }
140
-
141
- // 非流式:透传 JSON(空 body 视为上游异常)
142
- const text = await upstream.text().catch(() => '')
143
- if (text === '') {
144
- const msg = 'opencode upstream: empty body'
223
+ // 上游只有流式(上面已强制 stream:true),所以这里恒回流。
224
+ // 客户端要非流式时由核心的 aggregateSSE 聚合——协议错配归核心吸收。
225
+ if (!upstream.body) {
226
+ const msg = 'opencode upstream: empty stream body'
145
227
  return { ok: false, state: 'transport', message: msg }
146
228
  }
147
- return { ok: true, status: upstream.status, body: text }
229
+ return { ok: true, stream: upstream.body }
148
230
  },
149
231
  dispose: (): void => {
150
232
  modelsCache = undefined