dsh-router-core 0.4.5 → 0.4.6

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/docs/suppliers.md CHANGED
@@ -138,7 +138,7 @@ js 只要知道这一条:**报 `rate_limit`/`transport`/`unknown` 只影响「
138
138
  type ChatOnceResult =
139
139
  | { ok: true; stream: ReadableStream<Uint8Array> } // 流式
140
140
  | { ok: true; status: number; body: string } // 非流式
141
- | { ok: false; state: AccountState; message: string } // 失败
141
+ | { ok: false; state: AccountState; message: string; resetAt?: number } // 失败
142
142
  ```
143
143
 
144
144
  要点:
@@ -151,7 +151,7 @@ type ChatOnceResult =
151
151
 
152
152
  | state | 核心处置 |
153
153
  |---|---|
154
- | `rate_limit` | **指数退避**:首次 2s,每次翻倍,上限 5 分钟;成功后等级清零 |
154
+ | `rate_limit` | **指数退避**:首次 2s,每次翻倍,上限 5 分钟;成功后等级清零。失败项带 `resetAt`(上游声明的重置时刻,epoch ms)时**冷到该点**——频率配额的重置是小时级的,退避上限只会到点回来再撞 |
155
155
  | `quota` | 冷却 10 分钟 |
156
156
  | `session_dead` | **连接级冷却 30 分钟**(该号所有模型都停用,到期自愈;没有永久禁用——403 分不清凭证死活与风控误伤) |
157
157
  | `unavailable` / `transport` / `unknown` | 瞬时短冷却 30s(**每次都冷**,不攒次数) |
package/lib/index.js CHANGED
@@ -10,6 +10,19 @@ import { homedir } from "node:os";
10
10
  //#region src/shared.ts
11
11
  /** Same-origin API base the client panel fetches (host half serves it). */
12
12
  const ROUTER_API_BASE = "/router/api";
13
+ /**
14
+ * 外部 OpenAI 兼容客户端的归因标签(`/v1` 没有宿主内部头 `x-dsh-router-session`
15
+ * 时的兜底)——否则 usage 记录里全是匿名流量,出事反查不出是谁在打
16
+ * (2026-10-15 实测:296 条匿名记录对不上任何会话)。
17
+ *
18
+ * 取值优先级:`user`(OpenAI 标准终端用户字段)→ `prompt_cache_key`
19
+ * (Zed 等客户端的缓存键,常带线程粒度)→ `User-Agent`(保底到客户端粒度)。
20
+ * 值截断到 64 字符;三者皆无返回 undefined(保持匿名)。
21
+ */
22
+ function clientIdentityOf(body, userAgent) {
23
+ const tag = (kind, v) => typeof v === "string" && v !== "" ? `ext/${kind}:${v.slice(0, 64)}` : void 0;
24
+ return tag("user", body.user) ?? tag("cache", body.prompt_cache_key) ?? (userAgent !== void 0 && userAgent !== "" ? `ext/ua:${userAgent.slice(0, 64)}` : void 0);
25
+ }
13
26
  //#endregion
14
27
  //#region src/router/prefix-affinity.ts
15
28
  /**
@@ -116,7 +129,9 @@ const MINUTE = 6e4;
116
129
  *
117
130
  * 冷却策略对齐 9router 的 accountFallback / errorConfig:
118
131
  * - `rate_limit` 用**指数退避**(9router 的 `backoff: true`):偶发限流只
119
- * 短冷,持续限流越退越久,比固定 1 分钟两头不讨好要准。
132
+ * 短冷,持续限流越退越久,比固定 1 分钟两头不讨好要准。上游自己声明了
133
+ * **重置时刻**(resetAt,如 6004「将在 … 重置」)时冷到该点——频率配额是
134
+ * 小时级的,退避上限(5min)只会到点回来再撞。
120
135
  * - 未知/瞬时错误用**瞬时短冷却**(9router 默认分支 TRANSIENT_COOLDOWN_MS):
121
136
  * 每次失败都冻结,坏号不会留在池里被下一个请求再选中。
122
137
  *
@@ -473,8 +488,13 @@ var AccountPool = class {
473
488
  blockKey(modelId) {
474
489
  return `${this.supplierId}${SEP}${modelId}`;
475
490
  }
476
- /** 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。 */
477
- noteFailure(uid, modelId, state, message) {
491
+ /**
492
+ * 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。
493
+ * `resetAt` = 上游声明的重置时刻(epoch ms):限流退避优先冷到该点——
494
+ * 频率配额是小时级的,5min 上限的指数退避只会到点回来再撞 6004,
495
+ * 把号轮流打冷(2026-10-15 的 11 号全灭事故)。
496
+ */
497
+ noteFailure(uid, modelId, state, message, resetAt) {
478
498
  const rule = RULES[state];
479
499
  if (rule.cooldown === 0) return;
480
500
  if (state === "session_dead") {
@@ -488,7 +508,9 @@ var AccountPool = class {
488
508
  e.reason = message;
489
509
  if (rule.cooldown === "backoff") {
490
510
  e.backoffLevel = Math.min(e.backoffLevel + 1, BACKOFF_MAX_LEVEL);
491
- e.until = Math.max(e.until, Date.now() + backoffMs(e.backoffLevel));
511
+ const backoffUntil = Date.now() + backoffMs(e.backoffLevel);
512
+ const until = resetAt !== void 0 && resetAt > Date.now() ? resetAt : backoffUntil;
513
+ e.until = Math.max(e.until, until);
492
514
  return;
493
515
  }
494
516
  if (rule.cooldown === "transient") {
@@ -2596,7 +2618,7 @@ var Router = class {
2596
2618
  }
2597
2619
  return false;
2598
2620
  }
2599
- pool.noteFailure(uid, req.model, r.state, r.message);
2621
+ pool.noteFailure(uid, req.model, r.state, r.message, r.resetAt);
2600
2622
  pool.dropBlock(req.model);
2601
2623
  if (trace !== void 0) {
2602
2624
  trace.attempts += 1;
@@ -3036,16 +3058,18 @@ async function* translateSse(payloads) {
3036
3058
  usage
3037
3059
  };
3038
3060
  }
3061
+ const hasCompleteToolCall = complete.some((block) => block.kind === "tool-call");
3039
3062
  const reason = pendingFinish ?? { kind: "stop" };
3063
+ const resolved = reason.kind === "stop" && hasCompleteToolCall ? { kind: "tool-calls" } : reason;
3040
3064
  yield {
3041
3065
  type: "finish",
3042
- reason: reason.kind === "stop" && order.length === 0 ? {
3066
+ reason: resolved.kind === "stop" && order.length === 0 ? {
3043
3067
  kind: "error",
3044
3068
  failure: {
3045
3069
  message: "model returned a completed response with no content",
3046
3070
  code: EMPTY_RESPONSE_CODE
3047
3071
  }
3048
- } : reason
3072
+ } : resolved
3049
3073
  };
3050
3074
  return;
3051
3075
  }
@@ -5108,7 +5132,7 @@ function apply(rawContext) {
5108
5132
  }
5109
5133
  const lv = typeof peek.reasoning_effort === "string" && peek.reasoning_effort !== "" ? peek.reasoning_effort : "auto";
5110
5134
  const sessionRaw = req.headers["x-dsh-router-session"];
5111
- const session = typeof sessionRaw === "string" && sessionRaw !== "" ? sessionRaw : void 0;
5135
+ const session = (typeof sessionRaw === "string" && sessionRaw !== "" ? sessionRaw : void 0) ?? clientIdentityOf(peek, req.headers["user-agent"]);
5112
5136
  await router.chatCompletions({
5113
5137
  rawBody: body,
5114
5138
  stream: !!peek.stream,
@@ -2,46 +2,70 @@ import { n as registerSupplierRow, t as stateFromHttpStatus } from "../../http-s
2
2
  import { randomBytes } from "node:crypto";
3
3
  //#region src/suppliers/opencode/plugin.ts
4
4
  /**
5
- * opencode 供应商插件 —— 参考 9Router(open-sse) 的 opencode free 实现。
5
+ * opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
6
6
  *
7
- * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,免费档)
8
- * - chat: POST /zen/v1/chat/completions(**只能流式**)
9
- * - models: GET /zen/v1/models(透传,只取免费模型)
7
+ * 上游(2026-10-15 实测):
8
+ * - chat: POST https://opencode.ai/inference/v1/chat/completions(**只能流式**)
9
+ * - models: GET https://opencode.ai/inference/v1/models(两个端点都通,取免费模型)
10
10
  *
11
- * 「免费」不等于「无鉴权」:Zen 免费档仍要求**模拟真 CLI 的握手**,否则一律
12
- * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`
13
- * (2026-09-22 实测)。三个条件缺一即 403,见下方 `UA` / `sessionId` / `decoyTools`。
14
- * 免费档也没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
11
+ * ⚠️ `/zen/v1/chat/completions` **已废弃**:2026-10-15 全矩阵实测该端点对任何
12
+ * 头部/工具组合一律 403,迁到 `/inference/v1` 后同样请求立刻 200。旧的
13
+ * `zen/v1` 常量是本次故障的根因之一,别再改回去。
14
+ * 注:2026-10-16 复核,`/zen/v1/chat/completions` 在握手完全正确时也能 200
15
+ * (即"一律 403"仅对当时的随机 hex session 成立);两个端点的差异对正确握手
16
+ * 不可见。`/inference/v1` 仍是**实测更稳**的那个,保留不回退。
17
+ *
18
+ * 「免费」不等于「无鉴权」:仍要求**模拟真 CLI 的握手**,否则一律
19
+ * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
20
+ * 2026-10-15 逐项消融后,真正的门只剩两条(其余全不敏感):
21
+ *
22
+ * 1. `tools` 里必须存在一个**小写精确名为 `read`** 的工具。
23
+ * —— `parameters` 完全不看(实测 `properties:{}`、无 `parameters`、
24
+ * `parameters:null` 全部 200);但名字大小写敏感(`READ` → 403),且
25
+ * `bash`/`glob`/`write` 等**不能替代** `read`(各自单独都给 403)。
26
+ * 2. `x-opencode-session` 的 12 位 hex 前缀必须是**倒序时间戳**,且其低 12 位
27
+ * (counter)落在 `[0,63]`(见 `newSessionId`)。
28
+ *
29
+ * 已实测**不敏感**(改了照样 200,故不写死):UA 具体版本(`opencode/1.18.31`
30
+ * 与 `opencode/latest/2.0.29/cli` 等价)、`x-opencode-client` 的 `cli`/`desktop`、
31
+ * `x-opencode-request` 有无、`x-opencode-project` 是否 40-hex、工具 `description`。
32
+ *
33
+ * 免费档没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
15
34
  * (连接池、签到由能力检测自动隐藏)。
16
35
  */
17
36
  const id = "opencode";
18
37
  const name$1 = "OpenCode";
19
38
  /** 面板图标。 */
20
39
  const icon = "data:image/png;base64,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";
21
- const BASE = "https://opencode.ai/zen/v1";
40
+ /** 上游免费档的推理端点。`/zen/v1` 已于 2026-10-15 废弃(全组合 403)。 */
41
+ const BASE = "https://opencode.ai/inference/v1";
22
42
  const MODELS_URL = `${BASE}/models`;
23
43
  const CHAT_URL = `${BASE}/chat/completions`;
