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 +2 -2
- package/lib/index.js +32 -8
- package/lib/suppliers/opencode/index.js +58 -30
- package/lib/types/router/account-pool.d.ts +7 -2
- package/lib/types/router/types.d.ts +7 -3
- package/lib/types/router/usage-store.d.ts +4 -2
- package/lib/types/shared.d.ts +13 -0
- package/lib/types/suppliers/contract.d.ts +7 -1
- package/package.json +1 -1
- package/src/index.ts +8 -5
- package/src/llm/adapter.test.ts +54 -0
- package/src/llm/adapter.ts +24 -2
- package/src/router/account-pool.test.ts +35 -0
- package/src/router/account-pool.ts +15 -4
- package/src/router/index.ts +1 -1
- package/src/router/types.ts +7 -3
- package/src/router/usage-store.ts +4 -2
- package/src/shared.test.ts +35 -0
- package/src/shared.ts +22 -0
- package/src/suppliers/contract.ts +7 -2
- package/src/suppliers/opencode/plugin.test.ts +79 -25
- package/src/suppliers/opencode/plugin.ts +60 -29
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
} :
|
|
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 供应商插件 ——
|
|
5
|
+
* opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - chat:
|
|
9
|
-
* - models: GET /
|
|
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
|
-
*
|
|
12
|
-
* 403 `
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
41
|
-
|
|
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 =
|
|
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
|
-
*
|
|
85
|
+
* 免费档要求的占位工具:**只需要 `read` 一个**,小写精确。
|
|
62
86
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
*
|
|
29
|
+
* 调用方身份(有则记):宿主会话 id(`session-<uuid>`)或外部 OpenAI 客户端
|
|
30
|
+
* 标签(`ext/…`,见 shared.ts 的 clientIdentityOf)。用于「最近命中」按当前
|
|
31
|
+
* 会话归因(徽章要显示当前会话的命中,而不是全局最近),出事时也能反查
|
|
32
|
+
* 是哪个客户端在打。
|
|
31
33
|
*/
|
|
32
34
|
session?: string;
|
|
33
35
|
}
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
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,
|
package/src/llm/adapter.test.ts
CHANGED
|
@@ -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')
|
package/src/llm/adapter.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
: (
|
|
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
|
-
|
|
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
|
-
|
|
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') {
|
package/src/router/index.ts
CHANGED
|
@@ -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) {
|
package/src/router/types.ts
CHANGED
|
@@ -61,9 +61,13 @@ export interface ChatRequest {
|
|
|
61
61
|
/** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
|
|
62
62
|
lv?: string
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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-
|
|
2
|
+
* opencode(OpenCode Zen 免费档)握手契约测试 —— 锁住 2026-10-15 实测的那组硬门。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 为什么值得固化:这些**都不是可选优化,缺一即 403**
|
|
5
5
|
* `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* 它们全是「看不出来」的那种约定——代码删掉一行、或把端点/工具名改回去,
|
|
7
|
+
* 单测本该立刻变红,否则下次只能在真机对话里发现整个供应商死了。
|
|
8
8
|
*
|
|
9
|
-
* 实测依据(2026-
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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('
|
|
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
|
-
|
|
98
|
+
assert.equal(stub.captured[0]!.url, EXPECTED_CHAT_URL, '/zen/v1 是本次故障根因,不得回退')
|
|
99
|
+
} finally {
|
|
100
|
+
stub.restore()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
//
|
|
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('
|
|
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
|
|
100
|
-
|
|
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('调用方自带的同名工具不被占位工具覆盖(
|
|
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
|
|
120
|
-
assert.equal(
|
|
121
|
-
assert.equal(
|
|
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
|
-
//
|
|
124
|
-
assert.
|
|
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 供应商插件 ——
|
|
2
|
+
* opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - chat:
|
|
6
|
-
* - models: GET /
|
|
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
|
-
*
|
|
9
|
-
* 403 `
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
212
|
+
// 上游要求 tools 里存在小写 `read`,否则 403(缺就补,已有则原样保留)
|
|
182
213
|
ensureDecoyTools(obj)
|
|
183
214
|
// effort 白名单:'off'/'none' 必须**删字段**(上游没有这两个值,
|
|
184
215
|
// 传 'off' 直接 400 invalid_request_error);low/high/max 实测可过。
|