dsh-xdpool 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/lib/bin.js +249 -16
- package/lib/client.js +3 -3
- package/lib/index.d.ts +53 -5
- package/lib/index.js +351 -53
- package/package.json +1 -1
- package/vendor/channel-pack/pack.js +41 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
本项目的所有重要变更都会记录在此文件。
|
|
3
3
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
## 2.4.1 — 11128 的真正成因:system 内容指纹
|
|
5
|
+
> 2.4.0 之后仍有用户撞上 `400 / 11128 Illegal API invocation from an unapproved channel`。定位下来,2.4.0 的归因是错的,这一版修的是真原因。
|
|
6
|
+
### 11128 不是身份头的问题,是 system 内容里的自报句
|
|
7
|
+
网关按**内容指纹**判定「这是不是官方客户端发出来的」,命中串是 system 消息里的 `You are an AI agent powered by DeepSeek Harness`。它只匹配 `role === "system"`(放进 user/tool 不管),字面子串、大小写不敏感,且带起始位置倾向——放在提示词第一行会被拦,前面垫一行普通文字就放行。
|
|
8
|
+
|
|
9
|
+
这句话由宿主的第一方插件 `@deepseek-ai/dsh-system-prompt` 固定注入,**任何未改装的安装都会 100% 命中**,与账号、密钥、渠道、UA 全部无关——这也解释了为什么它在「1 个账号失败后不再重试」:每个账号都会失败,重试没有意义。
|
|
10
|
+
|
|
11
|
+
修法是在发送前把 system 里的这句改写成网关放行的同义句(`powered by` → `running on`),只动命中的文本块,图片/缓存块的引用原样透传;没命中时返回同一个引用,不为「什么都没改」付深拷贝代价。改写清单做成了数组,将来网关加句只是多一行。
|
|
12
|
+
|
|
13
|
+
**实测(真实国内网关,同一账号三组对照)**:
|
|
14
|
+
| 请求体 | 结果 |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 经本插件改写 | `429 / 6004`(通过渠道校验,仅受频率限制) |
|
|
17
|
+
| 原样含该句(对照) | `400 / 11128` |
|
|
18
|
+
| system 干净(基线) | `429 / 6004` |
|
|
19
|
+
|
|
20
|
+
### 国际网关要求首条消息是 system
|
|
21
|
+
`www.workbuddy.ai` 对一个以 `user` 开头的请求体回 `400 / 11128 "first message is not system prompt"`,而**同一个请求体在国内网关是被接受的**。同一个错误码,两种完全不同成因——这正是不能只按业务码分支的原因。现在国际版请求若首条不是 system,会在最前面补一条占位 system,用户自己的消息位置和措辞一个字符都不动。
|
|
22
|
+
|
|
23
|
+
### 思考档位「关闭」会被两区同时拒绝
|
|
24
|
+
适配层把 `thinkingLevelMap.off` 钉成字面量 `'off'`,而 pi-ai 对「没有显式指定档位」的请求发的就是它——所以选「默认」和选「关闭」产生了同一个请求体。两区都不接受这个值:国际版 GPT 族回 `400 / 11133`(`extError.param === 'reasoning.effort'`),国内版在声明了 `onlyReasoning` 的模型上报错。现在发送前把它删掉,让上游走自己的默认档。**省略**是唯一在各个区、各个模型上都实测通过的写法;换成字面量 `'none'` 不是安全替代。
|
|
25
|
+
|
|
26
|
+
**实测(真实双网关,同一账号前后对照)**:
|
|
27
|
+
|
|
28
|
+
| 请求 | 修复前 | 修复后 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 国际版,首条为 user | `400 / 11128 first message is not system prompt` | `200` |
|
|
31
|
+
| 国际版,`reasoning_effort: "off"` | `400 / 11133 extError.param=reasoning.effort` | `200` |
|
|
32
|
+
|
|
33
|
+
### 免费模型限流提示:说清配额是按会话算的
|
|
34
|
+
免费车道的拒绝原文是「the provider asked to wait 74558s」——二十小时,读起来像是整个免费车道被封了。实际配额**按上游会话计**,换一个会话(或换一个模型)就另有一份额度。现在限流错误的文案补上了这一句;只有限流类错误带这句,凭据失效、模型不存在之类加上去只会引导用户去做无用重试。
|
|
35
|
+
### 其他
|
|
36
|
+
- 测试:新增 system 指纹改写 8 例 + 区域专属请求体规则 12 例 + 免费车道会话配额提示 1 例,全套 71 文件 932 用例。
|
|
37
|
+
## 2.4.0 — 会话身份头、清单不再冻结、中文路由可存
|
|
38
|
+
> 这一版收的是「用着用着就报错」的那一类问题:调用被网关判成非法渠道(11128)、国际版模型清单永远停在启动那一刻、中文名字的路由点了保存却没存下、上下文选择一选就弹回去。
|
|
39
|
+
### 会话身份头:补齐官方客户端的契约
|
|
40
|
+
网关对「这一轮对话属于谁」有一组身份头。补齐官方客户端的契约:
|
|
41
|
+
- **会话稳定三头**(同一会话内不变):`X-Conversation-ID`、`X-Conversation-Request-ID`、`acp-connection-id`。
|
|
42
|
+
- **每请求两头**(逐次随机):`X-Conversation-Message-ID`、`X-Request-ID`。
|
|
43
|
+
- 另附 `X-Agent-Intent`、`X-Agent-Purpose`、`x-codebuddy-request`、`X-Private-Data`、`X-Product` 与 W3C/B3 追踪族。
|
|
44
|
+
|
|
45
|
+
> **口径更正(见 2.4.1)**:本版曾把 `400 / 11128` 归因于「缺会话身份头」。这个归因是错的——实测国内网关根本不检查这组头(不带任何身份头、system 干净时正常放行)。身份头照官方契约补齐没有坏处,但 11128 的真正成因是 system 内容里的自报句,已在 2.4.1 修掉。
|
|
46
|
+
|
|
47
|
+
顺带修掉同源的一处:`role: "developer"` 的消息在国内网关同样触发 11128,现统一上送为 `system`。
|
|
48
|
+
### 国际版模型清单:不再冻结在启动那一刻
|
|
49
|
+
`createProvider` 的 `models` 是在创建 provider 时快照下来的数组,而原来的 `invalidate()` 只重排了 profile、复用了同一个 provider 对象,宿主按对象身份记忆化 `current()`,于是**新拿到的模型清单永远送不到选择器**——表现是「重新获取模型」点了没反应、国际版清单停在开机那一刻。现在每次 `invalidate()` 重建 provider 与 profile 两个对象,并通过 adapter handle 的 `replace()` 重新登记路由,由宿主自己发出 `llm/adapters-updated`。
|
|
50
|
+
(附带核查:国际版本来就有独立的 `modelSelectionGlobal` 键,与国内白名单分开读写,这一处无需改动。)
|
|
51
|
+
### 中文路由名现在存得下
|
|
52
|
+
链名 slug 原来是 `[^a-z0-9]+ → '-'`,中文名整串被压成空 id,规范化时被当作无效条目静默丢弃——于是「保存成功」但什么都没存。改为 Unicode 感知的 slug(`\p{L}`/`\p{N}`),拉丁名形态完全不变,已存的路由 id 继续可解析。
|
|
53
|
+
### 上下文大小:选中态不再一选就弹回
|
|
54
|
+
渠道卡把「从未保存过预算」当成了「已收窄到 200K」显示,用户点「完整」清空预算后该行又算回 200K,选中态立刻跳回第一项。现在未保存过预算就显示完整窗口;清空预算时同时保留 `nativeContextWindow`,radio 组不会在指针底下消失。
|
|
55
|
+
### 思考档位:不再只认一档
|
|
56
|
+
pack 兜底表把十几个模型的 `reasoningEfforts` 写成了单档(`["medium"]` / `["high"]`),于是国际版 `Deepseek-V4.1-Flash` 只能选 High。逐模型实测(两网关、五档全跑)后按真实能力补齐:凡是能调通的模型五档都接受、且各档 `reasoning_content` 长度不同。`gpt-5.3-codex` 五档皆 200 但 reasoning 恒空——档位合法而不可观测,已经在表里注明;`gemini-3.5-flash` 与 `primary-model` 的 400 是账号套餐权限(`model [X] is not allowed for user type [personal]`),与档位无关,未动。
|
|
57
|
+
### 重复账号:同一个人不再显示成两个
|
|
58
|
+
桌面端凭据同时带数字 `uin` 与 UUID `uid`,扫码渠道只带 `uid`,而账号 id 是按 `uin` 优先哈希的——同一个人走两条登录路径就整出两个池条目,各自带一部分模型。现在按 `uid` 建次级索引合并到同一个池条目,既不改哈希(否则全部冷却、失效标记、忽略名单、持久化快照一次性作废),也与凭据到达顺序无关。
|
|
59
|
+
### 「重新检测账号」不再静默失败
|
|
60
|
+
`listAllAccounts` 漏了 `await`,拿到的是一个 Promise,随后的 `.filter` 抛 `is not a function` 并被外层 try/catch 吞掉,表现是扫码账号全部并进池、重新检测永远没结果。
|
|
61
|
+
### 其他
|
|
62
|
+
- 测试:新增会话身份头与中文路由名的回归用例,全套 68 文件 910 用例。
|
|
4
63
|
## 2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
|
|
5
64
|
> 这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
|
|
6
65
|
### 账号池:扫码渠道按渠道入池、逐模型可控
|
package/lib/bin.js
CHANGED
|
@@ -277,6 +277,164 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
277
277
|
"X-IDE-Type": "WorkBuddy",
|
|
278
278
|
"X-IDE-Version": "5.5.6"
|
|
279
279
|
});
|
|
280
|
+
/**
|
|
281
|
+
* System-prompt strings the gateway treats as evidence of a non-official
|
|
282
|
+
* client, and the rewriting that clears the check.
|
|
283
|
+
*
|
|
284
|
+
* WHY THIS EXISTS
|
|
285
|
+
*
|
|
286
|
+
* `code 11128 Illegal API invocation from an unapproved channel` has a SECOND
|
|
287
|
+
* trigger, separate from the conversation-identity headers below: the gateway
|
|
288
|
+
* matches the CONTENT of system messages against a list of strings that identify
|
|
289
|
+
* other agents' clients. The first-party DSH system-prompt plugin injects
|
|
290
|
+
* `You are an AI agent powered by DeepSeek Harness.` as the opening line of
|
|
291
|
+
* every conversation, so every request from an unmodified install is rejected —
|
|
292
|
+
* regardless of which account in the pool is used, which is why the reported
|
|
293
|
+
* symptom was a 400 on the first account with no retry.
|
|
294
|
+
*
|
|
295
|
+
* Boundary, measured against the live domestic gateway (429 = accepted,
|
|
296
|
+
* 400/11128 = blocked):
|
|
297
|
+
* - the match is a literal substring, BUT only near the START of a system
|
|
298
|
+
* message: the sentence blocked a longer prompt when it was the first line,
|
|
299
|
+
* and passed when one plain line was prepended to it ("trailing text after
|
|
300
|
+
* it" above is the same finding from the other side).
|
|
301
|
+
* - it is case-insensitive (`you are an ai agent powered by deepseek harness`
|
|
302
|
+
* is blocked too).
|
|
303
|
+
* - only `role: "system"` is inspected: the same sentence in a `user` or
|
|
304
|
+
* `tool` message passes, and it is blocked in a second system message too.
|
|
305
|
+
* - `deepseek harness` alone, or `powered by deepseek` alone, passes — the
|
|
306
|
+
* whole phrase is what matches.
|
|
307
|
+
* - replacing `powered by` with `running on` passes, which is the rewrite
|
|
308
|
+
* below: it keeps the meaning for the model and shares no substring with
|
|
309
|
+
* the fingerprint.
|
|
310
|
+
*
|
|
311
|
+
* A prepended blank line would equally defeat a start-anchored check, but it
|
|
312
|
+
* would leave the banned string in the body: the list is server-side and may
|
|
313
|
+
* lose its anchor tomorrow. Rewriting the phrase is what the report's repro
|
|
314
|
+
* verified, so that is what this does.
|
|
315
|
+
*
|
|
316
|
+
* The list is an ARRAY because these fingerprints are appended to over time; a
|
|
317
|
+
* future rejection should be one more row here, not a new code path.
|
|
318
|
+
*/
|
|
319
|
+
const BANNED_SYSTEM_PHRASES = Object.freeze([[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"]]);
|
|
320
|
+
/**
|
|
321
|
+
* Rewrite fingerprinted system content, leaving everything else byte-identical.
|
|
322
|
+
*
|
|
323
|
+
* System content arrives as either a plain string or an array of content blocks
|
|
324
|
+
* (an OpenAI-compatible body allows both), so both shapes are handled — fixing
|
|
325
|
+
* only the string form would miss every request that carries an image or a
|
|
326
|
+
* cache-control block.
|
|
327
|
+
*
|
|
328
|
+
* Returns the SAME reference when nothing matched: a long conversation's system
|
|
329
|
+
* prompt can be large, and paying for a deep copy to change nothing is waste.
|
|
330
|
+
* Only matched `text` blocks are rebuilt; image and cache blocks pass through
|
|
331
|
+
* untouched so the request shape survives.
|
|
332
|
+
*/
|
|
333
|
+
function sanitizeSystemContent(content) {
|
|
334
|
+
if (typeof content === "string") return sanitizeSystemText(content);
|
|
335
|
+
if (!Array.isArray(content)) return content;
|
|
336
|
+
let changed = false;
|
|
337
|
+
const next = content.map((block) => {
|
|
338
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) return block;
|
|
339
|
+
const record = block;
|
|
340
|
+
const text = record["text"];
|
|
341
|
+
if (typeof text !== "string") return block;
|
|
342
|
+
const cleaned = sanitizeSystemText(text);
|
|
343
|
+
if (cleaned === text) return block;
|
|
344
|
+
changed = true;
|
|
345
|
+
return {
|
|
346
|
+
...record,
|
|
347
|
+
text: cleaned
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
return changed ? next : content;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Apply every banned-phrase rewrite to one string.
|
|
354
|
+
*
|
|
355
|
+
* The RegExp is rebuilt per call on purpose: a shared `g`-flagged instance
|
|
356
|
+
* carries `lastIndex` between calls, so the second request in a conversation
|
|
357
|
+
* could skip a match the first one caught.
|
|
358
|
+
*/
|
|
359
|
+
function sanitizeSystemText(text) {
|
|
360
|
+
let out = text;
|
|
361
|
+
for (const [pattern, replacement] of BANNED_SYSTEM_PHRASES) out = out.replace(new RegExp(pattern.source, pattern.flags), replacement);
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The conversation-identity headers the gateway's channel check demands.
|
|
366
|
+
*
|
|
367
|
+
* WHY THIS EXISTS
|
|
368
|
+
*
|
|
369
|
+
* `www.workbuddy.ai` turned "channel verification" from a soft check into a
|
|
370
|
+
* hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
|
|
371
|
+
* `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
|
|
372
|
+
* business code 11128 (`msg: "request illegal"` on the international gateway,
|
|
373
|
+
* `msg: "conversation id missing"` in the earlier wording). Verified by
|
|
374
|
+
* elimination against real credentials: delete any one of the three and the
|
|
375
|
+
* call 400s; send all three and it 200s with normal SSE. Every other header the
|
|
376
|
+
* desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
|
|
377
|
+
* `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
|
|
378
|
+
* so only these three are load-bearing.
|
|
379
|
+
*
|
|
380
|
+
* The values are NOT validated — plain non-UUID strings pass — so the gateway
|
|
381
|
+
* is checking for presence and consistency, not for a signature. That lets us
|
|
382
|
+
* derive them from the conversation instead of registering state with upstream.
|
|
383
|
+
*
|
|
384
|
+
* 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
|
|
385
|
+
* `msg: "Illegal API invocation from an unapproved channel"` for a different
|
|
386
|
+
* cause entirely: a `developer`-role message. `prepareChatBody()` already
|
|
387
|
+
* rewrites that role; see its comment. Never branch on the business code alone.
|
|
388
|
+
*/
|
|
389
|
+
const CONVERSATION_STABLE_SALT = "conversation";
|
|
390
|
+
/**
|
|
391
|
+
* The three stable conversation ids for one logical conversation.
|
|
392
|
+
*
|
|
393
|
+
* Stable across every call and every account in a conversation, because the
|
|
394
|
+
* upstream treats the triple as one session: regenerating them per request
|
|
395
|
+
* would read as a new conversation on each turn. Derived rather than stored so
|
|
396
|
+
* the identity survives a restart and needs no registry — the same reasoning
|
|
397
|
+
* `deriveDeviceId` uses for the device id.
|
|
398
|
+
*
|
|
399
|
+
* Falls back to a random seed when the caller has no key (a one-shot compaction
|
|
400
|
+
* outside any conversation), which is safe because presence is all that is
|
|
401
|
+
* checked.
|
|
402
|
+
*/
|
|
403
|
+
function conversationIdentityOf(conversationKey, seed = randomUUID) {
|
|
404
|
+
const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
|
|
405
|
+
const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
|
|
406
|
+
const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
407
|
+
return {
|
|
408
|
+
conversationId: asUuid(hash("id")),
|
|
409
|
+
conversationRequestId: asUuid(hash("request")),
|
|
410
|
+
acpConnectionId: asUuid(hash("acp"))
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Full header set for one chat completion.
|
|
415
|
+
*
|
|
416
|
+
* `chatHeaders()` stays untouched on purpose: it is shared with the
|
|
417
|
+
* points-automation endpoints, which have never been shown to carry
|
|
418
|
+
* conversation identity, and adding it there would change how that traffic is
|
|
419
|
+
* attributed. The per-request ids are fresh UUIDs every call; only the triple
|
|
420
|
+
* from `conversationIdentityOf` is stable.
|
|
421
|
+
*/
|
|
422
|
+
function chatCompletionHeaders(credential, conversationKey) {
|
|
423
|
+
const identity = conversationIdentityOf(conversationKey);
|
|
424
|
+
return {
|
|
425
|
+
...chatHeaders(credential),
|
|
426
|
+
...CHAT_IDENTITY_HEADERS,
|
|
427
|
+
"X-Conversation-ID": identity.conversationId,
|
|
428
|
+
"X-Conversation-Request-ID": identity.conversationRequestId,
|
|
429
|
+
"acp-connection-id": identity.acpConnectionId,
|
|
430
|
+
"X-Conversation-Message-ID": randomUUID(),
|
|
431
|
+
"X-Request-ID": randomUUID(),
|
|
432
|
+
"X-Agent-Intent": "craft",
|
|
433
|
+
"X-Agent-Purpose": "conversation",
|
|
434
|
+
"X-Private-Data": "true",
|
|
435
|
+
"x-codebuddy-request": "1"
|
|
436
|
+
};
|
|
437
|
+
}
|
|
280
438
|
/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
|
|
281
439
|
function refreshHeaders(credential) {
|
|
282
440
|
const headers = {
|
|
@@ -804,7 +962,15 @@ var WorkBuddyUpstreamClient = class {
|
|
|
804
962
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
805
963
|
* force `stream: true` (the upstream rejects non-streaming), convert the
|
|
806
964
|
* DSH `developer` role into `system` (upstream rejects `developer` with
|
|
807
|
-
* business code 11128),
|
|
965
|
+
* business code 11128), rewrite system content the gateway fingerprints as a
|
|
966
|
+
* non-official client (also 11128), and flatten `tool_choice` into its string
|
|
967
|
+
* form.
|
|
968
|
+
*
|
|
969
|
+
* This is the REGION-BLIND half. Everything here is measured good on both
|
|
970
|
+
* gateways, so it runs before the region is known — the pool lane picks its
|
|
971
|
+
* account after this call. The two region-only rules live in
|
|
972
|
+
* {@link prepareChatBodyFor} instead; see its comment for why they cannot be
|
|
973
|
+
* folded in here.
|
|
808
974
|
*/
|
|
809
975
|
prepareChatBody(raw) {
|
|
810
976
|
let body;
|
|
@@ -823,6 +989,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
823
989
|
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
824
990
|
const message = value;
|
|
825
991
|
if (message["role"] === "developer") message["role"] = "system";
|
|
992
|
+
if (message["role"] === "system") message["content"] = sanitizeSystemContent(message["content"]);
|
|
826
993
|
}
|
|
827
994
|
const choice = obj["tool_choice"];
|
|
828
995
|
if (typeof choice === "string") {
|
|
@@ -849,6 +1016,61 @@ var WorkBuddyUpstreamClient = class {
|
|
|
849
1016
|
return JSON.stringify(obj);
|
|
850
1017
|
}
|
|
851
1018
|
/**
|
|
1019
|
+
* The region-only half of body normalisation, applied once the account is
|
|
1020
|
+
* known.
|
|
1021
|
+
*
|
|
1022
|
+
* WHY IT IS SEPARATE
|
|
1023
|
+
*
|
|
1024
|
+
* The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
|
|
1025
|
+
* the body has to be ready for whichever account wins. So anything that
|
|
1026
|
+
* depends on the region cannot live there. Two rules do:
|
|
1027
|
+
*
|
|
1028
|
+
* 1. **International requires a leading `system`.** Measured: a body whose
|
|
1029
|
+
* first message is `user` answers `400 / 11128 "first message is not
|
|
1030
|
+
* system prompt"` on `www.workbuddy.ai`, while the identical body is
|
|
1031
|
+
* accepted on the domestic gateway. Same code, different cause — the
|
|
1032
|
+
* domestic gateway uses 11128 for the `developer` role and for the
|
|
1033
|
+
* content fingerprint, which is why nothing may branch on the code alone.
|
|
1034
|
+
* A placeholder assistant prompt is prepended, never merged: every
|
|
1035
|
+
* caller-supplied message keeps its position and wording.
|
|
1036
|
+
*
|
|
1037
|
+
* 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
|
|
1038
|
+
* `thinkingLevelMap.off` to the literal `'off'` so the level stays
|
|
1039
|
+
* selectable, and pi-ai emits that value for any request carrying no
|
|
1040
|
+
* explicit level — so "Default" and "Off" produce the same body.
|
|
1041
|
+
* Measured: the international GPT family answers `400 / 11133`
|
|
1042
|
+
* (`extError.param === 'reasoning.effort'`), and the domestic gateway
|
|
1043
|
+
* rejects it on models that declare `onlyReasoning`. Omission is the only
|
|
1044
|
+
* spelling measured good everywhere; a literal `'none'` is NOT a safe
|
|
1045
|
+
* substitute (accepted by the GPT-5.6 family, rejected by others).
|
|
1046
|
+
*
|
|
1047
|
+
* Both rules are no-ops on a body that already complies, so this is safe to
|
|
1048
|
+
* run over an already-prepared body.
|
|
1049
|
+
*/
|
|
1050
|
+
prepareChatBodyFor(raw, region) {
|
|
1051
|
+
const prepared = this.prepareChatBody(raw);
|
|
1052
|
+
let body;
|
|
1053
|
+
try {
|
|
1054
|
+
body = JSON.parse(prepared);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return prepared;
|
|
1057
|
+
}
|
|
1058
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return prepared;
|
|
1059
|
+
const obj = body;
|
|
1060
|
+
if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
|
|
1061
|
+
if (region === "global") {
|
|
1062
|
+
const messages = obj["messages"];
|
|
1063
|
+
if (Array.isArray(messages)) {
|
|
1064
|
+
const first = messages[0];
|
|
1065
|
+
if (!(typeof first === "object" && first !== null && !Array.isArray(first) && first["role"] === "system")) messages.unshift({
|
|
1066
|
+
role: "system",
|
|
1067
|
+
content: "You are a helpful assistant."
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return JSON.stringify(obj);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
852
1074
|
* Parse a raw OpenAI chat body without normalising it.
|
|
853
1075
|
*
|
|
854
1076
|
* The compactor needs the message array as objects, while `chatStream` only
|
|
@@ -864,12 +1086,20 @@ var WorkBuddyUpstreamClient = class {
|
|
|
864
1086
|
if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
|
|
865
1087
|
return body;
|
|
866
1088
|
}
|
|
867
|
-
/**
|
|
868
|
-
|
|
869
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Re-serialise `base` with a rewritten `messages` array, still normalised.
|
|
1091
|
+
*
|
|
1092
|
+
* `region` is optional so the composite lanes (aggregate / free) can keep
|
|
1093
|
+
* calling it without one; when supplied, the region-only rules of
|
|
1094
|
+
* {@link prepareChatBodyFor} ride along. Both in-repo callers are the
|
|
1095
|
+
* compaction paths, which always know their shim's region.
|
|
1096
|
+
*/
|
|
1097
|
+
buildChatBody(base, messages, region) {
|
|
1098
|
+
const raw = JSON.stringify({
|
|
870
1099
|
...base,
|
|
871
1100
|
messages
|
|
872
|
-
})
|
|
1101
|
+
});
|
|
1102
|
+
return region === void 0 ? this.prepareChatBody(raw) : this.prepareChatBodyFor(raw, region);
|
|
873
1103
|
}
|
|
874
1104
|
/**
|
|
875
1105
|
* Run one NON-streaming completion and return the assistant text.
|
|
@@ -879,13 +1109,10 @@ var WorkBuddyUpstreamClient = class {
|
|
|
879
1109
|
* single string. Throws on any failure: the compactor then falls back to
|
|
880
1110
|
* plain truncation rather than failing the user's turn.
|
|
881
1111
|
*/
|
|
882
|
-
async completeChat(credential, prepared, signal) {
|
|
1112
|
+
async completeChat(credential, prepared, signal, conversationKey) {
|
|
883
1113
|
const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
884
1114
|
method: "POST",
|
|
885
|
-
headers:
|
|
886
|
-
...chatHeaders(credential),
|
|
887
|
-
...CHAT_IDENTITY_HEADERS
|
|
888
|
-
},
|
|
1115
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
889
1116
|
body: prepared,
|
|
890
1117
|
...signal === void 0 ? {} : { signal }
|
|
891
1118
|
});
|
|
@@ -897,15 +1124,12 @@ var WorkBuddyUpstreamClient = class {
|
|
|
897
1124
|
return await readCompletionText(response.body);
|
|
898
1125
|
}
|
|
899
1126
|
/** Forward one chat completion. Never throws for upstream failures. */
|
|
900
|
-
async chatStream(credential, prepared, signal) {
|
|
1127
|
+
async chatStream(credential, prepared, signal, conversationKey) {
|
|
901
1128
|
let response;
|
|
902
1129
|
try {
|
|
903
1130
|
response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
904
1131
|
method: "POST",
|
|
905
|
-
headers:
|
|
906
|
-
...chatHeaders(credential),
|
|
907
|
-
...CHAT_IDENTITY_HEADERS
|
|
908
|
-
},
|
|
1132
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
909
1133
|
body: prepared,
|
|
910
1134
|
...signal === void 0 ? {} : { signal }
|
|
911
1135
|
});
|
|
@@ -3325,8 +3549,17 @@ var WorkBuddyAccountPool = class {
|
|
|
3325
3549
|
});
|
|
3326
3550
|
}
|
|
3327
3551
|
}
|
|
3552
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
3553
|
+
for (const account of byId.values()) {
|
|
3554
|
+
const uid = account.credential.uid;
|
|
3555
|
+
if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
|
|
3556
|
+
}
|
|
3328
3557
|
for (const credential of found) {
|
|
3329
|
-
const
|
|
3558
|
+
const byIdentity = workbuddyAccountId(credential);
|
|
3559
|
+
const uid = credential.uid;
|
|
3560
|
+
const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
|
|
3561
|
+
const id = knownId ?? byIdentity;
|
|
3562
|
+
if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
|
|
3330
3563
|
const existing = byId.get(id);
|
|
3331
3564
|
if (existing === void 0) {
|
|
3332
3565
|
byId.set(id, {
|
package/lib/client.js
CHANGED
|
@@ -11674,7 +11674,7 @@ window.__ModuleLoader__.load({
|
|
|
11674
11674
|
...model,
|
|
11675
11675
|
...budget === void 0 ? {
|
|
11676
11676
|
contextBudget: void 0,
|
|
11677
|
-
|
|
11677
|
+
contextWindow: model.nativeContextWindow ?? model.contextWindow
|
|
11678
11678
|
} : {
|
|
11679
11679
|
contextBudget: budget,
|
|
11680
11680
|
contextWindow: budget,
|
|
@@ -12079,7 +12079,7 @@ window.__ModuleLoader__.load({
|
|
|
12079
12079
|
const recuse = recommendUse(merged);
|
|
12080
12080
|
const native = model.nativeContextWindow ?? model.contextWindow;
|
|
12081
12081
|
const capped = native !== void 0 && native > CONTEXT_BUDGET_DEFAULT;
|
|
12082
|
-
const budget = model.contextBudget ??
|
|
12082
|
+
const budget = model.contextBudget ?? native;
|
|
12083
12083
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
12084
12084
|
className: "dsm-xdpool-cpool-modelrow",
|
|
12085
12085
|
"data-off": !on,
|
|
@@ -12188,7 +12188,7 @@ window.__ModuleLoader__.load({
|
|
|
12188
12188
|
*
|
|
12189
12189
|
* @module dsh-xdpool/version
|
|
12190
12190
|
*/
|
|
12191
|
-
const POOL_VERSION = "2.
|
|
12191
|
+
const POOL_VERSION = "2.4.1";
|
|
12192
12192
|
//#endregion
|
|
12193
12193
|
//#region src/client/PoolCard.tsx
|
|
12194
12194
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -469,9 +469,50 @@ export declare class WorkBuddyUpstreamClient {
|
|
|
469
469
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
470
470
|
* force `stream: true` (the upstream rejects non-streaming), convert the
|
|
471
471
|
* DSH `developer` role into `system` (upstream rejects `developer` with
|
|
472
|
-
* business code 11128),
|
|
472
|
+
* business code 11128), rewrite system content the gateway fingerprints as a
|
|
473
|
+
* non-official client (also 11128), and flatten `tool_choice` into its string
|
|
474
|
+
* form.
|
|
475
|
+
*
|
|
476
|
+
* This is the REGION-BLIND half. Everything here is measured good on both
|
|
477
|
+
* gateways, so it runs before the region is known — the pool lane picks its
|
|
478
|
+
* account after this call. The two region-only rules live in
|
|
479
|
+
* {@link prepareChatBodyFor} instead; see its comment for why they cannot be
|
|
480
|
+
* folded in here.
|
|
473
481
|
*/
|
|
474
482
|
prepareChatBody(raw: string): string;
|
|
483
|
+
/**
|
|
484
|
+
* The region-only half of body normalisation, applied once the account is
|
|
485
|
+
* known.
|
|
486
|
+
*
|
|
487
|
+
* WHY IT IS SEPARATE
|
|
488
|
+
*
|
|
489
|
+
* The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
|
|
490
|
+
* the body has to be ready for whichever account wins. So anything that
|
|
491
|
+
* depends on the region cannot live there. Two rules do:
|
|
492
|
+
*
|
|
493
|
+
* 1. **International requires a leading `system`.** Measured: a body whose
|
|
494
|
+
* first message is `user` answers `400 / 11128 "first message is not
|
|
495
|
+
* system prompt"` on `www.workbuddy.ai`, while the identical body is
|
|
496
|
+
* accepted on the domestic gateway. Same code, different cause — the
|
|
497
|
+
* domestic gateway uses 11128 for the `developer` role and for the
|
|
498
|
+
* content fingerprint, which is why nothing may branch on the code alone.
|
|
499
|
+
* A placeholder assistant prompt is prepended, never merged: every
|
|
500
|
+
* caller-supplied message keeps its position and wording.
|
|
501
|
+
*
|
|
502
|
+
* 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
|
|
503
|
+
* `thinkingLevelMap.off` to the literal `'off'` so the level stays
|
|
504
|
+
* selectable, and pi-ai emits that value for any request carrying no
|
|
505
|
+
* explicit level — so "Default" and "Off" produce the same body.
|
|
506
|
+
* Measured: the international GPT family answers `400 / 11133`
|
|
507
|
+
* (`extError.param === 'reasoning.effort'`), and the domestic gateway
|
|
508
|
+
* rejects it on models that declare `onlyReasoning`. Omission is the only
|
|
509
|
+
* spelling measured good everywhere; a literal `'none'` is NOT a safe
|
|
510
|
+
* substitute (accepted by the GPT-5.6 family, rejected by others).
|
|
511
|
+
*
|
|
512
|
+
* Both rules are no-ops on a body that already complies, so this is safe to
|
|
513
|
+
* run over an already-prepared body.
|
|
514
|
+
*/
|
|
515
|
+
prepareChatBodyFor(raw: string, region: WorkBuddyRegion): string;
|
|
475
516
|
/**
|
|
476
517
|
* Parse a raw OpenAI chat body without normalising it.
|
|
477
518
|
*
|
|
@@ -479,8 +520,15 @@ export declare class WorkBuddyUpstreamClient {
|
|
|
479
520
|
* accepts the serialised string form.
|
|
480
521
|
*/
|
|
481
522
|
parseChatBody(raw: string): Record<string, unknown> | undefined;
|
|
482
|
-
/**
|
|
483
|
-
|
|
523
|
+
/**
|
|
524
|
+
* Re-serialise `base` with a rewritten `messages` array, still normalised.
|
|
525
|
+
*
|
|
526
|
+
* `region` is optional so the composite lanes (aggregate / free) can keep
|
|
527
|
+
* calling it without one; when supplied, the region-only rules of
|
|
528
|
+
* {@link prepareChatBodyFor} ride along. Both in-repo callers are the
|
|
529
|
+
* compaction paths, which always know their shim's region.
|
|
530
|
+
*/
|
|
531
|
+
buildChatBody(base: Record<string, unknown>, messages: readonly ChatMessage[], region?: WorkBuddyRegion): string;
|
|
484
532
|
/**
|
|
485
533
|
* Run one NON-streaming completion and return the assistant text.
|
|
486
534
|
*
|
|
@@ -489,9 +537,9 @@ export declare class WorkBuddyUpstreamClient {
|
|
|
489
537
|
* single string. Throws on any failure: the compactor then falls back to
|
|
490
538
|
* plain truncation rather than failing the user's turn.
|
|
491
539
|
*/
|
|
492
|
-
completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<string>;
|
|
540
|
+
completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<string>;
|
|
493
541
|
/** Forward one chat completion. Never throws for upstream failures. */
|
|
494
|
-
chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
|
|
542
|
+
chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<ChatStreamResult>;
|
|
495
543
|
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
496
544
|
refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
|
|
497
545
|
/**
|
package/lib/index.js
CHANGED
|
@@ -328,6 +328,164 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
328
328
|
"X-IDE-Type": "WorkBuddy",
|
|
329
329
|
"X-IDE-Version": "5.5.6"
|
|
330
330
|
});
|
|
331
|
+
/**
|
|
332
|
+
* System-prompt strings the gateway treats as evidence of a non-official
|
|
333
|
+
* client, and the rewriting that clears the check.
|
|
334
|
+
*
|
|
335
|
+
* WHY THIS EXISTS
|
|
336
|
+
*
|
|
337
|
+
* `code 11128 Illegal API invocation from an unapproved channel` has a SECOND
|
|
338
|
+
* trigger, separate from the conversation-identity headers below: the gateway
|
|
339
|
+
* matches the CONTENT of system messages against a list of strings that identify
|
|
340
|
+
* other agents' clients. The first-party DSH system-prompt plugin injects
|
|
341
|
+
* `You are an AI agent powered by DeepSeek Harness.` as the opening line of
|
|
342
|
+
* every conversation, so every request from an unmodified install is rejected —
|
|
343
|
+
* regardless of which account in the pool is used, which is why the reported
|
|
344
|
+
* symptom was a 400 on the first account with no retry.
|
|
345
|
+
*
|
|
346
|
+
* Boundary, measured against the live domestic gateway (429 = accepted,
|
|
347
|
+
* 400/11128 = blocked):
|
|
348
|
+
* - the match is a literal substring, BUT only near the START of a system
|
|
349
|
+
* message: the sentence blocked a longer prompt when it was the first line,
|
|
350
|
+
* and passed when one plain line was prepended to it ("trailing text after
|
|
351
|
+
* it" above is the same finding from the other side).
|
|
352
|
+
* - it is case-insensitive (`you are an ai agent powered by deepseek harness`
|
|
353
|
+
* is blocked too).
|
|
354
|
+
* - only `role: "system"` is inspected: the same sentence in a `user` or
|
|
355
|
+
* `tool` message passes, and it is blocked in a second system message too.
|
|
356
|
+
* - `deepseek harness` alone, or `powered by deepseek` alone, passes — the
|
|
357
|
+
* whole phrase is what matches.
|
|
358
|
+
* - replacing `powered by` with `running on` passes, which is the rewrite
|
|
359
|
+
* below: it keeps the meaning for the model and shares no substring with
|
|
360
|
+
* the fingerprint.
|
|
361
|
+
*
|
|
362
|
+
* A prepended blank line would equally defeat a start-anchored check, but it
|
|
363
|
+
* would leave the banned string in the body: the list is server-side and may
|
|
364
|
+
* lose its anchor tomorrow. Rewriting the phrase is what the report's repro
|
|
365
|
+
* verified, so that is what this does.
|
|
366
|
+
*
|
|
367
|
+
* The list is an ARRAY because these fingerprints are appended to over time; a
|
|
368
|
+
* future rejection should be one more row here, not a new code path.
|
|
369
|
+
*/
|
|
370
|
+
const BANNED_SYSTEM_PHRASES = Object.freeze([[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"]]);
|
|
371
|
+
/**
|
|
372
|
+
* Rewrite fingerprinted system content, leaving everything else byte-identical.
|
|
373
|
+
*
|
|
374
|
+
* System content arrives as either a plain string or an array of content blocks
|
|
375
|
+
* (an OpenAI-compatible body allows both), so both shapes are handled — fixing
|
|
376
|
+
* only the string form would miss every request that carries an image or a
|
|
377
|
+
* cache-control block.
|
|
378
|
+
*
|
|
379
|
+
* Returns the SAME reference when nothing matched: a long conversation's system
|
|
380
|
+
* prompt can be large, and paying for a deep copy to change nothing is waste.
|
|
381
|
+
* Only matched `text` blocks are rebuilt; image and cache blocks pass through
|
|
382
|
+
* untouched so the request shape survives.
|
|
383
|
+
*/
|
|
384
|
+
function sanitizeSystemContent(content) {
|
|
385
|
+
if (typeof content === "string") return sanitizeSystemText(content);
|
|
386
|
+
if (!Array.isArray(content)) return content;
|
|
387
|
+
let changed = false;
|
|
388
|
+
const next = content.map((block) => {
|
|
389
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) return block;
|
|
390
|
+
const record = block;
|
|
391
|
+
const text = record["text"];
|
|
392
|
+
if (typeof text !== "string") return block;
|
|
393
|
+
const cleaned = sanitizeSystemText(text);
|
|
394
|
+
if (cleaned === text) return block;
|
|
395
|
+
changed = true;
|
|
396
|
+
return {
|
|
397
|
+
...record,
|
|
398
|
+
text: cleaned
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
return changed ? next : content;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Apply every banned-phrase rewrite to one string.
|
|
405
|
+
*
|
|
406
|
+
* The RegExp is rebuilt per call on purpose: a shared `g`-flagged instance
|
|
407
|
+
* carries `lastIndex` between calls, so the second request in a conversation
|
|
408
|
+
* could skip a match the first one caught.
|
|
409
|
+
*/
|
|
410
|
+
function sanitizeSystemText(text) {
|
|
411
|
+
let out = text;
|
|
412
|
+
for (const [pattern, replacement] of BANNED_SYSTEM_PHRASES) out = out.replace(new RegExp(pattern.source, pattern.flags), replacement);
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The conversation-identity headers the gateway's channel check demands.
|
|
417
|
+
*
|
|
418
|
+
* WHY THIS EXISTS
|
|
419
|
+
*
|
|
420
|
+
* `www.workbuddy.ai` turned "channel verification" from a soft check into a
|
|
421
|
+
* hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
|
|
422
|
+
* `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
|
|
423
|
+
* business code 11128 (`msg: "request illegal"` on the international gateway,
|
|
424
|
+
* `msg: "conversation id missing"` in the earlier wording). Verified by
|
|
425
|
+
* elimination against real credentials: delete any one of the three and the
|
|
426
|
+
* call 400s; send all three and it 200s with normal SSE. Every other header the
|
|
427
|
+
* desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
|
|
428
|
+
* `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
|
|
429
|
+
* so only these three are load-bearing.
|
|
430
|
+
*
|
|
431
|
+
* The values are NOT validated — plain non-UUID strings pass — so the gateway
|
|
432
|
+
* is checking for presence and consistency, not for a signature. That lets us
|
|
433
|
+
* derive them from the conversation instead of registering state with upstream.
|
|
434
|
+
*
|
|
435
|
+
* 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
|
|
436
|
+
* `msg: "Illegal API invocation from an unapproved channel"` for a different
|
|
437
|
+
* cause entirely: a `developer`-role message. `prepareChatBody()` already
|
|
438
|
+
* rewrites that role; see its comment. Never branch on the business code alone.
|
|
439
|
+
*/
|
|
440
|
+
const CONVERSATION_STABLE_SALT = "conversation";
|
|
441
|
+
/**
|
|
442
|
+
* The three stable conversation ids for one logical conversation.
|
|
443
|
+
*
|
|
444
|
+
* Stable across every call and every account in a conversation, because the
|
|
445
|
+
* upstream treats the triple as one session: regenerating them per request
|
|
446
|
+
* would read as a new conversation on each turn. Derived rather than stored so
|
|
447
|
+
* the identity survives a restart and needs no registry — the same reasoning
|
|
448
|
+
* `deriveDeviceId` uses for the device id.
|
|
449
|
+
*
|
|
450
|
+
* Falls back to a random seed when the caller has no key (a one-shot compaction
|
|
451
|
+
* outside any conversation), which is safe because presence is all that is
|
|
452
|
+
* checked.
|
|
453
|
+
*/
|
|
454
|
+
function conversationIdentityOf(conversationKey, seed = randomUUID) {
|
|
455
|
+
const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
|
|
456
|
+
const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
|
|
457
|
+
const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
458
|
+
return {
|
|
459
|
+
conversationId: asUuid(hash("id")),
|
|
460
|
+
conversationRequestId: asUuid(hash("request")),
|
|
461
|
+
acpConnectionId: asUuid(hash("acp"))
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Full header set for one chat completion.
|
|
466
|
+
*
|
|
467
|
+
* `chatHeaders()` stays untouched on purpose: it is shared with the
|
|
468
|
+
* points-automation endpoints, which have never been shown to carry
|
|
469
|
+
* conversation identity, and adding it there would change how that traffic is
|
|
470
|
+
* attributed. The per-request ids are fresh UUIDs every call; only the triple
|
|
471
|
+
* from `conversationIdentityOf` is stable.
|
|
472
|
+
*/
|
|
473
|
+
function chatCompletionHeaders(credential, conversationKey) {
|
|
474
|
+
const identity = conversationIdentityOf(conversationKey);
|
|
475
|
+
return {
|
|
476
|
+
...chatHeaders(credential),
|
|
477
|
+
...CHAT_IDENTITY_HEADERS,
|
|
478
|
+
"X-Conversation-ID": identity.conversationId,
|
|
479
|
+
"X-Conversation-Request-ID": identity.conversationRequestId,
|
|
480
|
+
"acp-connection-id": identity.acpConnectionId,
|
|
481
|
+
"X-Conversation-Message-ID": randomUUID(),
|
|
482
|
+
"X-Request-ID": randomUUID(),
|
|
483
|
+
"X-Agent-Intent": "craft",
|
|
484
|
+
"X-Agent-Purpose": "conversation",
|
|
485
|
+
"X-Private-Data": "true",
|
|
486
|
+
"x-codebuddy-request": "1"
|
|
487
|
+
};
|
|
488
|
+
}
|
|
331
489
|
/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
|
|
332
490
|
function refreshHeaders(credential) {
|
|
333
491
|
const headers = {
|
|
@@ -964,7 +1122,15 @@ var WorkBuddyUpstreamClient = class {
|
|
|
964
1122
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
965
1123
|
* force `stream: true` (the upstream rejects non-streaming), convert the
|
|
966
1124
|
* DSH `developer` role into `system` (upstream rejects `developer` with
|
|
967
|
-
* business code 11128),
|
|
1125
|
+
* business code 11128), rewrite system content the gateway fingerprints as a
|
|
1126
|
+
* non-official client (also 11128), and flatten `tool_choice` into its string
|
|
1127
|
+
* form.
|
|
1128
|
+
*
|
|
1129
|
+
* This is the REGION-BLIND half. Everything here is measured good on both
|
|
1130
|
+
* gateways, so it runs before the region is known — the pool lane picks its
|
|
1131
|
+
* account after this call. The two region-only rules live in
|
|
1132
|
+
* {@link prepareChatBodyFor} instead; see its comment for why they cannot be
|
|
1133
|
+
* folded in here.
|
|
968
1134
|
*/
|
|
969
1135
|
prepareChatBody(raw) {
|
|
970
1136
|
let body;
|
|
@@ -983,6 +1149,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
983
1149
|
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
984
1150
|
const message = value;
|
|
985
1151
|
if (message["role"] === "developer") message["role"] = "system";
|
|
1152
|
+
if (message["role"] === "system") message["content"] = sanitizeSystemContent(message["content"]);
|
|
986
1153
|
}
|
|
987
1154
|
const choice = obj["tool_choice"];
|
|
988
1155
|
if (typeof choice === "string") {
|
|
@@ -1009,6 +1176,61 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1009
1176
|
return JSON.stringify(obj);
|
|
1010
1177
|
}
|
|
1011
1178
|
/**
|
|
1179
|
+
* The region-only half of body normalisation, applied once the account is
|
|
1180
|
+
* known.
|
|
1181
|
+
*
|
|
1182
|
+
* WHY IT IS SEPARATE
|
|
1183
|
+
*
|
|
1184
|
+
* The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
|
|
1185
|
+
* the body has to be ready for whichever account wins. So anything that
|
|
1186
|
+
* depends on the region cannot live there. Two rules do:
|
|
1187
|
+
*
|
|
1188
|
+
* 1. **International requires a leading `system`.** Measured: a body whose
|
|
1189
|
+
* first message is `user` answers `400 / 11128 "first message is not
|
|
1190
|
+
* system prompt"` on `www.workbuddy.ai`, while the identical body is
|
|
1191
|
+
* accepted on the domestic gateway. Same code, different cause — the
|
|
1192
|
+
* domestic gateway uses 11128 for the `developer` role and for the
|
|
1193
|
+
* content fingerprint, which is why nothing may branch on the code alone.
|
|
1194
|
+
* A placeholder assistant prompt is prepended, never merged: every
|
|
1195
|
+
* caller-supplied message keeps its position and wording.
|
|
1196
|
+
*
|
|
1197
|
+
* 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
|
|
1198
|
+
* `thinkingLevelMap.off` to the literal `'off'` so the level stays
|
|
1199
|
+
* selectable, and pi-ai emits that value for any request carrying no
|
|
1200
|
+
* explicit level — so "Default" and "Off" produce the same body.
|
|
1201
|
+
* Measured: the international GPT family answers `400 / 11133`
|
|
1202
|
+
* (`extError.param === 'reasoning.effort'`), and the domestic gateway
|
|
1203
|
+
* rejects it on models that declare `onlyReasoning`. Omission is the only
|
|
1204
|
+
* spelling measured good everywhere; a literal `'none'` is NOT a safe
|
|
1205
|
+
* substitute (accepted by the GPT-5.6 family, rejected by others).
|
|
1206
|
+
*
|
|
1207
|
+
* Both rules are no-ops on a body that already complies, so this is safe to
|
|
1208
|
+
* run over an already-prepared body.
|
|
1209
|
+
*/
|
|
1210
|
+
prepareChatBodyFor(raw, region) {
|
|
1211
|
+
const prepared = this.prepareChatBody(raw);
|
|
1212
|
+
let body;
|
|
1213
|
+
try {
|
|
1214
|
+
body = JSON.parse(prepared);
|
|
1215
|
+
} catch {
|
|
1216
|
+
return prepared;
|
|
1217
|
+
}
|
|
1218
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return prepared;
|
|
1219
|
+
const obj = body;
|
|
1220
|
+
if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
|
|
1221
|
+
if (region === "global") {
|
|
1222
|
+
const messages = obj["messages"];
|
|
1223
|
+
if (Array.isArray(messages)) {
|
|
1224
|
+
const first = messages[0];
|
|
1225
|
+
if (!(typeof first === "object" && first !== null && !Array.isArray(first) && first["role"] === "system")) messages.unshift({
|
|
1226
|
+
role: "system",
|
|
1227
|
+
content: "You are a helpful assistant."
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return JSON.stringify(obj);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1012
1234
|
* Parse a raw OpenAI chat body without normalising it.
|
|
1013
1235
|
*
|
|
1014
1236
|
* The compactor needs the message array as objects, while `chatStream` only
|
|
@@ -1024,12 +1246,20 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1024
1246
|
if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
|
|
1025
1247
|
return body;
|
|
1026
1248
|
}
|
|
1027
|
-
/**
|
|
1028
|
-
|
|
1029
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* Re-serialise `base` with a rewritten `messages` array, still normalised.
|
|
1251
|
+
*
|
|
1252
|
+
* `region` is optional so the composite lanes (aggregate / free) can keep
|
|
1253
|
+
* calling it without one; when supplied, the region-only rules of
|
|
1254
|
+
* {@link prepareChatBodyFor} ride along. Both in-repo callers are the
|
|
1255
|
+
* compaction paths, which always know their shim's region.
|
|
1256
|
+
*/
|
|
1257
|
+
buildChatBody(base, messages, region) {
|
|
1258
|
+
const raw = JSON.stringify({
|
|
1030
1259
|
...base,
|
|
1031
1260
|
messages
|
|
1032
|
-
})
|
|
1261
|
+
});
|
|
1262
|
+
return region === void 0 ? this.prepareChatBody(raw) : this.prepareChatBodyFor(raw, region);
|
|
1033
1263
|
}
|
|
1034
1264
|
/**
|
|
1035
1265
|
* Run one NON-streaming completion and return the assistant text.
|
|
@@ -1039,13 +1269,10 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1039
1269
|
* single string. Throws on any failure: the compactor then falls back to
|
|
1040
1270
|
* plain truncation rather than failing the user's turn.
|
|
1041
1271
|
*/
|
|
1042
|
-
async completeChat(credential, prepared, signal) {
|
|
1272
|
+
async completeChat(credential, prepared, signal, conversationKey) {
|
|
1043
1273
|
const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
1044
1274
|
method: "POST",
|
|
1045
|
-
headers:
|
|
1046
|
-
...chatHeaders(credential),
|
|
1047
|
-
...CHAT_IDENTITY_HEADERS
|
|
1048
|
-
},
|
|
1275
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
1049
1276
|
body: prepared,
|
|
1050
1277
|
...signal === void 0 ? {} : { signal }
|
|
1051
1278
|
});
|
|
@@ -1057,15 +1284,12 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1057
1284
|
return await readCompletionText(response.body);
|
|
1058
1285
|
}
|
|
1059
1286
|
/** Forward one chat completion. Never throws for upstream failures. */
|
|
1060
|
-
async chatStream(credential, prepared, signal) {
|
|
1287
|
+
async chatStream(credential, prepared, signal, conversationKey) {
|
|
1061
1288
|
let response;
|
|
1062
1289
|
try {
|
|
1063
1290
|
response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
1064
1291
|
method: "POST",
|
|
1065
|
-
headers:
|
|
1066
|
-
...chatHeaders(credential),
|
|
1067
|
-
...CHAT_IDENTITY_HEADERS
|
|
1068
|
-
},
|
|
1292
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
1069
1293
|
body: prepared,
|
|
1070
1294
|
...signal === void 0 ? {} : { signal }
|
|
1071
1295
|
});
|
|
@@ -3501,8 +3725,17 @@ var WorkBuddyAccountPool = class {
|
|
|
3501
3725
|
});
|
|
3502
3726
|
}
|
|
3503
3727
|
}
|
|
3728
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
3729
|
+
for (const account of byId.values()) {
|
|
3730
|
+
const uid = account.credential.uid;
|
|
3731
|
+
if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
|
|
3732
|
+
}
|
|
3504
3733
|
for (const credential of found) {
|
|
3505
|
-
const
|
|
3734
|
+
const byIdentity = workbuddyAccountId(credential);
|
|
3735
|
+
const uid = credential.uid;
|
|
3736
|
+
const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
|
|
3737
|
+
const id = knownId ?? byIdentity;
|
|
3738
|
+
if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
|
|
3506
3739
|
const existing = byId.get(id);
|
|
3507
3740
|
if (existing === void 0) {
|
|
3508
3741
|
byId.set(id, {
|
|
@@ -10043,9 +10276,22 @@ function stringList$2(value) {
|
|
|
10043
10276
|
}
|
|
10044
10277
|
return out;
|
|
10045
10278
|
}
|
|
10046
|
-
/**
|
|
10279
|
+
/**
|
|
10280
|
+
* The combo id a name slugifies to, or '' when the name cannot form one.
|
|
10281
|
+
*
|
|
10282
|
+
* Unicode-aware on purpose. This used to be `/[^a-z0-9]+/g`, which meant any
|
|
10283
|
+
* name that was not ASCII — every Chinese chain name — slugged to the empty
|
|
10284
|
+
* string, and `normalizeCombos` then dropped the entry with a bare `continue`.
|
|
10285
|
+
* The card reported "saved", the store held nothing, and the chain vanished on
|
|
10286
|
+
* the next read: the "new route cannot be saved" report. A name is user data and
|
|
10287
|
+
* may be written in any script, so the id has to survive one.
|
|
10288
|
+
*
|
|
10289
|
+
* Latin names keep the old shape (`writing-fallback`), because ids already
|
|
10290
|
+
* stored that way must keep resolving; only characters that used to be stripped
|
|
10291
|
+
* to nothing are now kept.
|
|
10292
|
+
*/
|
|
10047
10293
|
function comboId(name) {
|
|
10048
|
-
const slug = text$6(name).toLowerCase().replace(/[
|
|
10294
|
+
const slug = text$6(name).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48).replace(/-+$/u, "");
|
|
10049
10295
|
return slug === "" ? "" : `${COMBO_PREFIX}${slug}`;
|
|
10050
10296
|
}
|
|
10051
10297
|
/** True when the id names a combo row. */
|
|
@@ -10068,7 +10314,9 @@ function isComboRow(row) {
|
|
|
10068
10314
|
*
|
|
10069
10315
|
* A chain with no members is DROPPED rather than kept as an empty row: a model
|
|
10070
10316
|
* that cannot answer anything is not a model, and showing one in the picker
|
|
10071
|
-
* only produces a request that fails with nothing to fix.
|
|
10317
|
+
* only produces a request that fails with nothing to fix. The card warns about
|
|
10318
|
+
* that case before the save (`route.noMembers`), which is why dropping here is
|
|
10319
|
+
* not a silent loss.
|
|
10072
10320
|
*/
|
|
10073
10321
|
function normalizeCombos(raw) {
|
|
10074
10322
|
if (!Array.isArray(raw)) return [];
|
|
@@ -11596,7 +11844,6 @@ const NO_COST = {
|
|
|
11596
11844
|
cacheRead: 0,
|
|
11597
11845
|
cacheWrite: 0
|
|
11598
11846
|
};
|
|
11599
|
-
/** pi-ai input modalities: images only when the catalog advertises them. */
|
|
11600
11847
|
function modelInput(info) {
|
|
11601
11848
|
return info.supportsImages ? ["text", "image"] : ["text"];
|
|
11602
11849
|
}
|
|
@@ -11773,26 +12020,44 @@ function createWorkBuddyAdapter(options) {
|
|
|
11773
12020
|
if (aggregateCatalog) return [...catalog.providerRows().map((row) => toAggregateModel(row, baseUrl, providerId)), ...catalog.comboRows().map((row) => toComboModel(row, baseUrl, providerId))];
|
|
11774
12021
|
return catalog.visible().map((info) => toPiModel(info, baseUrl, providerId));
|
|
11775
12022
|
};
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
12023
|
+
/**
|
|
12024
|
+
* Build a fresh provider object whose `models` array is the catalog as it
|
|
12025
|
+
* stands right now.
|
|
12026
|
+
*
|
|
12027
|
+
* `createProvider` snapshots the array it is handed, and the host keeps its
|
|
12028
|
+
* own snapshot keyed by the IDENTITY of the `profiles` map
|
|
12029
|
+
* (`dsh-llm-pi-ai/lib/index.js` `PiAiAdapter.current()`: it rebuilds the
|
|
12030
|
+
* model collection only when `this.snapshot.profiles !== profiles`, then
|
|
12031
|
+
* calls `models.setProvider(profile.piProvider)`). Reusing one provider object
|
|
12032
|
+
* across `invalidate()` therefore could not work: the host would recognise the
|
|
12033
|
+
* unchanged profiles map, hand back its cached snapshot, and re-collect the
|
|
12034
|
+
* very same provider — so a model list that changed upstream (a new model
|
|
12035
|
+
* released, an account's entitlements changing) stayed frozen until restart.
|
|
12036
|
+
* That is what users saw on the international gateway. Returning a brand-new
|
|
12037
|
+
* provider from a brand-new profile is the only lever the adapter is given.
|
|
12038
|
+
*/
|
|
12039
|
+
const makeProvider = () => {
|
|
12040
|
+
return {
|
|
12041
|
+
...createProvider({
|
|
12042
|
+
id: providerId,
|
|
12043
|
+
name: displayName,
|
|
12044
|
+
auth: { apiKey: {
|
|
12045
|
+
name: "XD Pool loopback secret",
|
|
12046
|
+
async resolve({ credential }) {
|
|
12047
|
+
const apiKey = credential?.key;
|
|
12048
|
+
return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
|
|
12049
|
+
auth: { apiKey },
|
|
12050
|
+
source: "XD Pool"
|
|
12051
|
+
};
|
|
12052
|
+
}
|
|
12053
|
+
} },
|
|
12054
|
+
models: buildModels(),
|
|
12055
|
+
api: openAICompletionsApi()
|
|
12056
|
+
}),
|
|
12057
|
+
getModels: () => buildModels()
|
|
12058
|
+
};
|
|
11794
12059
|
};
|
|
11795
|
-
const
|
|
12060
|
+
const makeProfile = (piProvider) => ({
|
|
11796
12061
|
provider: providerId,
|
|
11797
12062
|
displayName,
|
|
11798
12063
|
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -11800,8 +12065,9 @@ function createWorkBuddyAdapter(options) {
|
|
|
11800
12065
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
11801
12066
|
modelErrors: /* @__PURE__ */ new Map(),
|
|
11802
12067
|
...REQUEST_IMAGE_BUDGETS,
|
|
11803
|
-
piProvider
|
|
11804
|
-
};
|
|
12068
|
+
piProvider
|
|
12069
|
+
});
|
|
12070
|
+
const profile = makeProfile(makeProvider());
|
|
11805
12071
|
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
11806
12072
|
return {
|
|
11807
12073
|
providerId,
|
|
@@ -11815,7 +12081,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
11815
12081
|
}),
|
|
11816
12082
|
buildModels,
|
|
11817
12083
|
invalidate: () => {
|
|
11818
|
-
profiles = /* @__PURE__ */ new Map([[providerId,
|
|
12084
|
+
profiles = /* @__PURE__ */ new Map([[providerId, makeProfile(makeProvider())]]);
|
|
11819
12085
|
}
|
|
11820
12086
|
};
|
|
11821
12087
|
}
|
|
@@ -11968,7 +12234,7 @@ function readPackAccountPool(ctx) {
|
|
|
11968
12234
|
*/
|
|
11969
12235
|
async function readPackCredentials(pool) {
|
|
11970
12236
|
if (pool === void 0) return [];
|
|
11971
|
-
const entries = pool.listAllAccounts().filter((entry) => {
|
|
12237
|
+
const entries = (await pool.listAllAccounts()).filter((entry) => {
|
|
11972
12238
|
const provider = text$3(entry.provider);
|
|
11973
12239
|
return provider !== void 0 && PACK_REGION_CHANNELS.includes(provider);
|
|
11974
12240
|
});
|
|
@@ -13330,6 +13596,24 @@ function freeFetch(url, init = {}) {
|
|
|
13330
13596
|
});
|
|
13331
13597
|
}
|
|
13332
13598
|
/**
|
|
13599
|
+
* Add the one fact a 429 on this lane can never convey on its own.
|
|
13600
|
+
*
|
|
13601
|
+
* The free tier meters quota PER UPSTREAM SESSION, not per key and not per
|
|
13602
|
+
* account — the gateway's own sentence is "the provider asked to wait 74558s",
|
|
13603
|
+
* i.e. twenty hours, which reads like a ban on the whole provider. It is not:
|
|
13604
|
+
* a different conversation maps to a different session and is served
|
|
13605
|
+
* immediately. Saying so costs one clause and turns a message that looks like
|
|
13606
|
+
* "this lane is over" into one that tells the user what to do next.
|
|
13607
|
+
*
|
|
13608
|
+
* Only a rate class gets the hint: a dead credential or a missing model has
|
|
13609
|
+
* nothing to do with the session budget, and adding the clause there would
|
|
13610
|
+
* invite the user to retry something that cannot succeed.
|
|
13611
|
+
*/
|
|
13612
|
+
function withSessionQuotaHint(error, retryAfterMs) {
|
|
13613
|
+
if (!(error instanceof AggregateError) || error.kind !== "soft_rate" || retryAfterMs === void 0 || error.message.includes("per conversation")) return error;
|
|
13614
|
+
return new AggregateError(error.kind, `${error.message} — this quota is counted per conversation on the free tier, so a new conversation (or another model) starts with its own budget`, error.status);
|
|
13615
|
+
}
|
|
13616
|
+
/**
|
|
13333
13617
|
* POST one streaming turn and hand back chat-shaped deltas.
|
|
13334
13618
|
*
|
|
13335
13619
|
* The shape of the ANSWER decides how it is read, never its Content-Type: a front
|
|
@@ -13373,7 +13657,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
|
|
|
13373
13657
|
const retryAfter = retryAfterMs(response);
|
|
13374
13658
|
if (!response.ok) {
|
|
13375
13659
|
const refusal = await response.text().catch(() => "");
|
|
13376
|
-
throw classifyUpstream(response.status, refusal, retryAfter, "");
|
|
13660
|
+
throw withSessionQuotaHint(classifyUpstream(response.status, refusal, retryAfter, ""), retryAfter);
|
|
13377
13661
|
}
|
|
13378
13662
|
if (response.body === null) throw new AggregateError("server", "the free lane returned no body", response.status);
|
|
13379
13663
|
let events;
|
|
@@ -13406,7 +13690,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
|
|
|
13406
13690
|
} catch {
|
|
13407
13691
|
continue;
|
|
13408
13692
|
}
|
|
13409
|
-
if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw classifyUpstream(response.status, event, retryAfter, "");
|
|
13693
|
+
if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw withSessionQuotaHint(classifyUpstream(response.status, event, retryAfter, ""), retryAfter);
|
|
13410
13694
|
const translated = chatChunksFor(wire, decoded, tools);
|
|
13411
13695
|
for (const delta of translated.frames) hooks.onDelta(delta);
|
|
13412
13696
|
if (translated.usage !== void 0) usage = translated.usage;
|
|
@@ -18204,6 +18488,7 @@ function createWorkBuddyShim(options) {
|
|
|
18204
18488
|
}
|
|
18205
18489
|
const attempts = [];
|
|
18206
18490
|
let last;
|
|
18491
|
+
const conversationKey = conversationKeyOf(raw);
|
|
18207
18492
|
for (const member of members) {
|
|
18208
18493
|
if (signal.aborted) return;
|
|
18209
18494
|
attempts.push(member.modelId);
|
|
@@ -18218,8 +18503,8 @@ function createWorkBuddyShim(options) {
|
|
|
18218
18503
|
};
|
|
18219
18504
|
continue;
|
|
18220
18505
|
}
|
|
18221
|
-
const prepared = client.
|
|
18222
|
-
const result = await client.chatStream(account.credential, prepared, signal);
|
|
18506
|
+
const prepared = client.prepareChatBodyFor(raw, region ?? "cn");
|
|
18507
|
+
const result = await client.chatStream(account.credential, prepared, signal, conversationKey);
|
|
18223
18508
|
if (result.ok) {
|
|
18224
18509
|
const turnId = randomBytes(8).toString("hex");
|
|
18225
18510
|
await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageRecorder === void 0 ? void 0 : {
|
|
@@ -18342,7 +18627,7 @@ function createWorkBuddyShim(options) {
|
|
|
18342
18627
|
writeOpenAIError(res, 503, `${lane}_unavailable`, lane === "free" ? `${modelId} is not available right now; the free lane's model listing is refreshed on the XD Pool card` : `${modelId} is not available right now; sign in to the co-paid lane on the XD Pool card`);
|
|
18343
18628
|
return;
|
|
18344
18629
|
}
|
|
18345
|
-
const prepared = client.
|
|
18630
|
+
const prepared = client.prepareChatBodyFor(raw, region ?? "cn");
|
|
18346
18631
|
const conversationKey = conversationKeyOf(raw);
|
|
18347
18632
|
const tried = [];
|
|
18348
18633
|
let last;
|
|
@@ -18381,7 +18666,7 @@ function createWorkBuddyShim(options) {
|
|
|
18381
18666
|
return;
|
|
18382
18667
|
}
|
|
18383
18668
|
tried.push(account.label);
|
|
18384
|
-
const result = await client.chatStream(account.credential, prepared, controller.signal);
|
|
18669
|
+
const result = await client.chatStream(account.credential, prepared, controller.signal, conversationKey);
|
|
18385
18670
|
if (result.ok) {
|
|
18386
18671
|
await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageContext);
|
|
18387
18672
|
return;
|
|
@@ -18639,8 +18924,8 @@ async function recoverFromContextOverrun(options) {
|
|
|
18639
18924
|
...parsed,
|
|
18640
18925
|
stream: true,
|
|
18641
18926
|
max_tokens: Math.max(256, Math.floor(budget / 2))
|
|
18642
|
-
}, request);
|
|
18643
|
-
return await client.completeChat(summariser.credential, body, signal ?? controller.signal);
|
|
18927
|
+
}, request, region ?? "cn");
|
|
18928
|
+
return await client.completeChat(summariser.credential, body, signal ?? controller.signal, conversationKeyOf(raw));
|
|
18644
18929
|
} }, controller.signal);
|
|
18645
18930
|
compacted = outcome.messages;
|
|
18646
18931
|
summary = outcome.summary;
|
|
@@ -18654,7 +18939,7 @@ async function recoverFromContextOverrun(options) {
|
|
|
18654
18939
|
ok: false,
|
|
18655
18940
|
detail: compactionDetail === "" ? "compaction could not reduce the prompt" : compactionDetail
|
|
18656
18941
|
};
|
|
18657
|
-
const retryBody = client.buildChatBody(parsed, compacted);
|
|
18942
|
+
const retryBody = client.buildChatBody(parsed, compacted, region ?? "cn");
|
|
18658
18943
|
const tried = [];
|
|
18659
18944
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
18660
18945
|
if (controller.signal.aborted) return {
|
|
@@ -18667,7 +18952,7 @@ async function recoverFromContextOverrun(options) {
|
|
|
18667
18952
|
detail: "no account available after compaction"
|
|
18668
18953
|
};
|
|
18669
18954
|
tried.push(account.label);
|
|
18670
|
-
const result = await client.chatStream(account.credential, retryBody, controller.signal);
|
|
18955
|
+
const result = await client.chatStream(account.credential, retryBody, controller.signal, conversationKeyOf(raw));
|
|
18671
18956
|
if (!result.ok) usage?.recorder.record({
|
|
18672
18957
|
region: usage.region,
|
|
18673
18958
|
model: usage.model,
|
|
@@ -20415,6 +20700,19 @@ function apply(ctx, config = {}) {
|
|
|
20415
20700
|
adaptersByRegion.global.invalidate();
|
|
20416
20701
|
adaptersByRegion.aggregate.invalidate();
|
|
20417
20702
|
adaptersByRegion.free.invalidate();
|
|
20703
|
+
for (const [handle, routes] of [
|
|
20704
|
+
[releaseAdapterCn, [POOL_PROVIDER_BY_REGION.cn]],
|
|
20705
|
+
[releaseAdapterGlobal, [POOL_PROVIDER_BY_REGION.global]],
|
|
20706
|
+
[releaseAdapterAggregate, [XDPOOL_AGGREGATE_PROVIDER]],
|
|
20707
|
+
[releaseAdapterFree, [XDPOOL_FREE_PROVIDER]]
|
|
20708
|
+
]) {
|
|
20709
|
+
if (handle === void 0) continue;
|
|
20710
|
+
try {
|
|
20711
|
+
handle.replace([...routes]);
|
|
20712
|
+
} catch (error) {
|
|
20713
|
+
ctx.logger.warn("dsh-xdpool: could not re-announce adapter routes", error);
|
|
20714
|
+
}
|
|
20715
|
+
}
|
|
20418
20716
|
};
|
|
20419
20717
|
aggregateCatalog.onChange(() => {
|
|
20420
20718
|
invalidateCatalog();
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-xdpool",
|
|
3
3
|
"displayName": "DSH XD Pool",
|
|
4
4
|
"description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.4.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-xdpool#readme",
|
|
@@ -34239,7 +34239,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34239
34239
|
name: "GLM-5.1",
|
|
34240
34240
|
contextWindow: 2e5,
|
|
34241
34241
|
supportsImages: true,
|
|
34242
|
-
|
|
34242
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34243
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34244
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34245
|
+
// the picker.
|
|
34246
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34243
34247
|
maxOutputTokens: 48e3
|
|
34244
34248
|
},
|
|
34245
34249
|
{
|
|
@@ -34247,7 +34251,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34247
34251
|
name: "GLM-5V-Turbo",
|
|
34248
34252
|
contextWindow: 2e5,
|
|
34249
34253
|
supportsImages: true,
|
|
34250
|
-
|
|
34254
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34255
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34256
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34257
|
+
// the picker.
|
|
34258
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34251
34259
|
maxOutputTokens: 64e3
|
|
34252
34260
|
},
|
|
34253
34261
|
{
|
|
@@ -34255,7 +34263,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34255
34263
|
name: "Kimi-K3-1",
|
|
34256
34264
|
contextWindow: 1e6,
|
|
34257
34265
|
supportsImages: true,
|
|
34258
|
-
|
|
34266
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34267
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34268
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34269
|
+
// the picker.
|
|
34270
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34259
34271
|
maxOutputTokens: 32e3
|
|
34260
34272
|
},
|
|
34261
34273
|
{
|
|
@@ -34274,7 +34286,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34274
34286
|
name: "Kimi-K2.7",
|
|
34275
34287
|
contextWindow: 256e3,
|
|
34276
34288
|
supportsImages: true,
|
|
34277
|
-
|
|
34289
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34290
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34291
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34292
|
+
// the picker.
|
|
34293
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34278
34294
|
maxOutputTokens: 32e3
|
|
34279
34295
|
},
|
|
34280
34296
|
{
|
|
@@ -34282,7 +34298,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34282
34298
|
name: "Kimi-K2.6",
|
|
34283
34299
|
contextWindow: 256e3,
|
|
34284
34300
|
supportsImages: true,
|
|
34285
|
-
|
|
34301
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34302
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34303
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34304
|
+
// the picker.
|
|
34305
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34286
34306
|
maxOutputTokens: 32e3
|
|
34287
34307
|
},
|
|
34288
34308
|
{
|
|
@@ -34290,7 +34310,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
|
|
|
34290
34310
|
name: "MiniMax-M3",
|
|
34291
34311
|
contextWindow: 512e3,
|
|
34292
34312
|
supportsImages: true,
|
|
34293
|
-
|
|
34313
|
+
// Measured 2026-10-21 against the live gateway: all five levels answered
|
|
34314
|
+
// 200 with a distinct reasoning_content length per level. The single level
|
|
34315
|
+
// this used to declare was a guess, and it hid four working levels from
|
|
34316
|
+
// the picker.
|
|
34317
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34294
34318
|
maxOutputTokens: 64e3
|
|
34295
34319
|
}
|
|
34296
34320
|
];
|
|
@@ -34316,8 +34340,8 @@ var WORKBUDDY_FALLBACK_MODELS = [
|
|
|
34316
34340
|
// (`node scripts/dump-max-output.mjs`)。该值就是用户在 IDE 里实际拿到的
|
|
34317
34341
|
// 单次输出额度,远端不可用时由本表顶替。远端未下发的模型保持 undefined。
|
|
34318
34342
|
{ id: "default-model", name: "Auto", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
|
|
34319
|
-
{ id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
|
|
34320
|
-
{ id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
|
|
34343
|
+
{ id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
|
|
34344
|
+
{ id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
|
|
34321
34345
|
{ id: "primary-model", name: "Primary", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["high"], maxOutputTokens: 72e3 },
|
|
34322
34346
|
{ id: "deep-model", name: "Deep", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
|
|
34323
34347
|
{
|
|
@@ -34349,14 +34373,14 @@ var WORKBUDDY_FALLBACK_MODELS = [
|
|
|
34349
34373
|
defaultReasoningEffort: "high",
|
|
34350
34374
|
maxOutputTokens: 64e3
|
|
34351
34375
|
},
|
|
34352
|
-
{ id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["high"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
|
|
34376
|
+
{ id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
|
|
34353
34377
|
{
|
|
34354
34378
|
// 2026-09 补录:新加坡区的同代模型(-sg 后缀),远端下发且实测能看图。
|
|
34355
34379
|
id: "deepseek-v4.1-flash-sg",
|
|
34356
34380
|
name: "Deepseek-V4.1-Flash",
|
|
34357
34381
|
contextWindow: 1e6,
|
|
34358
34382
|
supportsImages: true,
|
|
34359
|
-
reasoningEfforts: ["high"],
|
|
34383
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
34360
34384
|
defaultReasoningEffort: "high",
|
|
34361
34385
|
maxOutputTokens: 128e3
|
|
34362
34386
|
},
|
|
@@ -34416,7 +34440,11 @@ var WORKBUDDY_FALLBACK_MODELS = [
|
|
|
34416
34440
|
},
|
|
34417
34441
|
// gpt-5.3-codex:远端未下发 maxOutputTokens,故不填(保持 undefined,
|
|
34418
34442
|
// 交由网关默认),不臆造数值。
|
|
34419
|
-
|
|
34443
|
+
// gpt-5.3-codex answered 200 at every level, but returned an EMPTY
|
|
34444
|
+
// reasoning_content at all five — the levels are legal yet not observably
|
|
34445
|
+
// distinct. Declaring the ladder states what may be sent; it does not
|
|
34446
|
+
// claim the model thinks.
|
|
34447
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"] },
|
|
34420
34448
|
{ id: "gemini-3.5-flash", name: "Gemini-3.5-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 65536 },
|
|
34421
34449
|
{
|
|
34422
34450
|
id: "glm-5.3",
|
|
@@ -34436,7 +34464,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
|
|
|
34436
34464
|
defaultReasoningEffort: "high",
|
|
34437
34465
|
maxOutputTokens: 48e3
|
|
34438
34466
|
},
|
|
34439
|
-
{ id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
|
|
34467
|
+
{ id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
|
|
34440
34468
|
{
|
|
34441
34469
|
// 2026-09 补录:远端 /v3/config 的 cli agent 白名单里有它,兜底表原先漏了。
|
|
34442
34470
|
id: "kimi-k2.8-preview",
|
|
@@ -34447,7 +34475,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
|
|
|
34447
34475
|
defaultReasoningEffort: "high",
|
|
34448
34476
|
maxOutputTokens: 32e3
|
|
34449
34477
|
},
|
|
34450
|
-
{ id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 }
|
|
34478
|
+
{ id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 }
|
|
34451
34479
|
];
|
|
34452
34480
|
var WORKBUDDY = {
|
|
34453
34481
|
id: "workbuddy",
|