24
44
  /**
25
- * User-Agent 必须带**版本号**且 ≥ 1.18。
26
- *
27
- * 裸 `opencode` → 403 FreeTierError;`opencode/1.17.x` → **426 UpgradeRequired**
28
- * (上游明确要求升级客户端);`opencode/1.18.0` 起通过。别去掉版本号,
29
- * 也别写死成能过的最高版本——取一个已实测通过的稳定值即可。
45
+ * User-Agent。**不是**门禁(实测 `opencode/1.18.31` 与真 CLI 的
46
+ * `opencode/1.19.0 ai-sdk/...` 等价,裸 `opencode` 也不影响本门)。
47
+ * 保留一个带版本号的值只是别让上游按非 CLI 流量处理。
30
48
  */
31
49
  const UA = "opencode/1.18.31";
32
50
  /**
33
- * 免费档按 session 记账(每次生成一个新的即可——见 `chatOnce` 的实测说明),
34
- * `x-opencode-session` / `x-opencode-request` **必须**是上游认的形状,格式不对
35
- * 同样 403(不是忽略,是拒绝):
36
- * ses_<12 位小写 hex><14 位 base62>、msg_<同形状>
51
+ * 造上游认的 session/request id。
52
+ *
53
+ * ⚠️ 12 位 hex 前缀**不是任意 hex,也不是摘要派生**,它是真客户端的
54
+ * `~(Date.now() * 0x1000 + counter)`:倒序时间戳。2026-10-15 实测(逐项消融):
55
+ * - 倒序时间戳 counter=0/1/62/63 → 200;counter=64/65/100 → 403
56
+ * - 纯随机 hex → 403;sha256 派生 hex → 403(9router #4780 的成因)
57
+ * 即上游会把低 12 位当每毫秒计数器读,只接受 `[0,63]`;随机/派生值落进该窗口
58
+ * 只有约 1.6% 概率,所以旧实现几乎必然 403。(2026-10-16 每个边界值各打 3 次
59
+ * 复核:63 三次全 200、64 三次全 403,边界就是 63/64 之间。)
60
+ *
61
+ * counter 固定取 1:本插件是自己 session 的唯一写入方,做每毫秒计数器只增加状态。
37
62
  */
38
63
  function newSessionId(prefix) {
39
64
  const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
40
- const bytes = randomBytes(20);
41
- let time = "";
42
- for (let i = 0; i < 6; i++) time += bytes[i].toString(16).padStart(2, "0");
65
+ const time = (~(BigInt(Date.now()) * 4096n + 1n) & 281474976710655n).toString(16).padStart(12, "0");
66
+ const bytes = randomBytes(14);
43
67
  let rand = "";
44
- for (let i = 6; i < 20; i++) rand += B62[bytes[i] % 62] ?? "";
68
+ for (let i = 0; i < 14; i++) rand += B62[bytes[i] % 62] ?? "";
45
69
  return `${prefix}${time}${rand}`;
46
70
  }
47
71
  /**
@@ -58,17 +82,21 @@ function sessionHeaders() {
58
82
  };
59
83
  }
60
84
  /**
61
- * 免费档要求 tools 里**同时**存在 `bash` 和 `read`,否则 403。
85
+ * 免费档要求的占位工具:**只需要 `read` 一个**,小写精确。
62
86
  *
63
- * 为什么是「注入」而不是「覆盖」:上游只是**校验这两个名字在不在**,真正的
64
- * 工具照常工作(实测真实工具在前、占位在后,混合下发 200)。所以调用方已经
65
- * 带了同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的定义,
66
- * 否则会把真实工具签名糊掉。
87
+ * 2026-10-15 逐项消融实测:
88
+ * - `read` 单独 → 200;`bash` 单独 → 403;`glob`/`write`/`shell` 单独 → 403
89
+ * - `read` + 任意其他(含空 schema 的 bash/glob/grep…)→ 200
90
+ * - 把 `read` 改名成 `READ` → 403(大小写敏感)
91
+ * - `read` 的 `parameters` 完全不看:`properties:{}`、无 `parameters`、
92
+ * `parameters:null`、`parameters:{}` 全部 200
93
+ *
94
+ * 所以旧版「必须同时有 bash 和 read」是错的——**`bash` 不是门,`read` 才是**。
95
+ * 为什么是「注入」而不是「覆盖」:上游只校验名字在不在,真实工具照常工作,
96
+ * 所以调用方已带同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的
97
+ * 定义,否则会把真实工具签名糊掉。
67
98
  */
68
99
  const DECOY_TOOLS = [{
69
- name: "bash",
70
- description: "This tool is currently unavailable and must not be used."
71
- }, {
72
100
  name: "read",
73
101
  description: "This tool is currently unavailable and must not be used."
74
102
  }];
@@ -167,8 +167,13 @@ export declare class AccountPool {
167
167
  dropBlock(modelId: string): void;
168
168
  /** 块状态键:与冷却同粒度 (supplier, model),不含 uid。 */
169
169
  private blockKey;
170
- /** 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。 */
171
- noteFailure(uid: string, modelId: string, state: AccountState, message: string): void;
170
+ /**
171
+ * 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。
172
+ * `resetAt` = 上游声明的重置时刻(epoch ms):限流退避优先冷到该点——
173
+ * 频率配额是小时级的,5min 上限的指数退避只会到点回来再撞 6004,
174
+ * 把号轮流打冷(2026-10-15 的 11 号全灭事故)。
175
+ */
176
+ noteFailure(uid: string, modelId: string, state: AccountState, message: string, resetAt?: number): void;
172
177
  /** 记录一次成功:清零「该模型」的限流退避等级(好号不背历史惩罚)。 */
173
178
  noteSuccess(uid: string, modelId: string): void;
174
179
  /** 手动暂停整连接(面板/管理用途,跨模型)。ms<=0 表示解除。 */
@@ -55,9 +55,13 @@ export interface ChatRequest {
55
55
  /** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
56
56
  lv?: string;
57
57
  /**
58
- * 宿主盖的会话身份(dsh-llm `GenerateOptions.sessionId`),由 adapter 转成
59
- * 内部头带进来。用于把用量记录**按会话归因**(「最近命中」徽章要按当前会话
60
- * 显示,而不是显示别的会话的命中)。缺省 = 非宿主调用(外部 OpenAI 客户端)。
58
+ * 调用方身份(归因键)。来源两种:
59
+ * 1. 宿主会话身份(dsh-llm `GenerateOptions.sessionId`,adapter 转成内部头
60
+ * `x-dsh-router-session`)——形式 `session-<uuid>`;
61
+ * 2. 外部 OpenAI 兼容客户端的 `user` 字段(Zed/IDE 等多客户端场景)——形式
62
+ * `ext:<user>`,前缀标明「这不是宿主会话」。
63
+ * 用途:把用量记录**按调用方归因**(「最近命中」徽章按当前会话显示;出事时
64
+ * 能反查是哪个客户端在打)。缺省 = 匿名外部客户端。
61
65
  */
62
66
  session?: string;
63
67
  }
@@ -26,8 +26,10 @@ export interface UsageRecord {
26
26
  /** 失败原因(ok=false 时)。 */
27
27
  error?: string;
28
28
  /**
29
- * 宿主会话身份(有则记)。用于「最近命中」按当前会话归因 —— 徽章要显示
30
- * 当前会话的命中,而不是全局最近(team 多会话下后者会张冠李戴)。
29
+ * 调用方身份(有则记):宿主会话 id(`session-<uuid>`)或外部 OpenAI 客户端
30
+ * 标签(`ext/…`,见 shared.ts 的 clientIdentityOf)。用于「最近命中」按当前
31
+ * 会话归因(徽章要显示当前会话的命中,而不是全局最近),出事时也能反查
32
+ * 是哪个客户端在打。
31
33
  */
32
34
  session?: string;
33
35
  }
@@ -302,3 +302,16 @@ export interface RouterChartResponse {
302
302
  error?: string;
303
303
  chart?: RouterChartBucket[];
304
304
  }
305
+ /**
306
+ * 外部 OpenAI 兼容客户端的归因标签(`/v1` 没有宿主内部头 `x-dsh-router-session`
307
+ * 时的兜底)——否则 usage 记录里全是匿名流量,出事反查不出是谁在打
308
+ * (2026-10-15 实测:296 条匿名记录对不上任何会话)。
309
+ *
310
+ * 取值优先级:`user`(OpenAI 标准终端用户字段)→ `prompt_cache_key`
311
+ * (Zed 等客户端的缓存键,常带线程粒度)→ `User-Agent`(保底到客户端粒度)。
312
+ * 值截断到 64 字符;三者皆无返回 undefined(保持匿名)。
313
+ */
314
+ export declare function clientIdentityOf(body: {
315
+ user?: unknown;
316
+ prompt_cache_key?: unknown;
317
+ }, userAgent: string | undefined): string | undefined;
@@ -111,11 +111,17 @@ export type ChatOnceResult =
111
111
  status: number;
112
112
  body: string;
113
113
  }
114
- /** 失败:核心据此做冷却/换号。 */
114
+ /**
115
+ * 失败:核心据此做冷却/换号。
116
+ * `resetAt` = 上游自己声明的重置时刻(epoch ms,可选):限流类错误带它时
117
+ * 核心冷却到该点,而不是指数退避——频率配额的重置是小时级的,5min 上限的
118
+ * 退避只会到点回来再撞(2026-10-15 的 6004 事故:11 个号轮流被打冷)。
119
+ */
115
120
  | {
116
121
  ok: false;
117
122
  state: AccountState;
118
123
  message: string;
124
+ resetAt?: number;
119
125
  };
120
126
  /** 供应商模块 —— 契约(核心必须,差异化可选)。 */
