dsh-router-core 0.4.5 → 0.4.7

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.
Files changed (75) hide show
  1. package/README.md +152 -12
  2. package/docs/ext.md +1 -1
  3. package/docs/protocols.md +198 -0
  4. package/docs/suppliers.md +2 -2
  5. package/lib/client-registry.js +146 -106
  6. package/lib/client.js +146 -106
  7. package/lib/ext-prompt/index.js +182 -17
  8. package/lib/index.js +2764 -63
  9. package/lib/suppliers/opencode/index.js +58 -30
  10. package/lib/types/ext/contract.d.ts +20 -1
  11. package/lib/types/ext-prompt/content.d.ts +7 -1
  12. package/lib/types/ext-prompt/mount.d.ts +12 -1
  13. package/lib/types/ext-prompt/plugin.d.ts +10 -0
  14. package/lib/types/ext-prompt/render.d.ts +14 -3
  15. package/lib/types/ext-prompt/sections.d.ts +95 -0
  16. package/lib/types/protocol/anthropic.d.ts +35 -0
  17. package/lib/types/protocol/chat.d.ts +25 -0
  18. package/lib/types/protocol/entry.d.ts +137 -0
  19. package/lib/types/protocol/gemini.d.ts +57 -0
  20. package/lib/types/protocol/index.d.ts +13 -0
  21. package/lib/types/protocol/responses.d.ts +41 -0
  22. package/lib/types/protocol/sse.d.ts +41 -0
  23. package/lib/types/protocol/stream.d.ts +92 -0
  24. package/lib/types/protocol/types.d.ts +224 -0
  25. package/lib/types/router/account-pool.d.ts +7 -2
  26. package/lib/types/router/index.d.ts +42 -1
  27. package/lib/types/router/types.d.ts +26 -3
  28. package/lib/types/router/usage-store.d.ts +4 -2
  29. package/lib/types/shared.d.ts +24 -0
  30. package/lib/types/suppliers/contract.d.ts +15 -1
  31. package/package.json +6 -6
  32. package/src/client/ExtDetail.tsx +25 -5
  33. package/src/client/group-controls.test.ts +53 -0
  34. package/src/client/group-controls.ts +43 -0
  35. package/src/client/router.css +20 -1
  36. package/src/ext/contract.ts +20 -1
  37. package/src/ext-prompt/assembly.test.ts +63 -0
  38. package/src/ext-prompt/content.ts +8 -10
  39. package/src/ext-prompt/ext-prompt.test.ts +38 -28
  40. package/src/ext-prompt/index.ts +50 -2
  41. package/src/ext-prompt/mount.test.ts +18 -1
  42. package/src/ext-prompt/mount.ts +22 -7
  43. package/src/ext-prompt/plugin.ts +64 -1
  44. package/src/ext-prompt/render.test.ts +1 -1
  45. package/src/ext-prompt/render.ts +17 -6
  46. package/src/ext-prompt/section-controls.test.ts +148 -0
  47. package/src/ext-prompt/section-filter.test.ts +140 -0
  48. package/src/ext-prompt/sections.test.ts +123 -0
  49. package/src/ext-prompt/sections.ts +193 -0
  50. package/src/ext-route.test.ts +16 -0
  51. package/src/index.ts +117 -6
  52. package/src/llm/adapter.test.ts +54 -0
  53. package/src/llm/adapter.ts +24 -2
  54. package/src/protocol/anthropic.ts +662 -0
  55. package/src/protocol/chat.ts +272 -0
  56. package/src/protocol/entry.ts +440 -0
  57. package/src/protocol/gemini.ts +548 -0
  58. package/src/protocol/index.ts +31 -0
  59. package/src/protocol/protocol.test.ts +728 -0
  60. package/src/protocol/responses.ts +551 -0
  61. package/src/protocol/sse.ts +94 -0
  62. package/src/protocol/stream.ts +195 -0
  63. package/src/protocol/types.ts +249 -0
  64. package/src/protocol/wire-e2e.test.ts +208 -0
  65. package/src/protocol/wiring.test.ts +225 -0
  66. package/src/router/account-pool.test.ts +35 -0
  67. package/src/router/account-pool.ts +15 -4
  68. package/src/router/index.ts +143 -24
  69. package/src/router/types.ts +26 -3
  70. package/src/router/usage-store.ts +4 -2
  71. package/src/shared.test.ts +35 -0
  72. package/src/shared.ts +33 -0
  73. package/src/suppliers/contract.ts +15 -2
  74. package/src/suppliers/opencode/plugin.test.ts +79 -25
  75. package/src/suppliers/opencode/plugin.ts +60 -29
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 外部 OpenAI 客户端归因标签(clientIdentityOf)的闸门。
3
+ *
4
+ * 2026-10-15 事故反查暴露的盲区:`/v1` 没有宿主内部头的请求全落成匿名
5
+ * usage 记录(296 条对不上任何会话)。这里锁住三级兜底的优先级与边界。
6
+ * 用 node --test 跑(Node 原生 TS 剥离,零新依赖)。
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { clientIdentityOf } from './shared.ts'
11
+
12
+ test('归因优先级:user > prompt_cache_key > User-Agent', () => {
13
+ assert.equal(clientIdentityOf({ user: 'zed-thread-1', prompt_cache_key: 'k' }, 'Zed/0.2'), 'ext/user:zed-thread-1')
14
+ assert.equal(clientIdentityOf({ prompt_cache_key: 'k1' }, 'Zed/0.2'), 'ext/cache:k1')
15
+ assert.equal(clientIdentityOf({}, 'Zed/0.2'), 'ext/ua:Zed/0.2')
16
+ assert.equal(clientIdentityOf({}, undefined), undefined, '什么都没有 → 保持匿名')
17
+ })
18
+
19
+ test('空串/非字符串的 user 与 prompt_cache_key 当没有,落 UA 兜底', () => {
20
+ assert.equal(clientIdentityOf({ user: '' }, 'Zed/0.2'), 'ext/ua:Zed/0.2')
21
+ assert.equal(clientIdentityOf({ user: 123 }, 'Zed/0.2'), 'ext/ua:Zed/0.2')
22
+ assert.equal(clientIdentityOf({ user: '', prompt_cache_key: '' }, ''), undefined)
23
+ })
24
+
25
+ test('值截断到 64 字符(防超长标识灌进 usage 记录)', () => {
26
+ const long = 'x'.repeat(100)
27
+ assert.equal(clientIdentityOf({ user: long }, undefined), `ext/user:${'x'.repeat(64)}`)
28
+ assert.equal(clientIdentityOf({}, long), `ext/ua:${'x'.repeat(64)}`)
29
+ })
30
+
31
+ test('宿主会话 id 形态与外部标签形态不撞(lastHit 过滤互不误命中)', () => {
32
+ const host = 'session-72847baa-e309-42ad-aa6b-15107cdc6518'
33
+ const ext = clientIdentityOf({ user: '72847baa-e309-42ad-aa6b-15107cdc6518' }, undefined)
34
+ assert.notEqual(ext, host, '前缀区分宿主/外部,精确匹配过滤不会串')
35
+ })
package/src/shared.ts CHANGED
@@ -192,6 +192,17 @@ export interface ExtControlItem {
192
192
  body?: string
193
193
  /** 文本可编辑(缺省 = 只读;见 `ExtControl.editable`)。 */
194
194
  editable?: boolean
195
+ /**
196
+ * 分组名(可选;缺省 = 无组)—— 详情页按它分节渲染,组间插组标题行;
197
+ * 无组照旧平铺(见 `ExtControl.group`)。
198
+ */
199
+ group?: string
200
+ /**
201
+ * 参与拖动排序(可选;缺省 = 参与)—— `false` 的条目不给拖把手
202
+ * (见 `ExtControl.sortable`)。⚠️ 服务端**带值透传**,`false` 也会收到;
203
+ * 判断必须写 `sortable !== false`,`!sortable` 会把缺省(参与)当不参与。
204
+ */
205
+ sortable?: boolean
195
206
  /**
196
207
  * 自建条目(可删)还是内置(不可删、只有开关与还原)。
197
208
  *
@@ -321,3 +332,25 @@ export interface RouterChartResponse {
321
332
  error?: string
322
333
  chart?: RouterChartBucket[]
323
334
  }
335
+
336
+ /**
337
+ * 外部 OpenAI 兼容客户端的归因标签(`/v1` 没有宿主内部头 `x-dsh-router-session`
338
+ * 时的兜底)——否则 usage 记录里全是匿名流量,出事反查不出是谁在打
339
+ * (2026-10-15 实测:296 条匿名记录对不上任何会话)。
340
+ *
341
+ * 取值优先级:`user`(OpenAI 标准终端用户字段)→ `prompt_cache_key`
342
+ * (Zed 等客户端的缓存键,常带线程粒度)→ `User-Agent`(保底到客户端粒度)。
343
+ * 值截断到 64 字符;三者皆无返回 undefined(保持匿名)。
344
+ */
345
+ export function clientIdentityOf(
346
+ body: { user?: unknown; prompt_cache_key?: unknown },
347
+ userAgent: string | undefined,
348
+ ): string | undefined {
349
+ const tag = (kind: string, v: unknown): string | undefined =>
350
+ typeof v === 'string' && v !== '' ? `ext/${kind}:${v.slice(0, 64)}` : undefined
351
+ return (
352
+ tag('user', body.user)
353
+ ?? tag('cache', body.prompt_cache_key)
354
+ ?? (userAgent !== undefined && userAgent !== '' ? `ext/ua:${userAgent.slice(0, 64)}` : undefined)
355
+ )
356
+ }
@@ -13,6 +13,7 @@ import type { Context } from '@deepseek-ai/cordis'
13
13
  import type { ChatRequest, ModelInfo, SupplierAccount } from '../router/types.ts'
14
14
  import type { SupplierConfigStore } from '../supplier-config.ts'
15
15
  import type { CredentialStore } from '../credential-store.ts'
16
+ import type { ProtocolId } from '../protocol/types.ts'
16
17
 
17
18
  /** 供应商配置:loader 注入的运行时环境。 */
18
19
  export interface SupplierEnv {
@@ -47,9 +48,16 @@ export interface SupplierEnv {
47
48
  * `source` 是**可选标签**,声明这个供应商是从哪儿来的。核心用它把 `/health` 的
48
49
  * `source` 报准(面板按它分「内置 / 插件」两组,面板和插件页都看这个值)。
49
50
  * 不声明 = `external`(独立安装的供应商插件的现状,一个字段都不用改)。
51
+ *
52
+ * `wire` 是**协议标签**(可选):这个供应商的上游说哪种协议。缺省
53
+ * `'chat'`(= OpenAI Chat Completions,今天所有供应商的现状,一个都不用改)。
54
+ * 声明成别种时,核心在把 `req` 交给插件前把规范请求体翻成它的协议,
55
+ * 并把它的响应流翻回规范形态 —— 插件看到的仍是「OpenAI SSE」,
56
+ * 于是**契约不变、已有插件全都不动**。详见 `protocol/`。
50
57
  */
51
58
  export type SupplierFactory = ((env: SupplierEnv) => SupplierModule) & {
52
59
  source?: 'builtin'
60
+ wire?: ProtocolId
53
61
  }
54
62
 
55
63
  /**
@@ -117,8 +125,13 @@ export type ChatOnceResult =
117
125
  | { ok: true; stream: ReadableStream<Uint8Array> }
118
126
  /** 非流式:核心写 JSON。 */
119
127
  | { ok: true; status: number; body: string }
120
- /** 失败:核心据此做冷却/换号。 */
121
- | { ok: false; state: AccountState; message: string }
128
+ /**
129
+ * 失败:核心据此做冷却/换号。
130
+ * `resetAt` = 上游自己声明的重置时刻(epoch ms,可选):限流类错误带它时
131
+ * 核心冷却到该点,而不是指数退避——频率配额的重置是小时级的,5min 上限的
132
+ * 退避只会到点回来再撞(2026-10-15 的 6004 事故:11 个号轮流被打冷)。
133
+ */
134
+ | { ok: false; state: AccountState; message: string; resetAt?: number }
122
135
 
123
136
  /** 供应商模块 —— 契约(核心必须,差异化可选)。 */
124
137
  export interface SupplierModule {
@@ -1,17 +1,24 @@
1
1
  /**
2
- * opencode(Zen 免费档)握手契约测试 —— 锁住 2026-09-22 实测的那组硬门。
2
+ * opencode(OpenCode Zen 免费档)握手契约测试 —— 锁住 2026-10-15 实测的那组硬门。
3
3
  *
4
- * 为什么值得固化:这四条**都不是可选优化,缺一即 403**
4
+ * 为什么值得固化:这些**都不是可选优化,缺一即 403**
5
5
  * `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
6
- * 它们全是「看不出来」的那种约定——代码删掉一行 headers、或把 UA 改回
7
- * `opencode`,单测本该立刻变红,否则下次只能在真机对话里发现整个供应商死了。
6
+ * 它们全是「看不出来」的那种约定——代码删掉一行、或把端点/工具名改回去,
7
+ * 单测本该立刻变红,否则下次只能在真机对话里发现整个供应商死了。
8
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
9
+ * 实测依据(2026-10-15,逐项消融 + 单因子隔离):
10
+ * - 端点必须是 `/inference/v1/chat/completions`:`/zen/v1` 已废弃,
11
+ * 对任何头部/工具组合一律 403(旧实现死在这里)
12
+ * - `tools` 里必须存在**小写精确名为 `read`** 的工具:
13
+ * `read` 单独→200;`bash`/`glob`/`write`/`shell` 单独→403;
14
+ * `read` 改名 `READ`→403;但 `read` 的 `parameters` 完全不看
15
+ * (`properties:{}`、无 `parameters`、`parameters:null` 全 200)
16
+ * - session 的 12 位 hex 前缀必须是倒序时间戳且低 12 位 ∈ [0,63]:
17
+ * 纯随机 hex→403、sha256 派生→403、counter=64/300→403
18
+ * - 非流式(`stream:false`)即使其余齐全也 403 → 必须恒发 stream:true
19
+ *
20
+ * 已实测**不敏感**(故不作为断言,改回旧值也不算错):UA 具体版本、
21
+ * `x-opencode-client` 的 cli/desktop、工具 description。
15
22
  *
16
23
  * 用 node --test 跑(Node 原生剥 TS 类型):
17
24
  * node --test src/suppliers/opencode/plugin.test.ts
@@ -28,6 +35,19 @@ import type { ChatRequest } from '../../router/types.ts'
28
35
  const SES_RE = /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/
29
36
  const MSG_RE = /^msg_[0-9a-f]{12}[0-9A-Za-z]{14}$/
30
37
 
38
+ /** 上游免费档的推理端点。`/zen/v1` 已废弃,出现即回归。 */
39
+ const EXPECTED_CHAT_URL = 'https://opencode.ai/inference/v1/chat/completions'
40
+
41
+ /**
42
+ * 从 session id 还原出 counter:12 位 hex 是 `~(ts * 0x1000 + counter)`,
43
+ * 故先取反还原 `ts * 0x1000 + counter`,再取低 12 位即 counter
44
+ * (`ts * 0x1000` 的低 12 位恒为 0)。
45
+ */
46
+ function invertedCounter(id: string): bigint {
47
+ const time = BigInt('0x' + id.slice(4, 16))
48
+ return ~time & 0xffffffffffffn & 0xfffn
49
+ }
50
+
31
51
  interface Captured {
32
52
  url: string
33
53
  headers: Record<string, string>
@@ -70,25 +90,38 @@ function request(over: Partial<ChatRequest> = {}): ChatRequest {
70
90
  }
71
91
  }
72
92
 
73
- test('chatOnce 必须带版本化 UA + 合法形状的 session/request 头', async () => {
93
+ test('chat 必须打 /inference/v1(/zen/v1 已废弃,全组合 403)', async () => {
74
94
  const stub = stubFetch()
75
95
  try {
76
96
  const r = await makePlugin().chatOnce('', 'auto', request())
77
97
  assert.equal(r.ok, true, '握手齐全时应当成功')
78
- const h = stub.captured[0]!.headers
98
+ assert.equal(stub.captured[0]!.url, EXPECTED_CHAT_URL, '/zen/v1 是本次故障根因,不得回退')
99
+ } finally {
100
+ stub.restore()
101
+ }
102
+ })
79
103
 
80
- // UA 必须带版本号(裸 'opencode' 会被上游按非 CLI 拒掉)
81
- assert.match(h['User-Agent'] ?? '', /^opencode\/\d+\.\d+/, 'UA 必须带 opencode/ 版本号')
104
+ test('session 的 12 位 hex 必须是倒序时间戳,低 12 位 counter ∈ [0,63]', async () => {
105
+ const stub = stubFetch()
106
+ try {
107
+ await makePlugin().chatOnce('', 'auto', request())
108
+ const h = stub.captured[0]!.headers
82
109
 
83
- // session/request 形状不对上游是**拒绝**而不是忽略 —— 形状必须严格
110
+ // 形状不对上游是**拒绝**而不是忽略 —— 形状必须严格
84
111
  assert.match(h['x-opencode-session'] ?? '', SES_RE, 'x-opencode-session 形状不对会被 403')
85
112
  assert.match(h['x-opencode-request'] ?? '', MSG_RE, 'x-opencode-request 形状不对会被 403')
113
+
114
+ // 纯随机 hex 与 sha256 派生 hex 都落不进 [0,63] 窗口 → 必然 403
115
+ for (const key of ['x-opencode-session', 'x-opencode-request'] as const) {
116
+ const counter = invertedCounter(h[key]!)
117
+ assert.ok(counter <= 63n, `${key} 的 counter 必须 ∈ [0,63](实测 64/300 会 403),得到 ${counter}`)
118
+ }
86
119
  } finally {
87
120
  stub.restore()
88
121
  }
89
122
  })
90
123
 
91
- test('请求体恒为流式,且 tools 同时含 bash 与 read', async () => {
124
+ test('tools 必须含小写 read;bash 不是门,且不得被注入(旧实现的回归点)', async () => {
92
125
  const stub = stubFetch()
93
126
  try {
94
127
  // 调用方明确要非流式 —— 上游免费档对非流式一律 403,插件必须翻成流式
@@ -96,32 +129,53 @@ test('请求体恒为流式,且 tools 同时含 bash 与 read', async () => {
96
129
  const body = stub.captured[0]!.body
97
130
  assert.equal(body.stream, true, '免费档只支持流式,非流式必须翻成 stream:true')
98
131
 
99
- const names = (body.tools as Array<{ function?: { name?: string } }>).map((t) => t.function?.name)
100
- assert.ok(names.includes('bash'), 'tools 缺 bash 会被 403')
132
+ const tools = body.tools as Array<{ function?: { name?: string } }>
133
+ const names = tools.map((t) => t.function?.name)
101
134
  assert.ok(names.includes('read'), 'tools 缺 read 会被 403')
135
+ // 实测:bash 不是门(bash 单独 → 403),注入它只会污染调用方的工具表
136
+ assert.equal(names.includes('bash'), false, 'bash 不是免费档的门,不该被注入')
137
+ assert.equal(tools.length, 1, '无调用方工具时只应补 read 一个占位')
138
+ } finally {
139
+ stub.restore()
140
+ }
141
+ })
142
+
143
+ test('read 的 parameters 不被检查(空 schema 即可通过)', async () => {
144
+ const stub = stubFetch()
145
+ try {
146
+ await makePlugin().chatOnce('', 'auto', request())
147
+ const tools = stub.captured[0]!.body.tools as Array<{ function: { name: string; parameters?: unknown } }>
148
+ const read = tools.find((t) => t.function.name === 'read')!
149
+ // 上游只看名字,故占位用空 properties 是对的(别"补"成真 schema 增加噪音)
150
+ assert.deepEqual(read.function.parameters, { type: 'object', properties: {} })
102
151
  } finally {
103
152
  stub.restore()
104
153
  }
105
154
  })
106
155
 
107
- test('调用方自带的同名工具不被占位工具覆盖(bash/read 只补缺)', async () => {
156
+ test('调用方自带的同名工具不被占位工具覆盖(read 只补缺)', async () => {
108
157
  const stub = stubFetch()
109
158
  try {
159
+ const realRead = {
160
+ type: 'function',
161
+ function: { name: 'read', description: 'REAL READ', parameters: { type: 'object', properties: { filePath: { type: 'string' } } } },
162
+ }
110
163
  const realBash = {
111
164
  type: 'function',
112
165
  function: { name: 'bash', description: 'REAL BASH', parameters: { type: 'object', properties: { command: { type: 'string' } } } },
113
166
  }
114
167
  await makePlugin().chatOnce('', 'auto', request({
115
- rawBody: JSON.stringify({ model: 'big-pickle', messages: [], stream: true, tools: [realBash] }),
168
+ rawBody: JSON.stringify({ model: 'big-pickle', messages: [], stream: true, tools: [realRead, realBash] }),
116
169
  }))
117
170
  const tools = stub.captured[0]!.body.tools as Array<{ function: { name: string; description?: string } }>
118
171
 
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', '调用方自己的定义必须原样保留')
172
+ const read = tools.filter((t) => t.function.name === 'read')
173
+ assert.equal(read.length, 1, 'read 不能被注入成两条(否则真实工具签名被挤掉)')
174
+ assert.equal(read[0]!.function.description, 'REAL READ', '调用方自己的定义必须原样保留')
175
+ assert.equal(tools.length, 2, '已有 read 时不该再补任何占位')
122
176
 
123
- // read 缺失 → 补上;bash 已有 → 不重复
124
- assert.ok(tools.some((t) => t.function.name === 'read'), '缺的 read 要补上')
177
+ // 调用方的 bash 必须原样留着(它不是门,但也绝不能被删)
178
+ assert.equal(tools.filter((t) => t.function.name === 'bash').length, 1, '调用方的 bash 不得被动')
125
179
  } finally {
126
180
  stub.restore()
127
181
  }
@@ -1,14 +1,33 @@
1
1
  /**
2
- * opencode 供应商插件 —— 参考 9Router(open-sse) 的 opencode free 实现。
2
+ * opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
3
3
  *
4
- * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,免费档)
5
- * - chat: POST /zen/v1/chat/completions(**只能流式**)
6
- * - models: GET /zen/v1/models(透传,只取免费模型)
4
+ * 上游(2026-10-15 实测):
5
+ * - chat: POST https://opencode.ai/inference/v1/chat/completions(**只能流式**)
6
+ * - models: GET https://opencode.ai/inference/v1/models(两个端点都通,取免费模型)
7
7
  *
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
- * 免费档也没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
8
+ * ⚠️ `/zen/v1/chat/completions` **已废弃**:2026-10-15 全矩阵实测该端点对任何
9
+ * 头部/工具组合一律 403,迁到 `/inference/v1` 后同样请求立刻 200。旧的
10
+ * `zen/v1` 常量是本次故障的根因之一,别再改回去。
11
+ * 注:2026-10-16 复核,`/zen/v1/chat/completions` 在握手完全正确时也能 200
12
+ * (即"一律 403"仅对当时的随机 hex session 成立);两个端点的差异对正确握手
13
+ * 不可见。`/inference/v1` 仍是**实测更稳**的那个,保留不回退。
14
+ *
15
+ * 「免费」不等于「无鉴权」:仍要求**模拟真 CLI 的握手**,否则一律
16
+ * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
17
+ * 2026-10-15 逐项消融后,真正的门只剩两条(其余全不敏感):
18
+ *
19
+ * 1. `tools` 里必须存在一个**小写精确名为 `read`** 的工具。
20
+ * —— `parameters` 完全不看(实测 `properties:{}`、无 `parameters`、
21
+ * `parameters:null` 全部 200);但名字大小写敏感(`READ` → 403),且
22
+ * `bash`/`glob`/`write` 等**不能替代** `read`(各自单独都给 403)。
23
+ * 2. `x-opencode-session` 的 12 位 hex 前缀必须是**倒序时间戳**,且其低 12 位
24
+ * (counter)落在 `[0,63]`(见 `newSessionId`)。
25
+ *
26
+ * 已实测**不敏感**(改了照样 200,故不写死):UA 具体版本(`opencode/1.18.31`
27
+ * 与 `opencode/latest/2.0.29/cli` 等价)、`x-opencode-client` 的 `cli`/`desktop`、
28
+ * `x-opencode-request` 有无、`x-opencode-project` 是否 40-hex、工具 `description`。
29
+ *
30
+ * 免费档没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
12
31
  * (连接池、签到由能力检测自动隐藏)。
13
32
  */
14
33
  import { randomBytes } from 'node:crypto'
@@ -23,32 +42,38 @@ export const priority = 0
23
42
  /** 面板图标。 */
24
43
  export const icon = 'data:image/png;base64,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'
25
44
 
26
- const BASE = 'https://opencode.ai/zen/v1'
45
+ /** 上游免费档的推理端点。`/zen/v1` 已于 2026-10-15 废弃(全组合 403)。 */
46
+ const BASE = 'https://opencode.ai/inference/v1'
27
47
  const MODELS_URL = `${BASE}/models`
28
48
  const CHAT_URL = `${BASE}/chat/completions`
29
49
 
30
50
  /**
31
- * User-Agent 必须带**版本号**且 ≥ 1.18。
32
- *
33
- * 裸 `opencode` → 403 FreeTierError;`opencode/1.17.x` → **426 UpgradeRequired**
34
- * (上游明确要求升级客户端);`opencode/1.18.0` 起通过。别去掉版本号,
35
- * 也别写死成能过的最高版本——取一个已实测通过的稳定值即可。
51
+ * User-Agent。**不是**门禁(实测 `opencode/1.18.31` 与真 CLI 的
52
+ * `opencode/1.19.0 ai-sdk/...` 等价,裸 `opencode` 也不影响本门)。
53
+ * 保留一个带版本号的值只是别让上游按非 CLI 流量处理。
36
54
  */
37
55
  const UA = 'opencode/1.18.31'
38
56
 
39
57
  /**
40
- * 免费档按 session 记账(每次生成一个新的即可——见 `chatOnce` 的实测说明),
41
- * `x-opencode-session` / `x-opencode-request` **必须**是上游认的形状,格式不对
42
- * 同样 403(不是忽略,是拒绝):
43
- * ses_<12 位小写 hex><14 位 base62>、msg_<同形状>
58
+ * 造上游认的 session/request id。
59
+ *
60
+ * ⚠️ 12 位 hex 前缀**不是任意 hex,也不是摘要派生**,它是真客户端的
61
+ * `~(Date.now() * 0x1000 + counter)`:倒序时间戳。2026-10-15 实测(逐项消融):
62
+ * - 倒序时间戳 counter=0/1/62/63 → 200;counter=64/65/100 → 403
63
+ * - 纯随机 hex → 403;sha256 派生 hex → 403(9router #4780 的成因)
64
+ * 即上游会把低 12 位当每毫秒计数器读,只接受 `[0,63]`;随机/派生值落进该窗口
65
+ * 只有约 1.6% 概率,所以旧实现几乎必然 403。(2026-10-16 每个边界值各打 3 次
66
+ * 复核:63 三次全 200、64 三次全 403,边界就是 63/64 之间。)
67
+ *
68
+ * counter 固定取 1:本插件是自己 session 的唯一写入方,做每毫秒计数器只增加状态。
44
69
  */
45
70
  function newSessionId(prefix: 'ses_' | 'msg_'): string {
46
71
  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')
72
+ const current = BigInt(Date.now()) * 0x1000n + 1n
73
+ const time = (~current & 0xffffffffffffn).toString(16).padStart(12, '0')
74
+ const bytes = randomBytes(14)
50
75
  let rand = ''
51
- for (let i = 6; i < 20; i++) rand += B62[bytes[i]! % 62] ?? ''
76
+ for (let i = 0; i < 14; i++) rand += B62[bytes[i]! % 62] ?? ''
52
77
  return `${prefix}${time}${rand}`
53
78
  }
54
79
 
@@ -67,15 +92,21 @@ function sessionHeaders(): Record<string, string> {
67
92
  }
68
93
 
69
94
  /**
70
- * 免费档要求 tools 里**同时**存在 `bash` 和 `read`,否则 403。
95
+ * 免费档要求的占位工具:**只需要 `read` 一个**,小写精确。
96
+ *
97
+ * 2026-10-15 逐项消融实测:
98
+ * - `read` 单独 → 200;`bash` 单独 → 403;`glob`/`write`/`shell` 单独 → 403
99
+ * - `read` + 任意其他(含空 schema 的 bash/glob/grep…)→ 200
100
+ * - 把 `read` 改名成 `READ` → 403(大小写敏感)
101
+ * - `read` 的 `parameters` 完全不看:`properties:{}`、无 `parameters`、
102
+ * `parameters:null`、`parameters:{}` 全部 200
71
103
  *
72
- * 为什么是「注入」而不是「覆盖」:上游只是**校验这两个名字在不在**,真正的
73
- * 工具照常工作(实测真实工具在前、占位在后,混合下发 200)。所以调用方已经
74
- * 带了同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的定义,
75
- * 否则会把真实工具签名糊掉。
104
+ * 所以旧版「必须同时有 bash 和 read」是错的——**`bash` 不是门,`read` 才是**。
105
+ * 为什么是「注入」而不是「覆盖」:上游只校验名字在不在,真实工具照常工作,
106
+ * 所以调用方已带同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的
107
+ * 定义,否则会把真实工具签名糊掉。
76
108
  */
77
109
  const DECOY_TOOLS: ReadonlyArray<{ name: string; description: string }> = [
78
- { name: 'bash', description: 'This tool is currently unavailable and must not be used.' },
79
110
  { name: 'read', description: 'This tool is currently unavailable and must not be used.' },
80
111
  ]
81
112
 
@@ -178,7 +209,7 @@ export default function factory(env: SupplierEnv): SupplierModule {
178
209
  // 免费档**只支持流式**:`stream:false` 即使握手齐全也 403(实测)。
179
210
  // 客户端要非流式时代理层会聚合 SSE,所以这里恒为 true。
180
211
  obj.stream = true
181
- // 上游要求 tools 里同时有 bash/read,否则 403(缺哪个补哪个)
212
+ // 上游要求 tools 里存在小写 `read`,否则 403(缺就补,已有则原样保留)
182
213
  ensureDecoyTools(obj)
183
214
  // effort 白名单:'off'/'none' 必须**删字段**(上游没有这两个值,
184
215
  // 传 'off' 直接 400 invalid_request_error);low/high/max 实测可过。