121
127
  export interface SupplierModule {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  import type { IncomingMessage, ServerResponse } from 'node:http'
34
34
  import { join } from 'node:path'
35
35
  import Schema from '@deepseek-ai/schemastery'
36
- import { ROUTER_API_BASE, type RouterPeriod } from './shared.ts'
36
+ import { ROUTER_API_BASE, type RouterPeriod, clientIdentityOf } from './shared.ts'
37
37
  import { Router } from './router/index.ts'
38
38
  import { RouterAdapter, type RouterAttachmentStore } from './llm/adapter.ts'
39
39
  import { KeysStore } from './keys.ts'
@@ -363,9 +363,9 @@ export function apply(rawContext: unknown): void {
363
363
  writeJson(res, 413, { error: { message: 'request body too large', type: 'api_error', code: 'request_too_large' } })
364
364
  return
365
365
  }
366
- let peek: { stream?: boolean; model?: string; reasoning_effort?: unknown }
366
+ let peek: { stream?: boolean; model?: string; reasoning_effort?: unknown; user?: unknown; prompt_cache_key?: unknown }
367
367
  try {
368
- peek = JSON.parse(body) as { stream?: boolean; model?: string; reasoning_effort?: unknown }
368
+ peek = JSON.parse(body) as { stream?: boolean; model?: string; reasoning_effort?: unknown; user?: unknown; prompt_cache_key?: unknown }
369
369
  } catch {
370
370
  writeJson(res, 400, { error: { message: 'invalid JSON body', type: 'api_error', code: 'invalid_request' } })
371
371
  return
@@ -373,9 +373,12 @@ export function apply(rawContext: unknown): void {
373
373
  const lv = typeof peek.reasoning_effort === 'string' && peek.reasoning_effort !== ''
374
374
  ? peek.reasoning_effort
375
375
  : 'auto'
376
- // 宿主会话身份(adapter 从 GenerateOptions.sessionId 转来的内部头);外部客户端没有。
376
+ // 调用方身份:宿主会话内部头(adapter 从 GenerateOptions.sessionId 转来)
377
+ // 优先;外部客户端没有内部头,用 user/prompt_cache_key/UA 兜底归因——
378
+ // 否则 usage 记录全是匿名,反查不出是哪个客户端在打。
377
379
  const sessionRaw = req.headers['x-dsh-router-session']
378
- const session = typeof sessionRaw === 'string' && sessionRaw !== '' ? sessionRaw : undefined
380
+ const session = (typeof sessionRaw === 'string' && sessionRaw !== '' ? sessionRaw : undefined)
381
+ ?? clientIdentityOf(peek, req.headers['user-agent'])
379
382
  await router.chatCompletions(
380
383
  {
381
384
  rawBody: body,
@@ -156,6 +156,60 @@ test('出口:空的 finish_reason 不许冲掉后面那个真终止原因', as
156
156
  assert.deepEqual(reason, { kind: 'tool-calls' })
157
157
  })
158
158
 
159
+ test('出口:上游报 stop 却发了完整工具调用时,必须以 tool-calls 收尾', async () => {
160
+ // 真实事故(2026-10-15,traework 上游 + Delta 客户端):带 tools 的请求上,
161
+ // 上游正常发出完整的 tool_calls(参数逐片拼全),收尾帧却写
162
+ // `finish_reason: "stop"` —— 与 OpenAI 契约不符(有工具调用应为 tool_calls)。
163
+ //
164
+ // 修复前 mapFinishReason('stop') 直接给 kind='stop',而「无内容」兜底又因
165
+ // 为有内容不触发,于是已攒好的工具块被静默丢弃;调用方据此认为「本轮结束、
166
+ // 没有工具要执行」,表现为「模型能对话但工具永远调不动」(Delta 侧即
167
+ // 「test 能访问但 tool 调用不了」)。
168
+ const reason = await finishOf(
169
+ frame({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"command":"ls"}' } }] } }] }),
170
+ frame({ choices: [{ delta: {}, finish_reason: 'stop' }] }),
171
+ '[DONE]',
172
+ )
173
+ assert.deepEqual(reason, { kind: 'tool-calls' })
174
+ })
175
+
176
+ test('出口:stop + 工具调用时,工具块本身也要照常交出(不能只改收尾)', async () => {
177
+ const calls = await toolCallsOf(
178
+ frame({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"command":"ls"}' } }] } }] }),
179
+ frame({ choices: [{ delta: {}, finish_reason: 'stop' }] }),
180
+ '[DONE]',
181
+ )
182
+ assert.deepEqual(calls, [{ type: 'tool-call', id: 'call_1', name: 'bash', arguments: '{"command":"ls"}' }])
183
+ })
184
+
185
+ test('出口:stop 且没有工具调用时,仍然收尾为 stop(不能误报 tool-calls)', async () => {
186
+ // 反向判据:修复不能把普通对话也标成 tool-calls,否则调用方会空等工具结果。
187
+ const reason = await finishOf(frame({ choices: [{ delta: { content: 'hello' }, finish_reason: 'stop' }] }), '[DONE]')
188
+ assert.deepEqual(reason, { kind: 'stop' })
189
+ })
190
+
191
+ test('出口:length 截断即使带了工具调用也不能改成 tool-calls', async () => {
192
+ // `length` 必须原样透传:dsh-llm 的 assembled() 靠 `finish.kind === 'max-tokens'`
193
+ // 丢弃被截断的工具调用,改成 tool-calls 等于把半截调用放出去执行。
194
+ const reason = await finishOf(
195
+ frame({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"command":"ls"}' } }] } }] }),
196
+ frame({ choices: [{ delta: {}, finish_reason: 'length' }] }),
197
+ '[DONE]',
198
+ )
199
+ assert.deepEqual(reason, { kind: 'max-tokens' })
200
+ })
201
+
202
+ test('出口:stop + 参数残缺的工具调用时,仍判 error(半成品不许当有效调用)', async () => {
203
+ // 与上一条互补:`stop` 只在工具块**完整**时才提升为 tool-calls。
204
+ // 参数残缺时既有逻辑已把 pendingFinish 改成 error,不能被这条修复绕过去。
205
+ const reason = await finishOf(
206
+ frame({ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', function: { name: 'bash', arguments: '{"command": "cd' } }] } }] }),
207
+ frame({ choices: [{ delta: {}, finish_reason: 'stop' }] }),
208
+ '[DONE]',
209
+ )
210
+ assert.equal(reason?.kind, 'error')
211
+ })
212
+
159
213
  test('出口:未知终止原因必须带非空 code(agent-loop 会拿它重建 LlmError)', async () => {
160
214
  const reason = await finishOf(frame({ choices: [{ delta: { content: 'x' }, finish_reason: 'content_filter' }] }), '[DONE]')
161
215
  assert.equal(reason?.kind, 'error')
@@ -444,13 +444,35 @@ export async function* translateSse(payloads: AsyncIterable<string> | Iterable<s
444
444
  const usage = toTokenUsage(accumulated)
445
445
  if (usage !== null) yield { type: 'usage', usage }
446
446
  }
447
+ // 上游报 `stop` **却发了工具调用**时,必须以 tool-calls 收尾。
448
+ //
449
+ // 为什么(2026-10-15 实测):traework 上游在带 tools 的请求上正常发出
450
+ // 完整的 tool_calls(参数逐片拼全),收尾帧却写 `finish_reason: "stop"`
451
+ // ——与 OpenAI 契约不符(有工具调用应为 `tool_calls`)。而 mapFinishReason
452
+ // 只看 reason 字面量,于是 kind='stop',下面那个 `order.length === 0` 的
453
+ // 兜底又因为「有内容」不触发,最终以 stop 收尾 → **已攒好的工具块被静默
454
+ // 丢弃**,调用方(Delta 等)据此认为「本轮结束、没有工具要执行」,表现为
455
+ // 「模型能对话但工具永远调不动」。
456
+ //
457
+ // 判据用 `complete` 而不是 `order`:只有**参数拼全**的工具块才配提升为
458
+ // tool-calls。注意在当前逻辑下两者等价——参数残缺时上面的 `pendingFinish`
459
+ // 已经把它改成 error,`reason.kind` 就不是 'stop',根本走不到这里;
460
+ // 仍写 `complete` 是因为语义直接(「完整的工具调用」),且不依赖那个
461
+ // 副作用:哪天 error 分支挪走或放宽,这里不会跟着把半成品放行。
462
+ //
463
+ // 只改 `stop`:`length`(截断)必须原样透传 —— dsh-llm 的 assembled()
464
+ // 正是靠 `finish.kind === 'max-tokens'` 去丢弃被截断的工具调用,
465
+ // 改掉它就等于把半截调用放出去执行。
466
+ const hasCompleteToolCall = complete.some((block) => block.kind === 'tool-call')
447
467
  const reason = pendingFinish ?? { kind: 'stop' }
468
+ const resolved =
469
+ reason.kind === 'stop' && hasCompleteToolCall ? { kind: 'tool-calls' } : reason
448
470
  yield {
449
471
  type: 'finish',
450
472
  reason:
451
- reason.kind === 'stop' && order.length === 0
473
+ resolved.kind === 'stop' && order.length === 0
452
474
  ? { kind: 'error', failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE } }
453
- : (reason as never),
475
+ : (resolved as never),
454
476
  }
455
477
  return
456
478
  }
@@ -660,3 +660,38 @@ test('【隔离】不同供应商的同名模型,各自独立冷却', () => {
660
660
  // B 供应商同名模型不受影响
661
661
  assert.equal(pb.pick(list, [], 'deepseek-v4-flash'), 'acc', 'B 池同名模型照常可用')
662
662
  })
663
+
664
+ // ============ 限流冷却对齐上游声明的重置时刻(resetAt) ============
665
+
666
+ /**
667
+ * 2026-10-15 事故:6004 文案自带「将在 … 重置」(小时级),但退避上限 5min →
668
+ * 到点回来再撞,11 个号轮流被打冷。锁住:带 resetAt 就冷到那个点;
669
+ * 过期/未带的 resetAt 退回指数退避(行为与改动前一致)。
670
+ */
671
+ test('【resetAt】限流冷却到上游声明的重置时刻,而不是 5min 退避上限', () => {
672
+ const p = pool()
673
+ const list = accs(['a'])
674
+ const resetAt = Date.now() + 3 * 3600_000 // 3 小时后重置
675
+ p.noteFailure('a', 'm1', 'rate_limit', 'codebuddy 6004: 您的使用量已超出频率限制,将在 … 重置', resetAt)
676
+ const row = p.decorate(list, 'm1')[0]
677
+ assert.equal(row?.cooling, true)
678
+ assert.equal(row?.until === undefined ? 0 : new Date(row.until).getTime(), resetAt, '冷到上游重置点')
679
+ assert.equal(p.pick(list, [], 'm1'), undefined, '重置前不该再被选中')
680
+ })
681
+
682
+ test('【resetAt】过期的重置时刻退回指数退避;不带 resetAt 行为不变', () => {
683
+ const p = pool()
684
+ const list = accs(['a'])
685
+ // 上游报的重置时刻已过 → 退避(首档 2s 量级),不是「不冷」也不是冷很久
686
+ p.noteFailure('a', 'm1', 'rate_limit', '429', Date.now() - 1000)
687
+ const row = p.decorate(list, 'm1')[0]
688
+ const until = row?.until === undefined ? 0 : new Date(row.until).getTime()
689
+ assert.ok(until > Date.now(), '仍要冷')
690
+ assert.ok(until - Date.now() <= 60_000, '退回短退避,而不是按过期时刻冷')
691
+ // 不带 resetAt(其它供应商/文案没解析出):同样短退避
692
+ const p2 = pool()
693
+ p2.noteFailure('b', 'm1', 'rate_limit', '429')
694
+ const row2 = p2.decorate(accs(['b']), 'm1')[0]
695
+ const until2 = row2?.until === undefined ? 0 : new Date(row2.until).getTime()
696
+ assert.ok(until2 > Date.now() && until2 - Date.now() <= 60_000)
697
+ })
@@ -54,7 +54,9 @@ const SESSION_DEAD_COOLDOWN_MS = 30 * MINUTE
54
54
  *
55
55
  * 冷却策略对齐 9router 的 accountFallback / errorConfig:
56
56
  * - `rate_limit` 用**指数退避**(9router 的 `backoff: true`):偶发限流只
57
- * 短冷,持续限流越退越久,比固定 1 分钟两头不讨好要准。
57
+ * 短冷,持续限流越退越久,比固定 1 分钟两头不讨好要准。上游自己声明了
58
+ * **重置时刻**(resetAt,如 6004「将在 … 重置」)时冷到该点——频率配额是
59
+ * 小时级的,退避上限(5min)只会到点回来再撞。
58
60
  * - 未知/瞬时错误用**瞬时短冷却**(9router 默认分支 TRANSIENT_COOLDOWN_MS):
59
61
  * 每次失败都冻结,坏号不会留在池里被下一个请求再选中。
60
62
  *
@@ -495,8 +497,13 @@ export class AccountPool {
495
497
  return `${this.supplierId}${SEP}${modelId}`
496
498
  }
497
499
 
498
- /** 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。 */
499
- noteFailure(uid: string, modelId: string, state: AccountState, message: string): void {
500
+ /**
501
+ * 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。
502
+ * `resetAt` = 上游声明的重置时刻(epoch ms):限流退避优先冷到该点——
503
+ * 频率配额是小时级的,5min 上限的指数退避只会到点回来再撞 6004,
504
+ * 把号轮流打冷(2026-10-15 的 11 号全灭事故)。
505
+ */
506
+ noteFailure(uid: string, modelId: string, state: AccountState, message: string, resetAt?: number): void {
500
507
  const rule = RULES[state]
501
508
  if (rule.cooldown === 0) return // no_such_model 等:不冷不记
502
509
  // session_dead 是连接级冷却(登录态问题,跨该号所有模型),到期自愈——
@@ -514,7 +521,11 @@ export class AccountPool {
514
521
  if (rule.cooldown === 'backoff') {
515
522
  // 连续被限流 → 等级递进冷却;成功(noteSuccess)会清零等级
516
523
  e.backoffLevel = Math.min(e.backoffLevel + 1, BACKOFF_MAX_LEVEL)
517
- e.until = Math.max(e.until, Date.now() + backoffMs(e.backoffLevel))
524
+ const backoffUntil = Date.now() + backoffMs(e.backoffLevel)
525
+ // 上游自己报了重置时刻就冷到那个点(且不低于已有的更长冷却);过期/
526
+ // 未报的 resetAt 退回指数退避。
527
+ const until = resetAt !== undefined && resetAt > Date.now() ? resetAt : backoffUntil
528
+ e.until = Math.max(e.until, until)
518
529
  return
519
530
  }
520
531
  if (rule.cooldown === 'transient') {
@@ -1355,7 +1355,7 @@ export class Router {
1355
1355
  }
1356
1356
  return false
1357
1357
  }
1358
- pool.noteFailure(uid, req.model, r.state, r.message)
1358
+ pool.noteFailure(uid, req.model, r.state, r.message, r.resetAt)
1359
1359
  // 故障切走:下次选号会换号,旧块的驻留计数不再适用(前缀归属变了)
1360
1360
  pool.dropBlock(req.model)
1361
1361
  if (trace !== undefined) {
@@ -61,9 +61,13 @@ export interface ChatRequest {
61
61
  /** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
62
62
  lv?: string
63
63
  /**
64
- * 宿主盖的会话身份(dsh-llm `GenerateOptions.sessionId`),由 adapter 转成
65
- * 内部头带进来。用于把用量记录**按会话归因**(「最近命中」徽章要按当前会话
66
- * 显示,而不是显示别的会话的命中)。缺省 = 非宿主调用(外部 OpenAI 客户端)。
64
+ * 调用方身份(归因键)。来源两种:
65
+ * 1. 宿主会话身份(dsh-llm `GenerateOptions.sessionId`,adapter 转成内部头
66
+ * `x-dsh-router-session`)——形式 `session-<uuid>`;
67
+ * 2. 外部 OpenAI 兼容客户端的 `user` 字段(Zed/IDE 等多客户端场景)——形式
68
+ * `ext:<user>`,前缀标明「这不是宿主会话」。
69
+ * 用途:把用量记录**按调用方归因**(「最近命中」徽章按当前会话显示;出事时
70
+ * 能反查是哪个客户端在打)。缺省 = 匿名外部客户端。
67
71
  */
68
72
  session?: string
69
73
  }
@@ -55,8 +55,10 @@ export interface UsageRecord {
55
55
  /** 失败原因(ok=false 时)。 */
56
56
  error?: string
57
57
  /**
58
- * 宿主会话身份(有则记)。用于「最近命中」按当前会话归因 —— 徽章要显示
59
- * 当前会话的命中,而不是全局最近(team 多会话下后者会张冠李戴)。
58
+ * 调用方身份(有则记):宿主会话 id(`session-<uuid>`)或外部 OpenAI 客户端
59
+ * 标签(`ext/…`,见 shared.ts 的 clientIdentityOf)。用于「最近命中」按当前
60
+ * 会话归因(徽章要显示当前会话的命中,而不是全局最近),出事时也能反查
61
+ * 是哪个客户端在打。
60
62
  */
61
63
  session?: string
62
64
  }
@@ -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
@@ -321,3 +321,25 @@ export interface RouterChartResponse {
321
321
  error?: string
322
322
  chart?: RouterChartBucket[]
323
323
  }
324
+
325
+ /**
326
+ * 外部 OpenAI 兼容客户端的归因标签(`/v1` 没有宿主内部头 `x-dsh-router-session`
327
+ * 时的兜底)——否则 usage 记录里全是匿名流量,出事反查不出是谁在打
328
+ * (2026-10-15 实测:296 条匿名记录对不上任何会话)。
329
+ *
330
+ * 取值优先级:`user`(OpenAI 标准终端用户字段)→ `prompt_cache_key`
331
+ * (Zed 等客户端的缓存键,常带线程粒度)→ `User-Agent`(保底到客户端粒度)。
332
+ * 值截断到 64 字符;三者皆无返回 undefined(保持匿名)。
333
+ */
334
+ export function clientIdentityOf(
335
+ body: { user?: unknown; prompt_cache_key?: unknown },
336
+ userAgent: string | undefined,
337
+ ): string | undefined {
338
+ const tag = (kind: string, v: unknown): string | undefined =>
339
+ typeof v === 'string' && v !== '' ? `ext/${kind}:${v.slice(0, 64)}` : undefined
340
+ return (
341
+ tag('user', body.user)
342
+ ?? tag('cache', body.prompt_cache_key)
343
+ ?? (userAgent !== undefined && userAgent !== '' ? `ext/ua:${userAgent.slice(0, 64)}` : undefined)
344
+ )
345
+ }
@@ -117,8 +117,13 @@ export type ChatOnceResult =
117
117
  | { ok: true; stream: ReadableStream<Uint8Array> }
118
118
  /** 非流式:核心写 JSON。 */
119
119
  | { ok: true; status: number; body: string }
120
- /** 失败:核心据此做冷却/换号。 */
121
- | { ok: false; state: AccountState; message: string }
120
+ /**
121
+ * 失败:核心据此做冷却/换号。
122
+ * `resetAt` = 上游自己声明的重置时刻(epoch ms,可选):限流类错误带它时
123
+ * 核心冷却到该点,而不是指数退避——频率配额的重置是小时级的,5min 上限的
124
+ * 退避只会到点回来再撞(2026-10-15 的 6004 事故:11 个号轮流被打冷)。
125
+ */
126
+ | { ok: false; state: AccountState; message: string; resetAt?: number }
122
127
 
123
128
  /** 供应商模块 —— 契约(核心必须,差异化可选)。 */
124
129
  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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAgKADAAQAAAABAAAAgAAAAACaA7zWAAAACXBIWXMAABYlAAAWJQFJUiTwAAA+7ElEQVR4Ae2dXay36VXW9/t2ZtrpACXpB6UfVAKKKIIIoZSStlIUEwsp0VhJ9MCGwIkJBjUx8YR44seRiZ7ogfFAoxgMUquppIQ6pIhVxEoMYCBpGWxVWtKvmWln5p3X63etda3/ep6935lpqZ60d/s897rXuta11r3u+/n4//fe79y6+jy27/3e7/2qRx999GtF+Ybbt29/q/qvv3v37leqf/AcRnqrbt26NT06xrHRi8fj6Db+6aefNh6C+IXjrGMcLuTdbvKN/V58yeOMQ5+8wgtm8+y5bJ6tb/lx9R8U5tef97zn/aJ436M5/MZP//RPP5K4v9u+qv+7YPmu7/qu1yip71Gi3yeab1L/KuieeuopT5pi3NR2cW6yR3cTTjHuotdxC1mNxfVcPPB+sAkdOw3obCTk6BFiQ6adx6U9nom5eVp2jCDJRXoSMRYM8mqTW3Tbjsym5dAGsO999933iPT/XfifuHPnzs+85z3v+UB8P5f+c94AWvjvUCJvV1AW/qVKxovOwt+06EwmBaBPq/rcrRVpDLbgg+veBZPNhV02qzJOnNM4izG+5xiMTy0+Vu+8gztxTH7YGz8bID7dBzvq8DMZlD0PdVUvejbCAw884F6Q/6PjndL9/Xe/+92/hM9n2y4r8Rw93/CGN3y9Av41LfKfVUIPfOYzn7l68sknDzs7CW/KntNdrlT5evLYF3YKEt2puMAPmLZH5x7fxIIeJ7XY6Gm+PdB3rHCk6AWqwotuNi94xuZtX2OtDNllI5vXAJ3goYNk6QYTNbh7yfjFprvB1fOf/3zuEI9K/RPy+7u6I/zXcD+XfhJ5NvCb3vSm++6///4f1WL/FQV66ac//ekrXfV3udo7IXVV/O73xORSRUucYBkjq50xUygMwMAAVNt55yoNphB9Fne7H3y8GMTFHj7CdC4HjpMevh3fWPw27rnIOIJLS2zpnFNzTn7BpU8MNsILXvACNgJ3hL8l/d/TRngquGfqn/dMxthe97rXvVLk/0CL/ZeeeOKJh1h8Fp6mJFOMEZMYZjA9tnyBY6mGDoxauOLjHht3Duzd7Lh5rWh/+DhoLePWkMsmap0NkqfQyJ1PAk4+UDYvGEEvcRIAHUc46AOOPn18zr3sELso6hNzw6pggrEWWheSfkh35+8R6Ote+cpX/vwjjzzyye1wk1zZ32RpnW75f0i5/GMN/8hjjz02C8+kKse6pTFOy2Q1XmKsF/zmkNxzng0zDggdy/GRl29ws4BRBBd/kmlb5p2xIdg7Tuzow2us7NgMxZgGtUzhi799AYNjEyNySDaJ1N5tiUvf8PjO3DvW1luWfnS8Izz44IP4vE88P/jwww//cvvd2CXRG41a/O+Q4Z9rh7368ccfd2I9SfutZD0ZxqsxUA4uzBh6vGBTYOu2HXkXCkDs6mfHoEvrnFyQzsdQ7Bkj4rPH8V99SCf3ky16x8LmQEU6uvh0jtH7XQjdyiPQQ3+yxz+xwCYPz6fjeBNoMzyitfuBn/u5n3vvgXQNxnnpLHLlS3innvNfxS1fxOeCO5kuon06uK8i4Chl367nCW/bnpz5buJuzskbAsLYoU/tt7kdt33p8EGXHMfetoN+z6HCzaY91AAbWHHsDba5HZaT2rWcSbhM122td3eBub7mx5d563BsgNwJtAl+U+Jb7nUnuPEd4PWvf/0rxPPPtHv+AFc+DWL6FTwTOCcdKFgwG2cbyS59kj7YCLdiEdrjJj/4N5d1+DUWwZsWnuhtrOkc8kpO/a7RsOmMXRzmljUc5scuHtsk2xZemFqF3ldF64xHVosNrOu6eAog2ubxOKfGOWbsfCTXBniRMK979atf/a9veie4tgF+6Id+6P6PfOQj/1CJ//F95UMqHfFcaCZGUJp0DoxxN2z4VOc+uN0jmxNfaHvMMDjbCQXn0s+m6DjB4ytVXeEMnqHZp3mBTS4Vqngiyx7oIVbzDxf5wAVYrc2XrtSujzGyXAOBYRJqiJ7rhcF5MsRvcm67x/LzdzP63uDl0r/6pS996U99+MMfPnwzd20D6OPEjyjYj/biw+fgCMmkkxk9JnAElDLJpBgeNzi20NFjii8DK9akU6DoN4d1ISE/+DJuLobOXR2hPEbF+OQD7hqmfYBenBewHWyTOvz0ntdWYBfesZs3c5fJtkkyvMGbsDdCbB2vh440NWj8XT3Gb+mj4h984Qtf+KkPfOADh/eBmRDg1772td8o4Lv1Wf+lfLmzA2PvYOgtP5MdPK19DlXdesnOAc6lt5zT2ZbxxkeneI5F3KVLzofiNP8sxvZN7GDEdW0xsSUGMjFPLQrcUzNDTtipT/JODy0OGh844p/4PfZcwHe8Ges7nCut7Uelf/173/veXwNDu12dz7f0/PvLeu578dEQFOJzsO0TW/oktDCIw3PCZQNKnVqdPC8FwDAg8O1j3fIPJ3aacfQ3tFFi76I5DvKey/YP1sCb+Yc3Mbd/65I3/WFzLX6JXgOlUu8E4dt9uAGp2RRdcFzQai+W/q//2I/92Kz7FOvbvu3bvkkvDD8v4At5eeiPX/F3MYe9tQQjUHrUpwQ8ubanKMScqzQ+STj+6LUZ58rAR8fkm5jyA0Me6sbsvOCQLn47l8m5fYGOzoObT+GK9TAPlMxjxbSKk3STHBi1M1d0mcshX4ydq50Zotut7aPSmG9qjUPW+t7SneAJPRLe9L73ve8/AMxO4Or/ESX2QhknkMaVaS/yMLfQ5hTbWBYtesEcvHXKYYowdwSogsdRzWP6hQd2GGKPMj5WrFOHs58wziVY9bT5Ygs3xlbW4pgpOnq1cFRwjVvvmhkwU7R7fGa+zRe9QdLRrFOfeRLLBmx9AEKfPPCxLT0OVpavOVIH1lZr8YDGP9iY2gDf/u3f/hop3sKVrwYHLRshk0tQ27BzpEm2neyjV08D4kQyaB8b2n7mchLCTQB8lbxde0JNI3K10x3LdwUA8CdGHNrf+QLZOLg0Zg5uDNGpOScpPd5+8UfXbebLeGNriIebY5u8fOHec3buxO42tY3i3MPVMZ2D7MnFXDwKZP++7/zO7/wqfHMH+G4p/SNd9XA4IjIg9XMwVjsUDbuVjcO9daaKrD58Eku2Y5/Acajl1mo8ipWTN0Kwu8cPrBp+lnsqVnInQjj5TIyTDb0LLjyNHIC4aTyLga0P0qQxtj+DhZ2cwKj5JHvypk/MzJnx5N3c8NMclxDyywGv72yyJ2FBak26Bi/RRvgT4G73C8GfgmwXyB4ijaOwTkK9J4azbNEdZLjSkOFQs7JtScyw1nnR22+Ka8fiC+nBt/HuyFlcNC+ABcVNTx7I9EDpF/8hz9hkJ25iuheF/XJqvgy9KDM4CvhPHeBpLlE4GdtLrHegdk/84K1unPNjWn0kXw27cJc4+Ke2/B7H7Vv66PcqKX9Bt/9X9vM/BB3bXYJMYLiTaICt2/7INAcV3i8l/XOFscluWfabCrz5TGZCQYmXlly2LraND0468y78Oc7ekIL5arJu+UB9SeKyUazHZ8VDd1Nz3PxI9wRIju7vweXcto38TuNJBJteBjk+9NBDD732Pi361+r5+fK++lmAa4sgMnSHJFBdi9yYTEJ2cwmbwvEWevX2t7/9SsGzE70rK0Q8q++JmCOW1mXoHh3tzBH9TTbg6GmNk/uoUO9B5PTY027SYbP+pnzj2D1f0lz96q/+6tW73vUuz2HlkRoNl4mPec68E2v5J9TkKJsfodoAX6mf7v6++6T4o1p8fyOIYxcjCwa5VLOQDoZCDdJzYhMoiQp3WGDuMt///d9/pZ9XJ7kv6J6a8wL7jne8g+OuvonNorsuq/61y1Vzapq1Uk/Nsx5ev+UzmyNFbt/EeON9UnyLNsA4CjgLW9xzJUwg+cxCQ9hN4txpzBdD7Ipzi7fQ/F4Bd4Mvtiv/Sh2PRX69jl/xUqOWLJ4XKjLrYUNvgtROulmPxuC61wC3weDHmgvzrbdl+JpNhKMaDlanb+Uk1HqDWo6jA8fefOjsS2COL7ZLBagVB7/Vg1Y1S8vQCyIMbdd5PhqHTXaL9DkgQ8k4cuN/P3eA12BoAALgcWgnmznRFolxpb2cwwe0dxo+/A6hD3QLc3H8ApWoBTWhPl1vr0NfKJapeduoUm61s05l9joCTSXti1986U2gXrivvE+DBxsQUn8chAQmtfKoTSGod6CJIZKdzq3JjbdjBRlbFr7jjb68vzDP1I+6pDbaBP5toXtVo2tM3fcaSH24xdu21mDWFn9i0ut4kA3ggbq9m5C9kIt4SIJV7xbC7okbk9wvbgzU5jES0Bd6r5pkDWYjqIZeROpFfcB0k3hZ7NQ8xt3HZ+HDORfufX2bsR/MTcAieqE09gpCEjM9gWnRIcMVPWMaYw7Z/HNpqfxdAH45DPwCPuXqp9eROqefKyi1Vj29kJRMupFXCf3oxdxfkeeKlGu9SMrtFo+cfQfAP0B4I88iV6zLbd8gsqkNcS3R5jsvuPH45CAwjYCzA7dcSVdykuV4M+7kcxNuJiWsdiZn50Fhgt8ydo8LWD7UfOExpYXfq7L5B3B4Ozc3dehNIHEuNOpJbvMe1RQJMYwSdtk8H4zyz9qMjliY6OH3BkAgAfoAWk4SOLmhpwWH3DoMnje6tnss2ZugJ5nJGtM4XNwyEwYHWQkv8qOtXK/712TLmrl1v6s4OXSMIrpssqEPHzgdmfDYJSTH5O6+eY1vjvimJvDpcFqTTy8U/I7Xte+x10Yyixk6h2c9sk5giXFuse9HgHhuvOqFnY1x5jn4ALQCh0tLTayRfRYeebc9OsgL97n4ECNxE8/8izcXQLDB7TEcrjQ9NdmgAnI1jP6zyTX5rcXaNTxHYjxXsXPR2onDt/xK5Vp25iDOXh4+BtoAYRNlnB5e2zo5HG7ENpcTAcOBDgI11w4dPBr70Pj/WyPmvdoz2eITTProp38G/sFsAby+BTzVQ/RVb6CsCY2YHdf1t7L1wWiY2se8N9GsG26NrUdA0AlA7xUWIbbWo5M4d4lDIuEgGTDdZwLhST8TgntjGe+GQzRbDuYmXWznHqx+XaXUFDayNLElF0DoaBNfPrlCr+VZ877GGd9iKk5VZ3D6Jm5qQWzxZxF98azwUAhSz/XwKQ8Xu/MhnP3g6hb78G2OeQmEF+c+7JtJhqxjNe/1QPhSIPDxBRw5xYsu49jpORLPuPY3ZzlSBiQX0cVE7DEdbTjgZKxjsMQo0OQWfPLHbE4EGjw9L+dyzlPj4AzXKTjGjr90Wg0As6HAXJt7cYqm59sYOvA0jPFT39MqvMZZ/AKXXzaCdX4HIAAkuzGOPnLG3eMwmW3/4GWXWEkt3YEXfRpkjLYOG/q5clHwUpOiLH9MaeEYThkoeqK5X77B47/l8LlvfOzpD5geYKNOaRM3HBgk3z7Vvv1YpFk8dN0QRHshjg1V1OjyG1Kxt79dt87vAO3sbLexZQeFILbuIZNYeZJYMPBhQIdMA0sP3r0WkatNHw8wuhjn3jyAT43JhfdkmqGDNO9Z7sQ8H/N0gWeiYnH68sfXiQ9zTXTr4GAuuaMBjW65HcThzdzLx7TUiIOf24fHeZrWsyGWC79JnXOlm+8BCrxAwtjPtde8vQHaccEOE+D37ewX5wDhkrzrloU56MLfeLsD4IsIVZpgRcmGkDRZd3Gicy/dBz/wgatH9ZfKFAh/9Odmju2/ZLAUlLy6sHb3uInCmQkefGpQm0Qy83jJS15y9RVf8RVXTzMnYq02vDfo9dq+87cjeGouip5GXTyMsdFW3oc7xdIb16fhwlUcHrMJZgOcCgImwdltkScBiPHREd32Mb6DuwtHdATnqFzEJUNNLYhTT/GUN/hf0S9PvP/977/SP0i1C3FyuD7El5w/3+3LvuzLrt74xjdevfzlL7+6ozmRpwJNGKTDJ/GTPTVIj+OuK8MbcgdCPVJ/z42xWhY8PSmkvMhODn9+IURjci5795BYnxPjYKKjRxcfxmc/2ZLEwYa6fwvZBTM/BGpETrZW9Mnc8uNn5vrrliv9xas5OyahJB7zJk50yGnoMo4cHJjtl/HZN35ve9vbrt785jfXTzp1B7hEKY9kZE5d8SIfTC4CuJrP9TrHT27SzxVPmjpmAxCtcdFpeLnT4QqGBj+HXwJ5pnKgODfporQzhCSt/sbgjU9gY+DciTgGCaxg5izgaG3v8MjOUbGTK7deyUOTVIm1mlMiIextQ4dqwaqYrYATu32RddzkMzWDi/cZ5iFP03Dm5dVxmEdjbNTJpBl030FHS54MzNFa6Sxhiz49BuwcqVPrMn8740uu8wjAgRaijAGqqZugKAzVCaUHKBoTMHrb0DfBBVsOnK/FdKE6ngF9Mk85jI/eA4ZzYSvZir/tO1/L5MUE5csRP1Ohd0zl0hjPo2X798UwziZIrWBBZuElOgDjm3SGkk2lQ9zdzmPlQC7ecFwEapnnCjU6Ywlx5vQGaGVI6FPgzDu7J0R22UE7oY3P15IpohM0edVicvFV00WhOMR/pgYR8Tj4hUr6bqZPElE+Q+9iEU8++J5ddyKeh7iC8ViPsXJSDsyjuQ6LzJ1BTjUv5gdJ96fc4LYZ/fhIJrnWpY4MjVFc/FI3y5zw8aBxDO2kE3QccwcA08b0DFOA8Hj22fUGiCRG9UkCE7cYEvCEYqOPn4UalEhSo1yC9AoyCm5t8PApQH/7TgziqvOkBxcBdct7EZ0XegqhlgDRjw/GblNQfKhDiq8YxYO++GaRITaZ9CEVkQNiywuiOGnOsW/fHsOtw3opSIXT/DVUYqePPT6MkdXIceYg/eGrYBsSoHEpGNhpEDWZe2RafCWmmPGXaZ5lgsvcRUphRGQO8zRBNBTIX5io4ODw56BI/YulgaJ3ccyjGI5V+TgUerXB1LDO5C98uJz7ZbiRNVfeQXiRDQZ/Dimmx4tg/r5DvbELx9wUyPoTz140oewretdR0IrheLbWnJojdY+f57Swzo+xXwK5kraRWHiedFbdYHMxwXJkEoBpjHPHSNLoXZTtI1kEmOxjKYWUDo5tB8cG4B2gr5ZrscHQlNdh10uVRba98zrApGNsO6fkjg6ZRlx+k3c3e7Tf9nfuaz74xL6Ssdix1TmHmAXPNVp3ieQB1eYr6sNmnliyHTaHHwEUd71MDRkBOgv4LQuLHbV1JGWhlIiMzYFM1q1TV7ciC1KXqRcXUHNSrHl7BtzNdtnog2Xz0jSeK4YxcRtDDpMjNhp4+oL5agomm+VQKKDyCQZX569NMHHFZR22KULnmyKAwQYVsoTDxYBvcuoLR8PLVd/UiRPa1Dk5OrwdV84aO27H8EU1L4ExdjAwCTJyYw5FaH4ngr3bYGRPMtcWYr4Gxql9YTAZbtJ5vBIHmjn1HSAL4FCZB7DkA37LcHQ7YKK8oZ+JicfphU9Yx+1Jzt3OC9t527nn0ovquXkep7nt2CaucjgmtsS1r2IHDxabjtkEYFrnmrXP6PD1TwNDAvjcWhfDBES/CEcff/KRPPozN+MAwpUgti1+czZ+8fsWrE3gRwC5JB/6xAu3/BIuFOBlnruD7dK5kHDQ2p8BWFR26t45bCwAFpnNSWsfyxo4x61DBgsHfWwnzqljOGPPuAJcP29cW7NBHMvvAAQN8Cz32AXYwYIPKTgWg75xgjhvqarI8UkPNvgpTjnPle9CgutAMOIfDuzro2Aml5iM8bQbAvGkix2VuaQwEEipagi+m9zmivI81+MHEsjD7x4/x0OoPJDGhtz5JYeJbb9L7tZvX9mtS7+5WkeczCn8Dav5s1H3x8BJrLjB+rlnImavxu52kRcmpOBosxGQ1ZwEvm1zj8wj4HY/w31rBM+kseGphn5a2+Gi0bMB1Huh2YCN3b1CXa488Br7rtFYOjCisZt7fDjAb0D09MRVKz/5kjP6dqi5aOD3mdJ6bsyRSH4EXvidFzDmrLgmJidioB/ulrFx1HAW2wqUaiQTQCkqP29g+G56B9iOdk4QJSaxbpFmg70SCDl4zau++NgYdATkiA89+jRn2xiKRKNI1mfc/vj2MYUKb/hwJ163M24XJ58kjGkn0dWLIgItPOojJ4daUHIHlJjdJwcoDnbGwYb8MnbI1Aec/cveNNZ5MS/ug8N/8uw5mUPY0c8dAIBaDPbV2LzwIKs3pscjC2QgTu2DDbz5outhYWTPm37bnZw/F6/CmIcsLnMxLtRlqjuAsImPmpb5kD/peHOi96By3IuQn6Pjy5yAbbt1GLuZRvIFJ06NbJ55aYScA04aY8v4rBY/IDoyJ6k9B/wcDxeUYFqOHgCq3WKbPHCdOwAOIWvniVIx5uUHGGQ7QhIgYBJmcsZaWfzG4UrWzYuZQSWGDDXz6hBgY+cLIfzNoTzK1IW062HBMBtrwWLNk9jh6D5YFzS5MYH4QtWPEFSYiD+Pk3wDiAvH8PZ8QhS7AGRUVw+M4tQBn3td/a5l8jSiT3DQZHMNWu1Otmu62OOX8b4DmGuSEzfyDfwmV3KZD1zonLQHlZPH+C9OzNOSjONI2w6X236QyiPPUW095wRv52Y3cXhXwhW53Rki7qJ4Z0bHXMRlu7DB2SkYOIA0F4+u4JOH85aeWMb528uWrSwDq1ZDcfJLoR2anmbf9OU+OU189EV3bW7FfcM5ucePXA+/D9CTDHEK0fja7SsxJ5zChBR7t2wg+FCOATt+fr4Lj888DlLAxoCdmHAfC+YcuCp1CDabbWJZCcmlOR/Fj0niLEJyBW0OKZh4yRrwnokD8bJxiJvY5AcJLT2Fxp6mwjApD5k38Wn0cKu5dsxLsm3rZLvGJui4gCTOBmVy+FrXviHquVTceQQIZBIC4pyWsXS2n8iiCznm8Ex0OHQEa4rWVSHKHscpjsnsBqs47OmZ+gdBX/7lX371spe9zD8PqA8AnTep9kWzptLelF7FyWi+4MM3V9pl/pKAT0th79x5Wl8Df9r/4MW5XgbXArRYBFWCyj/yJY/S38BlZ/C9BpPLTQIYNZ+2HR3czWMT8jwCpCEQ+Xj3RVbvnURP6yTOZPZJAHAEpMOFHtsaI0MG0CAnJlVu9WW2zyQWTHNpE9zWv4n/fH0PcL8/rpWezVbUHRoqceh/vnpLa2UZnCU2/kc2+lcHLenUM5ClUmX+/uRCLvwqAhsP3ejhlC2uDNOcP/7MGQx+MiLT6HVIjbZacBpJNG5qvXDjgz+eUeC0ZMcs5op3+CIozg0w0SZAD0aHISSw7cgyJN5FkA6TDN4IyADPt0a7t63xBLy60wVxvApAXE/mPn2PcP99z/P3CdjTkDy+qFRs5atx3csuBkvoWQ7+z2dzT9HTMWUwDHj94SeB+5+4SezkNY805ckclIx5kE2tkedb2sq15XD0EFwSyYVkE/qOO2uCATh67OFI3ypMTmgeAWjUwMm3k10DjDFscnTtt2IUOXrMTcNHsPlVZ/vIPhkSO3Hxk1P7mzdjBsh5PvJLmLef5jdxpePKZeIGuwDSO3xxSH9Xi4eZK97BiYlCMLLRFtUC88/l2gWkZefpFOHlO406QBGTRr6eVw18pdugk4pUuYFpLHPgIohP12pcIqz+XGt85VZz7b5SrQV2YsLQZka8v8Ap3bVHgGNhCGmCE8Us1aN2MmBp2HpRJlBz2I/Fb5zxHqzCNdY243SyjgKNlkWWZ/tJ8ILS+7bdNmlRuc1V3QovfGThnX/zEU+KjgZn2dkyfB94KSHkwZGOo5iLBaUONF5yJ9dwdW2xg7UvcbspHw+c10U3uThH6bFLPmwIdO0XDpIENgEQw03vTKNonGcmnfvoMqRH1/pO8XJForjJjl98wSBTgJH5bdoKaV30JGF9+zOmwC6yrsKdBzLYE43Hnoy5mo8A3ihdGzlxawdHuYqnvEBYCkmhJpZz6NjGwqX5mAhf53SZr1Ru3iCSPL9SieqyQKjathdaKl/2sR16TPGDa/GRfU2j8wHHbxMa71MZEmyIGgNkWvwI0A1hfKKMLckc9CuRQ7G0MeDPcfbJpOplSpvI2PydQaGv5zyrF0BVg7P8aSq9zrWBPC/rK4+uXXV2qbgoVg0OOSf/9Mk7Y+bsj8LE7jpyUXTulQyJVaskDa0rv3FYJdYkEqN9vJZt8/o0zDGIlXeAyypWIviIC3/32KOwcwfKZrFO2JlIHEkEnu6Nt6+C+0WpJ4xOJICBujDWIW9e2dGbQ/q8UetmoH8PRXeEvMBBJ53+H0rz1KCv9H4UKOtWW+p3COVCOiSj5gn05nia5z93C8VPbv7Sp/MZH80teQbH2Hc+YR2UuSB3y9w0vNSq7IwBht4e6ORjHR2QxWE89ug7zHTz18EAOGg4qJtgcSbxMsnY2PToTzYNnZdf/sDJ/65+dFtKAhGz4lXi6NQqi0sMO5SpcB2rUshCcOXoi5N+ycPGvqtJcKWxuHAqz34BLCu4CgrWc62h5Kp22ZtJWC9+Y6YmAqUWMpVcSbgu2Hy1m8wAYNVa1/4VSBbVj9rRuwTdCzZvI7P47XvIAYqOQGe/prIa7v09gJUL4F1j5ToRaGHGJwnERh8doOiRCUxjpgJxLiw+TBjF8g8OPn72f2lZfP4aR4sPoxbaDX8JWm6p+krEQLwKb9jlhHfXqynMhw6fmJwvcbXhHOHCkHml3zWYWti/fFRgzxt8fITrSJ2+S1M1h0OcsbumrTPhxNBo6ztDuyqOv8XEzrGreSZ0/RIv5IyXbEwH2EkIUk8MKZOwsfhyMGF6WvotY7ejbulsCl76EjsyeDBNYxwKs3Kl69DfvwBTI2+d7WDmUutsPb0wtgAyDh3/a72xsDMHHgPM4fLWn/yYT2SBhakxOoLVrNmrFxyYUx06lbp9x24OsajH7ixl87D9q/DFR6i5/WPHLVzklncA5EmggU6AHbOCgTFpwWu3lsrzso9s7pvH1NExYHFp7Wd5TiSuJJMoVwnNG6KT9yeA1vG2rf8YTi9+h5cL5dE2o1TFRXr1f+trSS8LS0a+h+AIkE+tvLqkz47ouH4MeB7Fn3w9J+XM4tLQ09DHVharfWJu2MJR8Kyj0U6q7chzd5bOsrmLsgJ6Eq3gWSgfHYINr/2uPQIA4QaQngD0aibpQNkUZejkPQC4Jt+yf84ePX0mnZ+Y4Tt2FSRhOxkSATCbBzt4soQrt/l+3fSGKI6euzrSgsa+mg7+3gI9Q29LYbQcFb/xdqxyeIpc9WnmECmcnhObosfcIWiicTOGJNQqN4s1j9ajEafMTsqLhLx0DZkN09kXV2ONL80h1vA2yeUOwERaWZErqHUhpVfzqfE4MdZwNgwYTxCMfDJ/68Fl8XHMbdBGnfDhmAZHD8A+r78DcE4qrbmepw2gH87ol6i8IVhUUg1Pp9Y5XfTg9BN3kvAjhoVnOjUhNmG/V5i3H0nwsuH0bWFyJRfno/x85Z83Qdcydy6ms329geWDrrmcSMbgw4+sVin2RgGnVqfLOtRksBS+pMLh73jzKSDW3SdoB8ht5xqxcE5IEzFxONC3r1UMgWYD5DaJER0FCp4eLD04F68maj1z8jOYwvHip4+Anjv7UItaa9DpaOwEbUs12CzSyxXa+vpXuqqj8717t/4Laj0Qvn5GQE7cBWoTHN9PknfFa3IT1ByxZ57BFp/nK9E/Z/BX5nLTFC9/90c9aF0XD5ZvGY2gbJf6SaVE7Md6HPz4L4aUi84sQgeBIIQ4R8a+b0v2JRiY+NJvHXLG9NkAyAt7iQ8fHLWKE8N46SggtWDMwfwiF59dSKkEzwBZQv3femNra8jfE7AeOK1iXO4muZmR/xxyhIfmfNVjyyLP/CqAcdjTknf3gtfdpO1O2spSkFpPKgzal9VcA/zFtTHjI71lYRCN8S+EoOfA0AGuLTLENHA0JtG6+KC2EXIdvtrBc9i4enTDSUEq8cHODJYPeIp8+/AXwXUlkp7tqk98mSOyE3T+lWrFJS+yqgWmrzwvedVspFdZwJpfgq9+XTheyNZju2Nb+ac+5kRfBeDs5vlL0qXuuGtTeEErl0p9cymO7wpydX2haGx4CTU64b0W6PoA5xLhd/gUgCWNCTUxHJETFJiDYEsTfgZ26trHTg8nE6Ln7jMOZSzoJV6N249Y/C4+PQebYQ7/gItbtEtdlWNRJ0AJLMVscy8eIZifzubtzY0OC3FMqZOecPgn/7v9HpB86N1w0GG30hzOzL14hZEcvgZ5wZpLHfELF11wB1INmndmzBi1jrMOvdthA8hhAgaQfiViTCdjcwcKdHQGtrbxTgZ8DlWi8FrM4azEhw8stvggZ8z7Afq6SmquUMLqxeckNZ11OmN3CMP1TtJLhR5U4TDqKOVRVK7Ey4vgzse5iDxfDSfPzIEIyOmRczTWRsnOpm25K1gHhY7DBRhOEx9Pg9uYxPQvhOwdiExT/AQVtnJCjY0xdjXGE4CcGwvM7TQ29qS70WdjwouOZ2u9A/SGYCH4WMYP33QXoGx3/dm94jtBpc1tnPThuuPp+KTkPSWZ4Lv4lGSXmi9TL4pefOJOLezPZqyN2LnJnZyJSZQ9p+D23Arix+cBC+bURhHOxEkf/E322MjBnwKcoIJ0Uj3NWliBa6WZhI5FmCTqtiFgbMH1WF29E8CFjTgcNN/CLV2KFZ5WH3g1cB7Y6uoXF48S/khYi8me1Fmy/+/CE9OLp5Mnl8yNlCZjjGrA3TAxZ620omogBTE6f/rkY5feQcm/6UwVSvDRg6MOq3d9dAIyF5bs6K2TXsMp+WEQgzgBYHPYliuPmpyhOtUGQKABpGPcAVtdhizaiXA2iPxkcmAnpisVW9emegjhySFg/QRPeudBLhXOxa3MbCy9PwHUZoS4Po5pzNV/xxppq/lzPHXrO0LNTjaTrl/yUGrK2mpLzgFcaZ3OJML86r/vwzsM6vwyijczvjQ5Mcd8GmCzlrrqkzq5DuLpphL4YvHfGjTGIVJHCqxWtSLDlunDoT5rGB3radk1bjsKPwLUOzAKAAI7KGO1JDAkKE+E+DlCB7BPFyDcm9NxGku2DmL8SnTHcCHZOJUfKbjAT915yrv4ln//QimQBZHUahol+wKGW1dzeGu/sORSg3ceIajFh6vwl/S90L5ycwcozDwCCKmNSssce+D8kR1F8WicmbsaajfGxD34x9hr0kP7gBN+/E++l+SPvnUHULAhgXQF9SWtsQnoE0Q+I6MryGQYGFzZjROjJ2swfnx8ynNds5jHAjj0NHBVLmpbPxji2e+fBejqp34KaiwnZF4NUlKPNWQmgW08PteaAyZqbRSygOOONt7TTz9lOTkycBwAHGq7Lskuc058z63x0uEYKHy7toL6QsMeuYSbcc4HPmJ0s290+RQwZOJhNw5oOe1ksvg7Wcv4Q6bmSTBuzkmUhZXd1I0//AqVHFxACgt2ML0hsgG4FcMDJjXDtfDQw8O45ArJAvpGr6u++vhWRmCjKZlF98uieUSpuwgx+fqZTTj5CE4+57kxBuN/RbQoPQ5210Oyo2DTPNRd6oicWuwUrezY+LQNX6J11pUbdvAY1W5lA6Cb5JFDEgKB0aVlR+FnIhkmcWTw2NohSYCv4qmA+1tICqQELrfIxuG/YzevF5mQfBS7o3u5F0k+9VavsJLzUQychmo6KQZJJSEbrOhUVW9zNdxg5NXIYV4EtQGYE4f1stGYm+fEgHl5kzKo2MGe/RirHVIEmyaZJljFa7zNkWUnH5ode+wcBZy1Q+8NgLBbj4litcYVcYFikyqLnYW6hg3/8llMtcC7QF6kjg0Qv2QIV/H0YmrM7ZhXOn4djHsB2Fzb+Hu1mQoGHVBf7hkB0Fcu9J65T+gYNyu+vgPwTWD9QIh8nKOAzCNjeGbMACI6iyVvv65TBTKy0ij4lNVZCbtxDEVVCYcTRcGKbJ+jn18IQaFjk87CyhHZEU4YOGsmjQFLAhzIjUemmYaizIKnKOor/QLmLIfiUh85/HDwbZwudW2CffcgKXLAJ0VRms60xuYiSKUpQVh8CtSGxHbioJmdc/cc+BSQ/GRCpqVHJlfG1mHv2kRvPPNoX1yQY2+9hsVjvJQodIdxQIatpzO28ZVQc8rG2HZ4mcP5DlDl6GCQ0JrMP8a1t7VzskpkswU7aXX+lzgmOWH88YbANHBwN2Bu/9jQ2SY7/xbfPCLkW/ry5+rXV0PaBPq/rkwa55opGPaHFk2ank1dldo0vg2A7Bzw6SHSsdlYKvLm6udOQADyoTGvzAl9PrGgo4Hzdwc87tSKp+YTDL0OHATvjPFrDvxikC6i7cGnvhjV4mtO/DdX3gHQpw0wRBhEdIkWZPcQEql7dTUmeBq6NORwI+eInf5eeP6cms3AZ+987PJKwtPvEcoWhulqu7UuQfKs0Jg85a1Lz4Py7e1intFDSb4sdm0AZPKpnKrYyZ3+UoGak+faFwCpEJuDuezA96gda0BtXGscbmqNsYk6N5dTka//gEdGc5zvAJ7qcqriKDmCNhEcJtPYcvT0wbXBYwUzb+PmSkkcYZ1s+4+MsG0a2JZi43/HH8Wq8Leeriurwhnqk5e+XFlBSLvXoP5fCYKxXR1zQdt+VhsrPe8AevnjDkB+YDnIZ88JH77ioZ8mHA0/Dn64Zbk2BUYN56OzbXC3z4EKrA7rgjGw9fDS0iOrdl54dBznd4AJoInMiuOoAA4mJ4lVcGRsEKGk10FzQXDDrmZzibXzkMHlCKdv161Hl0ZhabkD1Bu+Cs63aLqdi0nB+rLuRSO6n+ri260nQlZWJ0rytl0Wjz2FaKQUODnz3kG7fVt/nKq7ADlioyHzh6uM++o2Jr6ZW+YdP8Z9SGUupxc/uIORbv6NAvSr4eOFwg88DZm8yDXtXr8RZGcccaIRLE7RZZwAPdbwErB1crG/yUgixSIZ+MLJl0L4J1HkbUfGh69+eZ7WFzJVFG8KBfR26HCVCZpjm8n01jFiphg0qJ6/Nxi/Ioaq8tXXAM4tfyK+I3heWyH5pjlHlzmqz4W060jYpCzIJEqtjKfbGGQ4aek9OI33D4OGOKQohL9U4SInGS/WOUAHYhMRfBJpLhdiJq5FpDlVZPngZ11PABldjtrBbBJ9BMRFVz4bQueZrBng8WKZztkXc+EqDotpE0GMLzTIyq3UbMwqB/PFhzsAX0YlL/x2LQrnmRmTTV38dQaDnqPyuej3GGqNSaqmUDCfGzcbQZwNvYCI0ziJlzXZj4AhBkwDeCJHnQW1EWzwYBs/OI1lvuxYDMEjP6VbuP/BxebBn2Kk5cui6MGyAe6/n/9OwH1Xn/iUYvIdgPyUibjjSc9A00LJ7HqI5dgWJngD2omOjxlqxDGPZNPq9s+/E+D4PYfMz3cB6dInucwpevA5iIHsOJ2xxs6gdUDcFs4+KINtGR7POr7xyXgeARhipKcF5IFO0SOi09ibABy2jNvmjhOt7ZZZYMbZ9bFlfRifWzYFtieffPLqh3/4h6/e+ta3Hp5n+ITDsrB7DviyXZSpe9b64LAIjGWxdxO5fU/6r/7qr/ZH1eRNn8OUjluhMmf0kYNNHVdIXz0aO5Hwn+Y0V+nyG1E+nqV8MpkD19wB8AiGXo56xNb30tEH06QOItm9TnNHiEJ+NKii8vpkfO4B8VKXz8+M5e9iRsb+0Y9+9OpLvuRLrr70S7/UtlxJYKpWFS++9DQKDvZejXziAyZ+yLG5mmuMniv64x//uDHB0XMQj7ij7xy2PTI4qOWTxWK8c5rFxqdbsFJV/j2HuvXWJCxjJx+pMg3dQ1cLaRM1zoBZ3BNGw8mJIAWu3ostRRLEZgy4fdhJJ5LDKb1AxmGnOPxd4JP6Uuh//tZvGQdHzQVELRK+tCx2csqMzz4G9wku/OjBhQt5+yUmPYtPH932Qwff9kcXLveKve0a+s1esWcBY8cW306ZTipf5amzxz5VUrMOrqsM7Wv8bIDod9+TMtdE0gTUZLpMpAmna79DUdro3ZKixIGY8UHO4QUgX8XCnmcn3/nzCcD/znD7ZsHBhQv+nrRDkfnZTiwaPZ8i8lu64UAfH94/iBM8sm3t3wZzxT+9beuUueTLrM0rmIuc3OIGF7rkFD39wtpXqvSG3WC3/vAOgCZB1MvH65Vk5lK/Fxn+scn/2m6FDwyTzYSJR8OP27sXt4ueAss4C26c/B0HXPvm+wOT5QSnDjbBcDXe/i0Hzo9ryQcbfXJ0/ICWT3CYvFnV4xM9mzSXGxjavmOA20chZg1w9fSUi4sEdjUNPb4Rs+Zh34yXv0XfLzH2BEwKsQ6pL8UQ2juigw4P4ymU1hEf2tINtgW51MRj2GNkOMa/sVztKS4Y/kSMAo9OZOG5o8cELV/ARD9Y+e+WnIPDtmUlZDj+4UAxfrYe52xs5w7X9mv4xIht+ISXTPMmwL8bQgaVFAqt1SHf0tkl+uYY/x5ffiewFdktAL3g3TvZfTs1u074KVH3YFHRn7DWSe9kmTCNK96GLjA6+GJ3Tqv4jMGza/n4SNxUwVgKhY7NAVYHmFx18NOit0+Pt4xPcgCPnCucMXba5sl/PAob+hzB2qFPiQVn8ImnsUsSblyCQZTeF1nbM/YG6Fj7Tn3gKtdZK2cz7wAeXU6zkASidRKeeQf3JOOydIJOEKnrto998ZQvhUyxVGT/MKcJw8cQmcMF05grO3xm1Tg4vUGNvDdhCpzv3jMm1+RLT9s28sviow+GiIW2i0/JkwHy5sY3PLFnTstPYtXLhLXgLdYaxDc5azybYPuYKMnKsMdLfflZQDu7E9h9JhD/jUEGt8lQaezdiD3+mryTRIUPR4ocnMDWz1hCCp580oNJO2+A6OkTJxuBHs7Ng5yDfGOLTH8tD8jh6Q0DJi1cWezMMzmEFxxy+OMXruQBb7CtuwRr28Ygq9UCIihOWuTEQH+4AwgAOgFmIQHGCUjLc6uJXTaZ5ooyV/AY1IC6oPFxgQgrWwptkHSVjke+erBzO9/fvGGNH/zmk87fLpbr6MIXPGZ80O8DHXGyYcBhJ/vBS+ZRlBdHiW7xSR7R40eLnnF0K/ah5vGlB5MWOf7q58JqjIM1zo6S/VgmvmSvDdjZAEkMI4QJFsLy8cgQxsRFg0vjeujx6OKbnlgcuSpE5Be6hF25XAreBUiBN7ZzmeKSRJ772BIXn+2HHBs+iSul9dseHC+eNMZI4aPPvKIDk3zDjS686BjnaF4NXVbBLl/jtg7srA86xqSB7zM0uBxHGE2hvuADPxuAYGoBJgg9jgRIIAczEIZu5NHYSbIJQWSHmhcsB4tEQ/bHNcWiYPDQUtDIuweTAz0t8eMPb1pscBKDRnw+TcjROYyfbMjY4YieHF2IxWsineBNA+9x94yTy9jgkN5NcvyFS30llhxfsPJvp6obY+xbDcy8dbIMGUPwamOeDSD7RXshyK+BjUML2QweNvllB0kbHYCOjegYTHbpnHxn6UJg2xuBcfAkHzlFg5gWGz0Hdni9uXrRz1z+t4ZlCyd+tM0dn/BjJ7/4pN86MAI4djiDs63t6MiBHlw2ZzDSzy6W3YsdG3js9Gpdwly7s0EMjy90jbeeDfC4jgc90inxINYwpJirMghqwWk4OIiX3oGio08DQ4Ez2e2TwqPbR4obLP2Zkys2OGyJvXmiSy70m/Oc18YjBx9O69Cf8t04MPGlj+/GrBx8i9Y7jIMJP7UGU8PLxQqf2mAkC3bZMy17jfANtvWPcy/8cN+K7diEUlez8iJbamfLwpOBI0rvbBrupMAC6d44isyxeZpsCoUtvviDp2eB0wcTvmyoju8uMcDQ4uNBn4JJz2d65OQNLDkk72Dp816wbbHjRzOu52SFTrHFr3FS20cuboEnn6l3480tmdp6gdVT++Csg1O6YHOH+yAb4Fd6A+BEk8pt7yo7JmFZB9d4+7bfEPTYYHAcmSxc4Ystdvy2PYu+8cHgs7HBYCcWDczeHDse8sbgDzYYbIkf+cwVf3xuyiW6nRs+afjtunROLOCsiXS0uLhnrCO7JXjbOtbocFbzvAD0mv8Gj4Bf1I7/k0ogz5JdMJwOG8HsXp/ErWeOAqJIhuntC0cSonh8RHv00UcPH9Wa1wUk0ZsaHGcb4+a+yeVz1t2L96x/tvGzJUDu3HHyH5+Aj9Y9A0Fc61mHKMC1DdFtj5MbuvBGJqbaL7IB/r12w10dt/Y/w9oOFbkScABOIVbvRadXc7KSPQMUQG2QCjWqJ5544u7DDz9864EHHpikhrgFcE1zNnn8bPaz02eLP/v/vx5zQXzoQx+aua14St1LsFSzOdBNfZG7xrNRDOi6Sx4uFl/rzUewn2UD/A85fli/ZfMKNkAKzy1JHjtA81k1OFjx0ZFM3aPriN4YOGt89elPf/rWT/7kTzL8YjtV4KGHHqK4aDlN3XpNrq0FuLOt6+71aS74XHts3IH5jSqt7/+S+tfZAB+S4Ze0MK948MEHcxXbAb8OAKET2KSyW02vg5ZN4AQwKqBcfEUbo012i80nU3m0r8bGRUkPpn095NQg52llYTxuvLrajMKOXlgXF9vihMINHc3OOHZ+S3/ORSb7ZCJZNDhi25Tm79PkJaBluHjzJy45qJlDfepwyAk7Nvw6yPCjjw57xuh41HMRSvxlHR9iA/Cm9I7PfOYzvAc4qMaToOR7NWNixJFASYrAajtJK7jdceQOQ0zh8tcq+IiiFpDd2nypKLZz3GwK++rkOO13LT5pghE3VKQ9fvFBh7wbOh32wV+HAdI5RmNNhgo9uhXD/lIFbwwwDmrSLqZKfALiY6VOjNWMFT56i+hjA4+8mgfUWxsA9b/U8TQbgPYuPRc+IsNLdBeAPEk60CKSWHNbPf5JBHmSID8c1GxnzET1wsM7B7b82JiQ8QvXnnh06RMnhR6sOG3jBCdt60pdm8DGOgGM4yEGZvmH37jO1bjkXTRzPtiSh6ypa+zmZaNzqDYeg98+w1qCTJf8V262xi96Um+dY+pC5/n/2wK/G4dsgN+U/A69mb/9BS94QRzomwfxsilwVMNm0h6M0sJlUwTjxeqJmpDkOM5NodDbDztj2pYZd05ZHPh9i4uJHty5wR3OZQOLQebLvGLv2IkV9TP2ywdccsmEGVuXDaCevGLHJ/HIibHbkq1kLksXmOuGftsee+wx7O/U8UGEJIX8Oh3vefGLX/wAdwFu0TTI+3Y9hDZcTknyorlBSiLq7+olxFcCunDHBd1ujMlBLQb7orvHxAcXno1r+ZlyvslfaVg9sW/gnl11nkOwPYeajEDKhebHXy4M3QVsb71/QVR+8UktFmWtEYpT3MzRvfj0Xzr9DL9R/Zhw3yH4+/HJHQD5P+r48U996lN/no9oJKCxJ4wx5N07eckz6cYcxrjpcPJth2d+LIlOE98Y/Il98MkQX7lsvG+d0kN1LWcrS+/84Wmsc4idfnE4R1Q6kkfw1gmb+dt15yvZZgxp0YFLHNkY+tS94eHG3ljjkMHtXpjkaDcT4Fjlm9zRc6F94hOfwP8nNPxv6GgB1ejq6uskvPdFL3rRi/lIsuwjdwKHQgh3HidR9CmodUrEycLTx3CDpaGny0QYSzYO3fmuARYHtcN84iefFLNQN5zh7XxSwEE1z4yDlWLHFaz2hfQ35WH/m7jg4+Bi4DEAhjtCN897BhgXP360UlvMybjY+eJNf7vAs/+7dcwG2HcAHH9Nx9/55Cc/+bd1F5gvaxZ5gqeg7hUkE44eLpLiysFnWia4FxF+UYQ7kzFt28wVDMVZergdP3aNZfZiOG4GsjNmSLzkPGAbrhd35zVy55C48IaPGlyCV8DD/AB3Dl54xqSTnOBOO1E19HInkOIQrnmtazBfvl1pTRn+TR2z+LZzOrX7Nf4nelP/M3ofONxiwSWAxOSL2vplQ7WLE7/BgU1r2QpIY0OEJ7zqHXPhPYZHukPRo8ImEF3ahmZBdw/ODsRpX/tIH5z12NN2jPYbLJjFFZdDj38f288yd03ZMgnLHdt2yWMHBl7k86j9nd/5HTbBv5Duz+l4cgcO6dYhv0LHv9Engj/Mf6Id0hsmkOBTJBxJdF/dUk3CMo0PeuAkT69j4iDTEjcy/YkblRdfHaE9Tt82dxgr1GURMyfw5KEGzBw6Oa+2mUOnGCfnBj/ruHkPm7l9PU/kxpCHJ0NPS48oDDm4v6kW4NPw40/WHn/88f8k3Vt11PfNAajPhJZqxG+U9E5tgldnE4xlCQqSpJbWootCEj2xsTOOfvfIuf1JNi+9HJ1n/IaoBSA7huSqnOzQtH14UAtjzviq37rBAgOzG/zAt+4kJ/fJKxzwtf/wolObfBiAbz3Da2Mr+7S52RT9eLr7sY99jC99HhHsLToOt/74808s36v9bxnepy+I/piOFz3/+c+HWLFq4iTHQBiJh+d3dLniwj8FRlGul6sRXS8+tBQD3jphrEWjT+GzKof4BpBQLVpVtvNrvkCSQwq9eSOD9XwQ0ggYOX3HG64TZOdK3Pi7hy/+4et+NpLG5mjstZzixxrpi55buvKz+D8g23+O/dw/0wYAy+75GW2Ab9Ez5JV6L+C/ZbODHyYi7LbhT7Muk6Ywa7LxL2AvmnYxPsa1nwshfQqVONuf2sRnF87cOtlXfNdsXDUrJ+N7DP+OgS2xTWWwU3XKDMGPLTy9cIkT8PB3DsPdPI7NvLpFYC7m8KRjFE5rdUtXPs/8/yL123Tcc/Fxe7YNAIY7wb9Vgq/S7eQbCJifXUufhMHRSHDrRl6TsK7z9xWzbEVSCiYpqe4CwfddIoVIQe3X8dHZjk9zD74xk1c7EgjRMS30uOM3zCsbrvSG40tTP9wd3+MTT7DD23YvLMaVezCJF35gBxvf8vHM1x3gx2X4Czp+PYB79c9lA+DLZ4h/paQ+pm+Tvlm76yG+08+3VptcmEtWXcTYT5MyjrnKPj6NnaIxjt+a8Pi0Tl0FC5ae1mrL63SO59XDjp82GfZrmHCCg1fj2qEo1Fqnrly792Ju3y3jxzg+0KCjSSeT5yGx1PFthZVak1t8yaMN8Nuy/w25/lUdH4fj2dpz3QDw8N3wL+h4t3bYg7ob/F7dbh7AwFWZBOlzyOQESbrtuf1qOC9DYA6F3H6xiSO+cLmosrnBRSNOt0PVosdN9gI3Ht/2P1x14Qxh+uhPfTjDv3ONK33soxOPdZsPmYMpCzjc0at3rny1q8/3t/Tt7RNak38q7A/q+Ckd9T2+hGdrIX823E32b5byL+p4i+4EL+Mlka+QuTPwiyUk220WNwsRffcGyraLdigUfosPt3CCc6Gwg4NHKg0nPpBzs5+UiY2D/fBv32s24Y05kzEmbGJuuX2AhC+5ozv4WaETPHDsxliL7F8f0x3Y3+trzDd779Dxj3T8/MY/V/kZq/QcSX6PcG/W8ad1fIPuBq/ijpBNwIb4bNqpeNdcn82Ow8a0fF6483jibN/NddJrWOsYffoQ7fGWY3+uPS+HHLrbupf8W/LlBzks/L/T8UEdn3P7fGyAHfzVGnyNjjfp+BYdX6vjNToe1PHF9tlXgL/Z+LCOX9HB2/zDOnix+00dn5f2fwGNEovHhSlHVwAAAABJRU5ErkJggg=='
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 实测可过。