dsh-multi-router 0.6.4 → 0.6.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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/cordis.patch.yml +2 -2
- package/lib/accounts.js +3 -3
- package/lib/{chunk-R3UQQMSV.js → chunk-6GEU5EGE.js} +3 -3
- package/lib/{chunk-OK2T2UAZ.js → chunk-FLL3DUVP.js} +2 -2
- package/lib/{chunk-TEZ2FMEO.js → chunk-KWXLQJS5.js} +13 -2
- package/lib/chunk-KWXLQJS5.js.map +7 -0
- package/lib/{chunk-BVH4K6WH.js → chunk-PI3XU3SB.js} +3 -3
- package/lib/{chunk-3U6KCQEP.js → chunk-VDDKRMZK.js} +5 -5
- package/lib/{chunk-3U6KCQEP.js.map → chunk-VDDKRMZK.js.map} +1 -1
- package/lib/cli.js +4 -4
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/clients.js +3 -3
- package/lib/{host-v0.6.4.js → host-v0.6.6.js} +5 -5
- package/lib/host.js +5 -5
- package/lib/index.js +5 -5
- package/lib/manager.js +5 -5
- package/lib/{upstream-42RS7F4H.js → upstream-VP6J7LU2.js} +2 -2
- package/package.json +12 -12
- package/lib/chunk-TEZ2FMEO.js.map +0 -7
- /package/lib/{chunk-R3UQQMSV.js.map → chunk-6GEU5EGE.js.map} +0 -0
- /package/lib/{chunk-OK2T2UAZ.js.map → chunk-FLL3DUVP.js.map} +0 -0
- /package/lib/{chunk-BVH4K6WH.js.map → chunk-PI3XU3SB.js.map} +0 -0
- /package/lib/{upstream-42RS7F4H.js.map → upstream-VP6J7LU2.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
|
|
4
4
|
|
|
5
|
+
## 0.6.6
|
|
6
|
+
|
|
7
|
+
- 修复 WorkBuddy 国际版(`workbuddy-ai` / `www.workbuddy.ai`)聊天请求固定返回 HTTP 400 空响应:网关要求请求携带 `X-Conversation-ID`、`X-Conversation-Request-ID`、`X-Conversation-Message-ID` 三件套,每个请求使用新的 UUID。国内版实测同样接受该三件套,因此两个区域统一发送,避免两条路径漂移。感谢 @YGtemple 提供的诊断与实测数据(#2)。
|
|
8
|
+
- 集成测试新增该三件套的逐请求断言;此前仓库内没有任何 `X-Conversation` 覆盖,改动失效时不会有测试报警。
|
|
9
|
+
- 该实现对应上游 `dsh-workbuddy-connect` 新增的 `conversationHeaders()`,本项目固定的 `v0.7.3`(`caad7a94`)尚不含此实现,改动已在 `THIRD_PARTY_NOTICES.md` 的本地集成改动中登记;升级 `upstream-lock.json` 时应删除该本地改动。
|
|
10
|
+
|
|
11
|
+
## 0.6.5
|
|
12
|
+
|
|
13
|
+
- 修复在 DSH 0.2.1-rc.2 上被判定为不兼容、无法启用的问题:五个 DSH peer 包由精确 `0.2.0-rc.2` 改为 `^0.2.0-rc.2`,同时兼容 0.2.0-rc.2 与 0.2.1-rc.2,不再需要版本豁免。
|
|
14
|
+
- 开发与回归依赖升级到 DSH 0.2.1-rc.2,编译与测试改为针对当前运行时,避免再次只验证旧版本。
|
|
15
|
+
- 插件自带的 `@earendil-works/pi-ai` 保持 0.87.1:DSH 0.2.1-rc.2 的宿主侧已升级到 1.1.0,实测 1.1.0 的 `Models` 容器驱动 0.87.1 provider 时,文本、用量与停止原因均正确;`pi-ai-guard` 的说明同步改写,不再声称本插件与宿主使用同一份 pi-ai。
|
|
16
|
+
|
|
5
17
|
## 0.6.4
|
|
6
18
|
|
|
7
19
|
- 路由编辑的上下文容量与默认思考深度合并至名称下方的「模型参数」区域;宽窗口并排、窄窗口上下排列,容量变化后仍自动核对可用思考档位。
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
一个插件内置 WorkBuddy、Trae、Qoder 的认证、模型发现和聊天传输,提供三家的多账号池及集中路由。运行不依赖原来三个 DSH 插件;三家代码随安装包一起打包。基于 MIT 上游,固定版本见 `upstream-lock.json`,许可见 `THIRD_PARTY_NOTICES.md`。
|
|
8
8
|
|
|
9
|
-
版本:0.6.
|
|
9
|
+
版本:0.6.6。适配 DSH Desktop / Web 0.2.1-rc.2(同时兼容 0.2.0-rc.2),Node.js ^22.19.0 或 >=24。
|
|
10
10
|
|
|
11
11
|
## 在 DSH 里管理
|
|
12
12
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
## 本地集成改动 / Local Integration Changes
|
|
19
19
|
|
|
20
20
|
- WorkBuddy 的共享适配器额外透传集成后 provider 目录中**显式声明**的 Minimal 推理模式;WorkBuddy 原有的模型发现逻辑与 provider 专有的档位词表未作改动。
|
|
21
|
+
- WorkBuddy 的 chat / probe 请求补回 `X-Conversation-ID`、`X-Conversation-Request-ID`、`X-Conversation-Message-ID` 三件套(每请求新 UUID,两个区域都发送),否则国际版网关对完整 chat 请求返回 400。此处对应上游 `dsh-workbuddy-connect` 后续版本新增的 `conversationHeaders()`;本项目固定的 `caad7a94`(v0.7.3)尚不含该实现,因此在 vendored 副本上做了等价改动。**升级 `upstream-lock.json` 到含该实现的上游版本时,应删除此处本地改动以免重复。**
|
|
21
22
|
- 三家代码被打包进同一个插件宿主,账号池、路由与用量统计为本项目新增实现。
|
|
22
23
|
|
|
23
24
|
---
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- insert:
|
|
2
|
-
- id: llm-multi-router-v2-0.6.
|
|
3
|
-
name: ./lib/host-v0.6.
|
|
2
|
+
- id: llm-multi-router-v2-0.6.6
|
|
3
|
+
name: ./lib/host-v0.6.6.js
|
package/lib/accounts.js
CHANGED
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
localAccountKey,
|
|
5
5
|
normalizeLocalAccountKey,
|
|
6
6
|
readPortableCredential
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-PI3XU3SB.js";
|
|
8
|
+
import "./chunk-FLL3DUVP.js";
|
|
9
|
+
import "./chunk-KWXLQJS5.js";
|
|
10
10
|
import "./chunk-S5KMFAQM.js";
|
|
11
11
|
import "./chunk-PNFQDYBK.js";
|
|
12
12
|
export {
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
WorkBuddyCredentialStore,
|
|
5
5
|
identityHeaders,
|
|
6
6
|
traeAccountId
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FLL3DUVP.js";
|
|
8
8
|
import {
|
|
9
9
|
WorkBuddyUpstreamClient,
|
|
10
10
|
modelWithCurrentPromotion,
|
|
11
11
|
prepareChatBody
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KWXLQJS5.js";
|
|
13
13
|
import {
|
|
14
14
|
REGION_GATEWAYS,
|
|
15
15
|
regionOfCredential
|
|
@@ -5418,4 +5418,4 @@ export {
|
|
|
5418
5418
|
refreshWorkBuddyCheckinCredential,
|
|
5419
5419
|
createWorkBuddyCheckin
|
|
5420
5420
|
};
|
|
5421
|
-
//# sourceMappingURL=chunk-
|
|
5421
|
+
//# sourceMappingURL=chunk-6GEU5EGE.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
regionOf
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KWXLQJS5.js";
|
|
4
4
|
import {
|
|
5
5
|
regionOfCredential,
|
|
6
6
|
regionOfEdition
|
|
@@ -2014,4 +2014,4 @@ export {
|
|
|
2014
2014
|
readTraeIdentity,
|
|
2015
2015
|
identityHeaders
|
|
2016
2016
|
};
|
|
2017
|
-
//# sourceMappingURL=chunk-
|
|
2017
|
+
//# sourceMappingURL=chunk-FLL3DUVP.js.map
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// upstream/workbuddy/src/upstream.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
1
4
|
// upstream/workbuddy/src/app-version.ts
|
|
2
5
|
import { readFile } from "node:fs/promises";
|
|
3
6
|
import { homedir } from "node:os";
|
|
@@ -356,6 +359,13 @@ function commonHeaders(credential) {
|
|
|
356
359
|
"User-Agent": CLIENT_UA
|
|
357
360
|
};
|
|
358
361
|
}
|
|
362
|
+
function conversationHeaders() {
|
|
363
|
+
return {
|
|
364
|
+
"X-Conversation-ID": randomUUID(),
|
|
365
|
+
"X-Conversation-Request-ID": randomUUID(),
|
|
366
|
+
"X-Conversation-Message-ID": randomUUID()
|
|
367
|
+
};
|
|
368
|
+
}
|
|
359
369
|
function chatHeaders(credential, userAgent, clientVersion) {
|
|
360
370
|
const headers = {
|
|
361
371
|
...commonHeaders(credential),
|
|
@@ -368,7 +378,8 @@ function chatHeaders(credential, userAgent, clientVersion) {
|
|
|
368
378
|
"X-IDE-Type": "WorkBuddy",
|
|
369
379
|
"X-IDE-Name": "WorkBuddy",
|
|
370
380
|
"X-IDE-Version": clientVersion,
|
|
371
|
-
"X-Product": "SaaS"
|
|
381
|
+
"X-Product": "SaaS",
|
|
382
|
+
...conversationHeaders()
|
|
372
383
|
};
|
|
373
384
|
return headers;
|
|
374
385
|
}
|
|
@@ -1088,4 +1099,4 @@ export {
|
|
|
1088
1099
|
prepareCnChatBody,
|
|
1089
1100
|
prepareInternationalChatBody
|
|
1090
1101
|
};
|
|
1091
|
-
//# sourceMappingURL=chunk-
|
|
1102
|
+
//# sourceMappingURL=chunk-KWXLQJS5.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../upstream/workbuddy/src/upstream.ts", "../upstream/workbuddy/src/app-version.ts", "../upstream/workbuddy/src/client-identity.ts", "../upstream/workbuddy/src/probe.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * WorkBuddy (CodeBuddy / copilot.tencent.com) upstream client: chat streaming,\n * token refresh, model catalog, and credit balance. The wire behavior is\n * ported from Sliverkiss/workbuddy2api (MIT), whose Go implementation is\n * battle-tested against the real endpoint.\n *\n * @module dsh-workbuddy-connect/upstream\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { appUserAgent, resolveAppVersion, type AppVersionInfo } from './app-version.ts'\nimport { chatUserAgent, fallbackChatIdentity, resolveChatIdentity, type ChatIdentity } from './client-identity.ts'\nimport type { WorkBuddyCredential } from './auth.ts'\nimport type { ProbeAttempt } from './probe.ts'\nimport { PROBE_MAX_TOKENS, PROBE_PROMPT } from './probe.ts'\n\n/** WorkBuddy region selected by the credential's login domain. */\nexport type WorkBuddyRegion = 'cn' | 'global'\n\n/** Upstream failure classes the shim maps onto distinct HTTP answers. */\nexport type UpstreamErrorKind =\n | 'hard_credit'\n | 'soft_rate'\n | 'session_dead'\n | 'not_found'\n | 'server'\n | 'client'\n\n/** One CLI-usable model as the upstream catalog describes it. */\nexport interface WorkBuddyUpstreamModel {\n id: string\n name: string\n contextWindow: number\n /** The upstream's preferred window before an optional maximum is selected. */\n defaultContextWindow?: number\n maxInputTokens?: number\n supportedContextWindows?: readonly number[]\n promotions?: readonly WorkBuddyPromotion[]\n maxTokens: number\n /**\n * Upstream-declared image input capability. Missing or false upstream data\n * resolves to false, so an unknown model stays text-only: over-claiming\n * admits an image the provider then rejects after the message is durable.\n */\n supportsImages: boolean\n /**\n * Reasoning metadata the upstream catalog declares per model. The wire\n * effort values (`low`, `medium`, `high`, `xhigh`, `max`) map directly onto\n * pi-ai's thinking levels, and the supported set decides which levels the\n * DSH model selector offers.\n */\n reasoning?: WorkBuddyModelReasoning\n /**\n * Billing convenience metadata: the credits multiplier string the upstream\n * reports (e.g. `\"x0.00\"` for free) and promotional badges like\n * `badge:\u9650\u65F6\u514D\u8D39:#FF0000` or `badge:\u591C\u95F4\u6298\u6263:#1E90FF`.\n *\n * The multiplier reaches the browser through the host LLM seam, which has no\n * locale service, so {@link normalizeCredits} trims it to a\n * language-neutral display form (`x0.79`) that reads the same in every UI\n * language. The raw upstream string (which may spell `x0.79 credits`) stays\n * on {@link WorkBuddyModelBilling.credits} for diagnostics.\n */\n billing?: WorkBuddyModelBilling\n}\n\n/** Reasoning metadata the upstream catalog declares for one model. */\nexport interface WorkBuddyModelReasoning {\n /** Whether the model does any reasoning at all (upstream `supportsReasoning`). */\n supports: boolean\n /** Whether the model can only think (upstream `onlyReasoning`). */\n onlyReasoning: boolean\n /** Selectable effort values; absent means the model has no explicit set. */\n supportedEfforts?: readonly WorkBuddyEffort[]\n /** Default effort the upstream uses when none is chosen. */\n defaultEffort?: WorkBuddyEffort\n /** Whether thinking can be switched off; false means it is always on. */\n canDisableThinking: boolean\n}\n\n/** The concrete effort spellings WorkBuddy exposes on the wire. */\nexport type WorkBuddyEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'\n\n/** Billing convenience metadata reported for one model. */\nexport interface WorkBuddyModelBilling {\n /** Credits multiplier, e.g. `\"x0.00\"` (free) or `\"x0.79\"`. */\n credits?: string\n /** Promotional tags, e.g. `\"\u9650\u65F6\u514D\u8D39\"`, `\"\u591C\u95F4\u6298\u6263\"`. */\n badges?: readonly string[]\n /** Whether the model is currently free (`x0.00` credits). */\n free: boolean\n /**\n * The rate cannot be stated for this model right now.\n *\n * Set when a row that arrived with promotions attached has no promotion in\n * force: the upstream bakes the discounted value into `credits`, so the\n * cached rate describes a discount that has ended. The original price is not\n * recoverable from the row, so the plugin reports \"unknown, refresh needed\"\n * rather than repeating a figure it can no longer stand behind \u2014 in\n * particular it never keeps claiming the model is free.\n */\n rateUnknown?: boolean\n}\n\n/** One billing package and its remaining credit. */\nexport interface WorkBuddyCreditAccount {\n packageName: string\n remain: number\n size: number\n unlimited?: true\n /**\n * When the package's own cycle ends, in epoch milliseconds.\n *\n * The upstream has always sent this (`CycleEndTime`, a zone-less\n * `YYYY-MM-DD HH:mm:ss` string) and the query itself already filters on\n * `PackageEndTimeRange`, but the field used to be dropped here \u2014 so a card\n * could show \"500 credit\" for a package expiring in an hour with nothing to\n * distinguish it from one expiring next month. Absent when the upstream omits\n * or malforms it, which must never read as \"no expiry\".\n *\n * The zone the upstream means is **not established**: reading the string as\n * the running machine's local time follows the official desktop client (see\n * {@link parseCycleEndTime}), and is not a claim about the account's zone.\n */\n endTimeMs?: number\n}\n\n/** Credit whose package expires within a horizon the user should act on. */\nexport interface WorkBuddyExpiringCredits {\n /** Sum of {@link WorkBuddyCreditAccount.remain} over the window. */\n total: number\n /** How many packages contribute to {@link total}. */\n packages: number\n /** The soonest `endTimeMs` among them. */\n earliestEndTimeMs?: number\n}\n\n/**\n * Credit at risk, bucketed by how soon it lapses.\n *\n * Thresholds are calendar-ish (24h / 3d / 7d) rather than \"this month\": a package\n * expiring at 08:00 tomorrow is not helped by a monthly average, and the upstream\n * genuinely staggers expiries per package.\n *\n * Only *future* horizons are reported. An already-lapsed package is deliberately\n * **not** summarised here: the personal billing query this feeds filters\n * `Status: [0, 3]` (`valid` + `usedUp`) from the current time forward, while the\n * upstream marks a lapsed package `Status: 2` (`expired`) \u2014 a status this query\n * never returns. Historical expiry would need its own query, which is out of\n * scope.\n */\nexport interface WorkBuddyCreditExpiry {\n within24h: WorkBuddyExpiringCredits\n within3d: WorkBuddyExpiringCredits\n within7d: WorkBuddyExpiringCredits\n}\n\n/** Aggregated credit answer for one credential. */\nexport interface WorkBuddyCredits {\n total: number\n accounts: readonly WorkBuddyCreditAccount[]\n /**\n * The account's cycle quota is uncapped (`limitNum === -1` on the CN\n * enterprise endpoint).\n *\n * A separate flag rather than a `-1`/`0` sentinel in {@link total}: the two\n * mean opposite things to a reader (\"no limit\" vs \"nothing left\"), and the\n * existing negative-clamp in the personal branch would turn a sentinel into\n * a plausible-looking zero. Every renderer must therefore test this flag\n * first and not fall back to `total` when it is set.\n */\n unlimited?: true\n cycleResetTime?: string\n /**\n * Credit lapsing soon, so a card can warn instead of only reporting a total.\n *\n * Absent when no account carried a usable end time \u2014 the plugin must not\n * claim \"nothing expires soon\" on the strength of data it never received.\n */\n expiry?: WorkBuddyCreditExpiry\n}\n\n/** Token refresh answer; fields the upstream omits stay absent. */\nexport interface WorkBuddyRefreshOutcome {\n accessToken: string\n refreshToken?: string\n expiresInSec?: number\n domain?: string\n}\n\n/** Chat answer: either a live SSE response or a classified failure. */\nexport type WorkBuddyChatResult =\n | { ok: true; response: Response }\n | { ok: false; status: number; kind: UpstreamErrorKind; message: string }\n\nconst CN_CHAT_BASE = 'https://copilot.tencent.com'\nconst CN_BILLING_BASE = 'https://www.codebuddy.cn'\nconst GLOBAL_BASE = 'https://www.workbuddy.ai'\n\n/**\n * Display name for the single synthetic row the enterprise endpoint produces.\n *\n * The endpoint reports one cycle quota, not the personal endpoint's list of\n * named packages, so the card's \"by package\" table has exactly one row.\n */\nconst enterprisePackageName = 'enterprise'\n\n/**\n * Field names and value types of a response document, for diagnostics.\n *\n * Names and `typeof` only. This string ends up in the status route and then in\n * the browser, and the response describes the account's own usage; the values\n * themselves must never travel. Only the document and its `data` member are\n * described, so the output stays small.\n */\nfunction describeShape(document: unknown): string {\n if (typeof document !== 'object' || document === null || Array.isArray(document)) {\n return typeof document\n }\n const record = document as Record<string, unknown>\n const at = (source: Record<string, unknown>): string => {\n const keys = Object.keys(source).slice(0, 24)\n return keys.length === 0 ? '(empty)' : keys.map(key => `${key}:${typeof source[key]}`).join(', ')\n }\n const top = `top-level { ${at(record)} }`\n const data = record['data']\n if (typeof data !== 'object' || data === null || Array.isArray(data)) return top\n return `${top}; data { ${at(data as Record<string, unknown>)} }`\n}\n\n/** Shared CLI-form User-Agent for refresh and the CN catalog; chat and probe present the desktop identity (client-identity.ts). */\nconst CLIENT_UA = 'CLI/2.63.2 CodeBuddy/2.63.2'\nconst JSON_TIMEOUT_MS = 30_000\nconst ERROR_BODY_LIMIT = 4096\n\n/** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */\nconst HARD_CREDIT_MARKERS: readonly string[] = [\n 'insufficient credit', 'no credit', 'credit exhausted', 'credits exhausted', 'out of credit',\n 'quota exceeded', 'quota exhaust', 'payment required', 'credit not enough',\n 'not enough credit',\n '\u79EF\u5206\u4E0D\u8DB3', '\u989D\u5EA6\u4E0D\u8DB3', '\u4F59\u989D\u4E0D\u8DB3', '\u79EF\u5206\u7528\u5B8C', '\u989D\u5EA6\u7528\u5C3D', '\u6CA1\u6709\u79EF\u5206',\n]\n\n/** The concrete effort spellings WorkBuddy exposes on the wire. */\nconst EFFORT_VALUES: readonly WorkBuddyEffort[] = ['low', 'medium', 'high', 'xhigh', 'max']\n\n/** Promotional badge keys the upstream tags carry, minus their color suffix. */\nconst BADGE_PREFIX = 'badge:'\n\n/** Parse the upstream `reasoning` object into {@link WorkBuddyModelReasoning}. */\nfunction resolveUpstreamReasoning(wrapped: Record<string, unknown>): { reasoning: WorkBuddyModelReasoning } {\n const supports = wrapped['supportsReasoning'] === true\n const onlyReasoning = wrapped['onlyReasoning'] === true\n const rawReasoning = wrapped['reasoning']\n let supportedEfforts: WorkBuddyEffort[] | undefined\n let defaultEffort: WorkBuddyEffort | undefined\n let canDisableThinking = true\n if (typeof rawReasoning === 'object' && rawReasoning !== null && !Array.isArray(rawReasoning)) {\n const reasoning = rawReasoning as Record<string, unknown>\n const rawEfforts = reasoning['supportedEfforts']\n if (Array.isArray(rawEfforts)) {\n const efforts = rawEfforts.filter((value): value is WorkBuddyEffort =>\n typeof value === 'string' && (EFFORT_VALUES as readonly string[]).includes(value))\n if (efforts.length > 0) supportedEfforts = efforts\n }\n if (typeof reasoning['defaultEffort'] === 'string'\n && (EFFORT_VALUES as readonly string[]).includes(reasoning['defaultEffort'] as string)) {\n defaultEffort = reasoning['defaultEffort'] as WorkBuddyEffort\n } else if (typeof reasoning['effort'] === 'string'\n && (EFFORT_VALUES as readonly string[]).includes(reasoning['effort'] as string)) {\n defaultEffort = reasoning['effort'] as WorkBuddyEffort\n }\n // Only an explicit `canDisableThinking: true` offers \"thinking off\"; older\n // rows omit the field and several of them reject `off` on the wire, so the\n // conservative default is \"cannot be disabled\".\n canDisableThinking = reasoning['canDisableThinking'] === true\n }\n return {\n reasoning: {\n supports,\n onlyReasoning,\n ...supportedEfforts === undefined ? {} : { supportedEfforts },\n ...defaultEffort === undefined ? {} : { defaultEffort },\n canDisableThinking,\n },\n }\n}\n\n/**\n * Reduce an upstream credits string to its language-neutral display form.\n *\n * The host LLM seam carries this text to the browser, and the host has no\n * locale service \u2014 whatever string is produced here is shown verbatim in every\n * UI language. The upstream is inconsistent in a way that matters: some catalog\n * rows report a bare multiplier (`x0.79`) and others append a unit word\n * (`x0.79 credits`), and the unit word would pin the display to English.\n * Dropping a trailing `credits` (case-insensitive, singular or plural) yields\n * the one spelling that reads identically in every language.\n *\n * @param credits - raw upstream credits string, e.g. `\"x0.79 credits\"`.\n * @returns the bare multiplier, or undefined when nothing displayable remains.\n */\nexport function normalizeCredits(credits: string | undefined): string | undefined {\n if (credits === undefined) return undefined\n const trimmed = credits.trim()\n if (trimmed === '') return undefined\n // A string that is only the unit word (`credits`) carries no multiplier.\n if (/^credits?$/iu.test(trimmed)) return undefined\n const bare = trimmed.replace(/\\s+credits?$/iu, '').trim()\n return bare === '' ? undefined : bare\n}\n\n/**\n * Parse the upstream `tags` / `credits` fields into billing metadata.\n *\n * @param wrapped - one catalog row.\n * @param extraTags - badge tags recovered from another document, merged in\n * after the row's own. The `/v3/config` product document carries the roster\n * but no `badge:*` tags; those live only in the console catalog, so the CN\n * refresh reads both and joins them here (see `fetchPromoBadges`).\n */\nfunction resolveUpstreamBilling(\n wrapped: Record<string, unknown>,\n extraTags?: readonly string[],\n): { billing: WorkBuddyModelBilling } {\n const rawCredits = wrapped['credits']\n const credits = typeof rawCredits === 'string' && rawCredits.trim() !== '' ? rawCredits.trim() : undefined\n const badges: string[] = []\n const rawTags: readonly unknown[] = [...Array.isArray(wrapped['tags']) ? wrapped['tags'] : [], ...extraTags ?? []]\n for (const tag of rawTags) {\n if (typeof tag !== 'string') continue\n const lowered = tag.toLowerCase()\n if (!lowered.startsWith(BADGE_PREFIX)) continue\n const label = tag.slice(BADGE_PREFIX.length).split(':')[0] ?? tag.slice(BADGE_PREFIX.length)\n if (label !== '' && !badges.includes(label)) badges.push(label)\n }\n // A `x0.00` multiplier means the model is currently free. Judge from the\n // normalized multiplier: the raw string may carry a trailing unit word\n // (`x0.00 credits`) that a bare-multiplier match would never see.\n const multiplier = normalizeCredits(credits)\n const free = multiplier !== undefined && /^x?0\\.0+$/u.test(multiplier)\n return {\n billing: {\n ...credits === undefined ? {} : { credits },\n ...badges.length === 0 ? {} : { badges },\n free,\n },\n }\n}\n\n/** Session-invalidation markers that mean \"sign in again in the WorkBuddy app\". */\nconst SESSION_DEAD_MARKERS: readonly string[] = ['Offline user session not found', '12153']\n\n/** Classify an upstream failure from its HTTP status and body excerpt. */\nexport function classifyUpstreamError(status: number, body: string): UpstreamErrorKind {\n if (status === 402) return 'hard_credit'\n // A 401 is an auth failure whatever its body says: there is no business\n // meaning for it on this upstream, and the session-dead markers below are\n // only an additional signal (they also catch dead sessions answered as\n // 403). Keeping every 401 in `session_dead` preserves the `(http 401)`\n // status note the host needs to classify it as AUTH.\n if (status === 401) return 'session_dead'\n const lower = body.toLowerCase()\n for (const marker of HARD_CREDIT_MARKERS) {\n if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return 'hard_credit'\n }\n for (const marker of SESSION_DEAD_MARKERS) {\n if (body.includes(marker)) return 'session_dead'\n }\n if (status === 429) return 'soft_rate'\n if (status === 404) return 'not_found'\n if (status >= 500) return 'server'\n if (status >= 400) return 'client'\n return 'client'\n}\n\n/**\n * Extract a user-facing error message from an upstream JSON response body.\n * Prefers WorkBuddy's localized `displayMsg.zh` / `displayMsg.en`, falling back to `msg`.\n */\nexport function extractDisplayErrorMessage(body: string): string | undefined {\n const trimmed = body.trim()\n if (!trimmed.startsWith('{')) return undefined\n try {\n const parsed: unknown = JSON.parse(trimmed)\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined\n const obj = parsed as Record<string, unknown>\n const displayMsg = obj['displayMsg']\n if (typeof displayMsg === 'object' && displayMsg !== null && !Array.isArray(displayMsg)) {\n const localized = displayMsg as Record<string, unknown>\n if (typeof localized['zh'] === 'string' && localized['zh'].trim() !== '') {\n return localized['zh'].trim()\n }\n if (typeof localized['en'] === 'string' && localized['en'].trim() !== '') {\n return localized['en'].trim()\n }\n }\n if (typeof obj['msg'] === 'string' && obj['msg'].trim() !== '') {\n return obj['msg'].trim()\n }\n } catch {\n // Non-JSON bodies stay undefined\n }\n return undefined\n}\n\n/** Region for a login domain; an empty domain means CN (matching upstream tooling). */\nexport function regionOf(domain: string): WorkBuddyRegion {\n const lowered = domain.trim().toLowerCase()\n if (lowered === 'workbuddy.ai' || lowered.endsWith('.workbuddy.ai')) return 'global'\n return 'cn'\n}\n\nfunction chatBase(credential: WorkBuddyCredential): string {\n return regionOf(credential.domain) === 'global' ? GLOBAL_BASE : CN_CHAT_BASE\n}\n\nfunction billingBase(credential: WorkBuddyCredential): string {\n return regionOf(credential.domain) === 'global' ? GLOBAL_BASE : CN_BILLING_BASE\n}\n\nfunction originReferer(credential: WorkBuddyCredential): string {\n return regionOf(credential.domain) === 'global' ? GLOBAL_BASE : CN_BILLING_BASE\n}\n\n/** Headers every upstream request shares. */\nfunction commonHeaders(credential: WorkBuddyCredential): Record<string, string> {\n return {\n 'Accept': 'application/json, text/plain, */*',\n 'X-Requested-With': 'XMLHttpRequest',\n 'Origin': originReferer(credential),\n 'Referer': `${originReferer(credential)}/`,\n 'User-Agent': CLIENT_UA,\n }\n}\n\n/**\n * Conversation-id triple the upstream gateway requires on chat requests.\n *\n * The names come from the official desktop bundle, and a fresh UUID per\n * request is what the wire measurement supports: the gateway keeps no history\n * keyed by the id, so the trio marks the channel rather than carrying\n * conversation state. Fresh ids also keep concurrent requests off one shared\n * conversation. The CN gateway was measured accepting the same trio, so both\n * regions carry it and the two paths cannot drift.\n */\nfunction conversationHeaders(): Record<string, string> {\n return {\n 'X-Conversation-ID': randomUUID(),\n 'X-Conversation-Request-ID': randomUUID(),\n 'X-Conversation-Message-ID': randomUUID(),\n }\n}\n\n/**\n * Chat request headers, including the X-No-* conventions the official CLI uses.\n *\n * `userAgent` and `clientVersion` carry the same resolved desktop identity for\n * chat and probe requests. Refresh shares `commonHeaders` but never this\n * override, so the two paths cannot drift into each other. The\n * `X-Conversation-*` trio rides along too \u2014 see {@link conversationHeaders} \u2014\n * because the international gateway rejects a chat that omits it.\n */\nfunction chatHeaders(credential: WorkBuddyCredential, userAgent: string, clientVersion: string): Record<string, string> {\n const headers: Record<string, string> = {\n ...commonHeaders(credential),\n 'User-Agent': userAgent,\n 'Content-Type': 'application/json',\n // \u5B89\u5168\u7EA2\u7EBF\uFF1Achat \u8BF7\u6C42\u7EDD\u4E0D\u643A\u5E26 refresh token\u3002\n ...credential.uid === '' ? { 'X-No-User-Id': '1' } : { 'X-User-Id': credential.uid },\n ...credential.enterpriseId === undefined || credential.enterpriseId === ''\n ? { 'X-No-Enterprise-Id': '1' }\n : { 'X-Enterprise-Id': credential.enterpriseId },\n ...credential.domain === '' ? { 'X-No-Department-Info': '1' } : { 'X-Domain': credential.domain },\n 'X-IDE-Type': 'WorkBuddy',\n 'X-IDE-Name': 'WorkBuddy',\n 'X-IDE-Version': clientVersion,\n 'X-Product': 'SaaS',\n ...conversationHeaders(),\n }\n return headers\n}\n\n/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */\nfunction refreshHeaders(credential: WorkBuddyCredential): Record<string, string> {\n const headers: Record<string, string> = {\n ...commonHeaders(credential),\n 'X-Refresh-Token': credential.refreshToken,\n 'X-Auth-Refresh-Source': 'workbuddy',\n }\n if (credential.enterpriseId !== undefined && credential.enterpriseId !== '') {\n headers['X-Enterprise-Id'] = credential.enterpriseId\n }\n return headers\n}\n\n/** Billing request headers. */\nfunction billingHeaders(credential: WorkBuddyCredential): Record<string, string> {\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${credential.accessToken}`,\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n }\n if (credential.uid !== '') headers['X-User-Id'] = credential.uid\n if (credential.enterpriseId !== undefined && credential.enterpriseId !== '') {\n headers['X-Enterprise-Id'] = credential.enterpriseId\n headers['X-Tenant-Id'] = credential.enterpriseId\n }\n if (credential.domain !== '') headers['X-Domain'] = credential.domain\n return headers\n}\n\n/**\n * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:\n * force `stream: true` (the upstream rejects non-streaming), flatten\n * `tool_choice` (the upstream's field is a string; object forms return 400),\n * and rewrite `developer` messages as `system`.\n *\n * The `developer` rewrite is load-bearing: pi-ai emits the system prompt as\n * `role: \"developer\"` (the OpenAI convention it adopted), but the WorkBuddy\n * upstream rejects that role with HTTP 400 code 11128 (\"Illegal API\n * invocation from an unapproved channel\"). Rewriting to `system` is the\n * compatible spelling the upstream accepts.\n */\nexport function prepareChatBody(source: string): string {\n let body: unknown\n try {\n body = JSON.parse(source)\n } catch {\n return source\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return source\n const obj = body as Record<string, unknown>\n obj['stream'] = true\n normalizeDeveloperRole(obj)\n normalizeToolChoice(obj)\n return JSON.stringify(obj)\n}\n\n/** Rewrite `role: \"developer\"` messages to `role: \"system\"` (upstream rejects developer). */\nfunction normalizeDeveloperRole(obj: Record<string, unknown>): void {\n const messages = obj['messages']\n if (!Array.isArray(messages)) return\n for (const message of messages) {\n if (typeof message !== 'object' || message === null || Array.isArray(message)) continue\n const wrapped = message as Record<string, unknown>\n if (wrapped['role'] === 'developer') wrapped['role'] = 'system'\n }\n}\n\n/** Rewrite OpenAI `tool_choice` spellings into the upstream's string form. */\nfunction normalizeToolChoice(obj: Record<string, unknown>): void {\n const suppress = (): void => {\n delete obj['tools']\n delete obj['functions']\n }\n const present = 'tool_choice' in obj\n if (!present) return\n const choice: unknown = obj['tool_choice']\n if (typeof choice === 'string') {\n if (choice.trim().toLowerCase() === 'none') {\n delete obj['tool_choice']\n suppress()\n }\n return\n }\n if (typeof choice === 'object' && choice !== null && !Array.isArray(choice)) {\n const wrapped = choice as Record<string, unknown>\n const type = typeof wrapped['type'] === 'string' ? wrapped['type'].trim().toLowerCase() : ''\n if (type === 'none') {\n delete obj['tool_choice']\n suppress()\n } else if (type === 'auto' || type === 'required') {\n obj['tool_choice'] = type\n } else if (type === 'function') {\n const fn = typeof wrapped['function'] === 'object' && wrapped['function'] !== null\n ? (wrapped['function'] as Record<string, unknown>)\n : undefined\n let name = typeof fn?.['name'] === 'string' ? fn['name'] : ''\n if (name === '' && typeof wrapped['name'] === 'string') name = wrapped['name']\n name = name.trim()\n obj['tool_choice'] = name !== '' ? name : 'auto'\n } else {\n delete obj['tool_choice']\n }\n return\n }\n delete obj['tool_choice']\n}\n\n/** One JSON-envelope response from the upstream, already unwrapped. */\ninterface Envelope {\n code: number\n msg: string\n data: unknown\n /**\n * The whole parsed response body.\n *\n * Carried alongside `data` because the two catalog shapes differ: the CN\n * endpoint always wraps (`{code,msg,data}`), while the international\n * `/v3/config` has also been observed answering with the product document\n * bare at the top level. `data` alone cannot express \"there was no wrapper,\n * the body itself is the answer\".\n */\n document: Record<string, unknown>\n}\n\nasync function readEnvelope(response: Response): Promise<Envelope> {\n const text = await response.text()\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n throw new Error(`workbuddy upstream returned non-JSON (http ${response.status}): ${text.slice(0, 160)}`)\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error(`workbuddy upstream returned an unexpected document (http ${response.status})`)\n }\n const document = parsed as Record<string, unknown>\n const envelope: Envelope = {\n code: typeof document['code'] === 'number' ? document['code'] : 0,\n msg: typeof document['msg'] === 'string' ? document['msg'] : '',\n data: 'data' in document ? document['data'] : undefined,\n document,\n }\n return envelope\n}\n\n/** Fail an envelope whose business code is non-zero, classified like HTTP errors. */\nfunction envelopeError(status: number, envelope: Envelope): Error {\n const kind = classifyUpstreamError(status, envelope.msg)\n return new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`)\n}\n\n/** Provenance of one successful catalog fetch, surfaced by the status card. */\nexport interface WorkBuddyCatalogFetch {\n fetchedAtMs: number\n /** Which document answered, e.g. `workbuddy-ai:app`. */\n source: string\n /** UA version used, when the request needed one. */\n appVersion?: AppVersionInfo\n}\n\n/** Constructor dependencies. */\nexport interface WorkBuddyUpstreamClientOptions {\n /** App-version resolver for international catalog requests; injectable for tests. */\n resolveAppVersion?: () => Promise<AppVersionInfo>\n /**\n * Chat-identity resolver for chat and probe requests; injectable for tests.\n * Defaults to `client-identity.ts`'s per-region chain. Refresh, catalog, and\n * billing never consult it \u2014 those requests keep their long-standing headers.\n */\n resolveChatIdentity?: (region: WorkBuddyRegion) => Promise<ChatIdentity>\n}\n\n/**\n * Parse the billing response's `CycleEndTime` into epoch milliseconds.\n *\n * The upstream sends a zone-less timestamp (`YYYY-MM-DD HH:mm:ss`). This reads it\n * as the running machine's local time, which is what the official desktop client\n * does \u2014 it feeds the same field straight to `new Date(value).getTime()`. That\n * makes the plugin agree with the app on the same machine; it is **not** evidence\n * of which zone the server or the account uses, which remains unestablished.\n *\n * A naive read does mean a machine in a different zone than the account's gets a\n * shifted instant. That is accepted here rather than guessed at: inventing an\n * offset would be asserting a zone nobody has verified.\n *\n * @returns Epoch milliseconds, or undefined when absent, malformed, or not a real\n * calendar moment (see the round-trip check below).\n */\nfunction parseCycleEndTime(value: unknown): number | undefined {\n if (typeof value !== 'string' || value.trim() === '') return undefined\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2}):(\\d{2})$/u.exec(value.trim())\n if (match === null) return undefined\n const [, y, mo, d, h, mi, s] = match\n const parts = [y, mo, d, h, mi, s].map(Number)\n const [year, month, day, hour, minute, second] = parts as\n [number, number, number, number, number, number]\n const date = new Date(year, month - 1, day, hour, minute, second)\n const parsed = date.getTime()\n if (!Number.isFinite(parsed)) return undefined\n // `Date` rolls overflowing fields forward instead of rejecting them: Feb 30\n // becomes Mar 2, month 13 becomes January of the next year, hour 25 becomes\n // 01:00 the next day. The shape regex above cannot catch that, so compare every\n // field back to what was asked for. A mismatch means the stamp is not a real\n // moment, and must read as \"unknown\" rather than as a confident nearby date.\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day\n || date.getHours() !== hour || date.getMinutes() !== minute || date.getSeconds() !== second) {\n return undefined\n }\n return parsed\n}\n\n/** Empty bucket, used whenever no account qualifies. */\nconst NO_EXPIRING_CREDIT: WorkBuddyExpiringCredits = { total: 0, packages: 0 }\n\n/**\n * Sum the credit lapsing within `horizonMs` of `nowMs`.\n *\n * Only positive balances count: a package with nothing left is not a loss, and\n * including it would make every long-lived account look permanently at risk.\n * Only *future* expiries count too \u2014 a package already past its end time would\n * otherwise satisfy every forward horizon at once (`end - now` is negative,\n * hence \"within 24 hours\"), making a long-dead package look permanently\n * imminent. See {@link WorkBuddyCreditExpiry} for why the past is not reported.\n */\nfunction expiringWithin(\n accounts: readonly WorkBuddyCreditAccount[],\n nowMs: number,\n horizonMs: number,\n): WorkBuddyExpiringCredits {\n return sumExpiring(accounts, (end) => end > nowMs && end - nowMs <= horizonMs)\n}\n\n/** Shared accumulator over accounts whose end time passes `matches`. */\nfunction sumExpiring(\n accounts: readonly WorkBuddyCreditAccount[],\n matches: (endTimeMs: number) => boolean,\n): WorkBuddyExpiringCredits {\n let total = 0\n let packages = 0\n let earliestEndTimeMs: number | undefined\n for (const account of accounts) {\n const end = account.endTimeMs\n if (end === undefined || account.remain <= 0) continue\n if (!matches(end)) continue\n total += account.remain\n packages += 1\n if (earliestEndTimeMs === undefined || end < earliestEndTimeMs) earliestEndTimeMs = end\n }\n if (packages === 0) return NO_EXPIRING_CREDIT\n return {\n total,\n packages,\n ...earliestEndTimeMs === undefined ? {} : { earliestEndTimeMs },\n }\n}\n\n/**\n * Bucket an account's credit by how soon it lapses.\n *\n * Returns undefined when no account carried a usable end time, so a caller can\n * tell \"nothing is expiring\" apart from \"we never learned when anything\n * expires\" \u2014 the second must not render as reassurance.\n */\nfunction summariseExpiry(\n accounts: readonly WorkBuddyCreditAccount[],\n nowMs: number,\n): { expiry?: WorkBuddyCreditExpiry } {\n if (!accounts.some((account) => account.endTimeMs !== undefined)) return {}\n const HOUR = 3600 * 1000\n const DAY = 24 * HOUR\n return {\n expiry: {\n within24h: expiringWithin(accounts, nowMs, DAY),\n within3d: expiringWithin(accounts, nowMs, 3 * DAY),\n within7d: expiringWithin(accounts, nowMs, 7 * DAY),\n },\n }\n}\n\n/**\n * Upstream HTTP client. One instance serves the whole plugin; requests take\n * the credential explicitly so token refreshes apply on the next call.\n *\n * One instance is *per variant*: the international provider needs its own\n * catalog source, UA version, and probe differences, and keeping them on the\n * instance avoids passing a variant through every call signature.\n */\nexport class WorkBuddyUpstreamClient {\n /**\n * Resolves the App-shaped UA version for international catalog requests.\n * Injectable so tests never read the real filesystem.\n */\n private readonly resolveAppVersion: () => Promise<AppVersionInfo>\n /** Chat-identity resolver; see {@link WorkBuddyUpstreamClientOptions.resolveChatIdentity}. */\n private readonly resolveChatIdentity: (region: WorkBuddyRegion) => Promise<ChatIdentity>\n\n /** Provenance of the most recent successful catalog fetch, for the card. */\n lastCatalog: WorkBuddyCatalogFetch | undefined\n\n constructor(options: WorkBuddyUpstreamClientOptions = {}) {\n this.resolveAppVersion = options.resolveAppVersion ?? (() => resolveAppVersion())\n this.resolveChatIdentity = options.resolveChatIdentity ?? (region => resolveChatIdentity(region))\n }\n\n /** POST the chat endpoint; a successful answer is the raw SSE response. */\n async chatStream(\n credential: WorkBuddyCredential,\n bodyJson: string,\n signal?: AbortSignal,\n ): Promise<WorkBuddyChatResult> {\n const region = regionOf(credential.domain)\n // Identity resolution must never block a message: any failure \u2014 a thrown\n // resolver included \u2014 degrades to the desktop fallback form (built-in\n // version, no CLI segment), never to the legacy CLI UA.\n let identity: ChatIdentity\n let userAgent: string\n try {\n identity = await this.resolveChatIdentity(region)\n userAgent = chatUserAgent(identity, region)\n } catch {\n identity = fallbackChatIdentity(region)\n userAgent = chatUserAgent(identity, region)\n }\n let response: Response\n try {\n response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {\n method: 'POST',\n headers: { ...chatHeaders(credential, userAgent, identity.clientVersion), 'Authorization': `Bearer ${credential.accessToken}` },\n body: region === 'global' ? prepareInternationalChatBody(bodyJson) : prepareCnChatBody(bodyJson),\n ...signal === undefined ? {} : { signal },\n })\n } catch (error: unknown) {\n return { ok: false, status: 0, kind: 'server', message: `transport error: ${String(error)}` }\n }\n if (response.ok) return { ok: true, response }\n const text = (await response.text()).slice(0, ERROR_BODY_LIMIT)\n return {\n ok: false,\n status: response.status,\n kind: classifyUpstreamError(response.status, text),\n message: text,\n }\n }\n\n /** POST the token-refresh endpoint; the caller merges the outcome. */\n async refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome> {\n const response = await fetch(`${chatBase(credential)}/v2/plugin/auth/token/refresh`, {\n method: 'POST',\n headers: refreshHeaders(credential),\n signal: AbortSignal.timeout(JSON_TIMEOUT_MS),\n })\n const envelope = await readEnvelope(response)\n if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope)\n const data = typeof envelope.data === 'object' && envelope.data !== null\n ? envelope.data as Record<string, unknown>\n : {}\n const accessToken = typeof data['accessToken'] === 'string' ? data['accessToken'] : ''\n if (accessToken === '') throw new Error('workbuddy token refresh returned no accessToken; sign in again in the WorkBuddy app')\n const outcome: WorkBuddyRefreshOutcome = { accessToken }\n if (typeof data['refreshToken'] === 'string' && data['refreshToken'] !== '') outcome.refreshToken = data['refreshToken']\n if (typeof data['expiresIn'] === 'number' && data['expiresIn'] > 0) outcome.expiresInSec = data['expiresIn']\n if (typeof data['domain'] === 'string' && data['domain'] !== '') outcome.domain = data['domain']\n return outcome\n }\n\n /**\n * GET the personal model catalog.\n *\n * Both variants read `/v3/config`, the product document the desktop product\n * itself fetches. CN used to read `/console/enterprises/personal/models`\n * (the console catalog) instead, and that was why its model list drifted\n * from the desktop App's selector: the console document lags the product\n * one, and the product roster itself churns day to day (`auto`,\n * `kimi-k3-1`, `minimax-m3` have each appeared and disappeared within a\n * week).\n *\n * What distinguishes the two variants here is the User-Agent, not the path:\n * the gateway splits `/v3/config` by client identity, and the split is\n * load-bearing. A CLI-shaped UA yields the CLI's roster \u2014 the chat models\n * this plugin serves \u2014 while an App-shaped UA yields the App's internal\n * roster. CN keeps the CLI UA it sends for chat, so the catalog it\n * advertises is exactly the one its own requests can use. The international\n * variant has no CLI identity, so it keeps the App-shaped UA.\n *\n * Membership is the `cli` roster intersected with the usable rows (see\n * {@link parseModelCatalog}); the promo badges the product document does\n * not carry are merged in from a best-effort console read \u2014 see\n * {@link fetchPromoBadges}.\n *\n * Responses are unwrapped and classified the same way \u2014 `readEnvelope` plus\n * `envelopeError` \u2014 so an expired session or exhausted credit is reported as\n * such rather than as a generic catalog failure.\n */\n async fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]> {\n const international = regionOf(credential.domain) === 'global'\n // `this.resolveAppVersion`, not the module-level function: the constructor\n // injects a resolver so tests never read the real filesystem, and calling\n // the module function directly made that seam inert.\n const appVersion = international ? await this.resolveAppVersion() : undefined\n const response = await fetch(`${chatBase(credential)}/v3/config`, {\n headers: {\n Authorization: `Bearer ${credential.accessToken}`,\n Accept: 'application/json',\n Origin: originReferer(credential),\n Referer: `${originReferer(credential)}/`,\n ...international ? { 'X-Requested-With': 'XMLHttpRequest', 'X-Product': 'SaaS' } : {},\n 'User-Agent': appVersion === undefined ? CLIENT_UA : appUserAgent(appVersion.version),\n },\n signal: signal === undefined\n ? AbortSignal.timeout(JSON_TIMEOUT_MS)\n : AbortSignal.any([signal, AbortSignal.timeout(JSON_TIMEOUT_MS)]),\n })\n const envelope = await readEnvelope(response)\n if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope)\n // Two catalog shapes share this path. The endpoints wrap their answer in\n // `{code,msg,data}`, but `/v3/config` has also been observed answering with\n // the product document bare at the top level (no wrapper at all). Treating\n // a missing `data` as an empty document turned that second shape into a\n // spurious \"no cli agent models\", so a body that itself looks like a\n // catalog (it carries models or agents) is used as the answer. A body with\n // neither shape still falls through to the empty-parse error below.\n const data = isObject(envelope.data) ? envelope.data\n : 'models' in envelope.document || 'agents' in envelope.document ? envelope.document\n : {}\n // Promo badges exist only in the console document, so CN merges them in.\n // The read is best-effort and separate: its failure costs the badges and\n // never the catalog.\n const promoBadges = international ? undefined : await this.fetchPromoBadges(credential, signal)\n const models = parseModelCatalog(data, international, promoBadges)\n this.lastCatalog = {\n fetchedAtMs: Date.now(),\n source: international ? 'workbuddy-ai:app' : 'workbuddy:cli',\n ...appVersion === undefined ? {} : { appVersion },\n }\n return models\n }\n\n /**\n * Read the console catalog's promotional tags, by model id.\n *\n * `/v3/config` carries no `badge:<label>:<color>` tags \u2014 the discount labels\n * the cards render (`\u9650\u65F6\u514D\u8D39`, `\u591C\u95F4\u6298\u6263`, \u2026) live only in\n * `/console/enterprises/personal/models`. Since the roster now comes from\n * the product document, those tags are read from the console one in a\n * second request and merged by id.\n *\n * Best-effort by construction: a badge is a label on a price, so failing to\n * read this document must not fail a catalog refresh. Every failure \u2014\n * network, envelope, an unreadable body \u2014 returns undefined, and the models\n * simply ship without badges.\n */\n private async fetchPromoBadges(\n credential: WorkBuddyCredential,\n signal?: AbortSignal,\n ): Promise<ReadonlyMap<string, readonly string[]> | undefined> {\n try {\n const response = await fetch(`${chatBase(credential)}/console/enterprises/personal/models`, {\n headers: {\n Authorization: `Bearer ${credential.accessToken}`,\n Accept: 'application/json',\n Origin: originReferer(credential),\n Referer: `${originReferer(credential)}/`,\n 'User-Agent': CLIENT_UA,\n },\n signal: signal === undefined\n ? AbortSignal.timeout(JSON_TIMEOUT_MS)\n : AbortSignal.any([signal, AbortSignal.timeout(JSON_TIMEOUT_MS)]),\n })\n if (!response.ok) return undefined\n const envelope = await readEnvelope(response)\n const data = isObject(envelope.data) ? envelope.data : envelope.document\n const rawModels = Array.isArray(data['models']) ? data['models'] : []\n const badges = new Map<string, readonly string[]>()\n for (const model of rawModels) {\n if (!isObject(model)) continue\n const id = typeof model['id'] === 'string' ? model['id'] : ''\n if (id === '') continue\n const tags = Array.isArray(model['tags'])\n ? model['tags'].filter((tag): tag is string =>\n typeof tag === 'string' && tag.toLowerCase().startsWith(BADGE_PREFIX))\n : []\n if (tags.length > 0) badges.set(id, tags)\n }\n return badges.size === 0 ? undefined : badges\n } catch {\n return undefined\n }\n }\n\n /**\n * POST the billing endpoint for the aggregated remaining credit.\n *\n * Two upstream shapes, chosen by account type:\n *\n * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks\n * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single\n * cycle quota. The personal endpoint serves these accounts an empty\n * `Accounts` list, which the card then renders as \"0 credit\" \u2014 a wrong\n * number rather than a visible failure (issue #31).\n * - **Everyone else** keeps the personal endpoint unchanged.\n *\n * The region gate is load-bearing: the enterprise endpoint is unverified for\n * the global region, so an international credential that happens to carry an\n * `enterpriseId` must stay on the measured personal path instead of being\n * moved onto an unmeasured one.\n */\n async fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits> {\n if (regionOf(credential.domain) === 'cn'\n && credential.enterpriseId !== undefined && credential.enterpriseId !== '') {\n return await this.fetchEnterpriseCredits(credential)\n }\n const now = new Date()\n const format = (date: Date): string => [\n date.getFullYear().toString().padStart(4, '0'),\n (date.getMonth() + 1).toString().padStart(2, '0'),\n date.getDate().toString().padStart(2, '0'),\n ].join('-') + ' ' + [\n date.getHours().toString().padStart(2, '0'),\n date.getMinutes().toString().padStart(2, '0'),\n date.getSeconds().toString().padStart(2, '0'),\n ].join(':')\n const response = await fetch(`${billingBase(credential)}/v2/billing/meter/get-user-resource`, {\n method: 'POST',\n headers: billingHeaders(credential),\n body: JSON.stringify({\n PageNumber: 1,\n PageSize: 100,\n ProductCode: 'p_tcaca',\n Status: [0, 3],\n PackageEndTimeRangeBegin: format(now),\n PackageEndTimeRangeEnd: format(new Date(now.getTime() + 365 * 101 * 24 * 3600 * 1000)),\n }),\n signal: AbortSignal.timeout(JSON_TIMEOUT_MS),\n })\n const envelope = await readEnvelope(response)\n if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope)\n const responseWrapper = typeof envelope.data === 'object' && envelope.data !== null\n ? envelope.data as Record<string, unknown>\n : {}\n const data = typeof responseWrapper['Response'] === 'object' && responseWrapper['Response'] !== null\n ? responseWrapper['Response'] as Record<string, unknown>\n : {}\n const inner = typeof data['Data'] === 'object' && data['Data'] !== null\n ? data['Data'] as Record<string, unknown>\n : {}\n const rawAccounts = Array.isArray(inner['Accounts']) ? inner['Accounts'] : []\n const accounts: WorkBuddyCreditAccount[] = []\n let total = 0\n for (const raw of rawAccounts) {\n if (typeof raw !== 'object' || raw === null) continue\n const account = raw as Record<string, unknown>\n const numberField = (key: string): number => (typeof account[key] === 'number' ? account[key] as number : 0)\n const size = numberField('CycleCapacitySize')\n const cycleRemain = numberField('CycleCapacityRemain')\n const cycleUsed = numberField('CycleCapacityUsed')\n const capacityRemain = numberField('CapacityRemain')\n let remain: number\n if (size > 0) remain = cycleRemain\n else if (cycleRemain > 0 || cycleUsed > 0) remain = cycleRemain\n else remain = capacityRemain\n if (remain < 0) remain = 0\n total += remain\n const endTimeMs = parseCycleEndTime(account['CycleEndTime'])\n accounts.push({\n packageName: typeof account['PackageName'] === 'string' ? account['PackageName'] : '(unnamed)',\n remain,\n size: size > 0 ? size : numberField('CapacitySize'),\n ...endTimeMs === undefined ? {} : { endTimeMs },\n })\n }\n return { total, accounts, ...summariseExpiry(accounts, now.getTime()) }\n }\n\n /**\n * CN enterprise credit read: a single cycle quota instead of a package list.\n *\n * Verified against the WorkBuddy desktop app (`app.asar`,\n * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the\n * body is an empty object and the account identity travels only in the\n * headers. The two official call sites disagree on the field spelling\n * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.\n *\n * A body carrying no recognisable quota field is a hard error rather than a\n * zero. Rendering `0` for \"we did not understand the answer\" is exactly how\n * issue #31 stayed invisible while users saw a plausible wrong number.\n *\n * The error names fields and types only: it reaches the browser, and the\n * response body may describe the account's usage.\n */\n private async fetchEnterpriseCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits> {\n const response = await fetch(`${CN_BILLING_BASE}/v2/billing/meter/get-enterprise-user-usage`, {\n method: 'POST',\n headers: billingHeaders(credential),\n body: JSON.stringify({}),\n signal: AbortSignal.timeout(JSON_TIMEOUT_MS),\n })\n const envelope = await readEnvelope(response)\n if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope)\n // The official reader accepts the payload at `data.data`, `data`, or the\n // envelope itself; the observed CN answer puts the fields at `data`.\n const sources: Record<string, unknown>[] = []\n for (const candidate of [envelope.data, envelope.document]) {\n if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) continue\n const record = candidate as Record<string, unknown>\n if (typeof record['data'] === 'object' && record['data'] !== null && !Array.isArray(record['data'])) {\n sources.push(record['data'] as Record<string, unknown>)\n }\n sources.push(record)\n }\n const numberAt = (source: Record<string, unknown>, key: string): number | undefined =>\n typeof source[key] === 'number' ? source[key] as number : undefined\n let limit: number | undefined\n let used: number | undefined\n let resetTime: string | undefined\n for (const source of sources) {\n const candidate = numberAt(source, 'limitNum') ?? numberAt(source, 'limit_num')\n if (candidate === undefined) continue\n limit = candidate\n used = numberAt(source, 'credit') ?? numberAt(source, 'used_num')\n if (typeof source['cycleResetTime'] === 'string' && source['cycleResetTime'] !== '') {\n resetTime = source['cycleResetTime'] as string\n }\n break\n }\n if (limit === undefined) {\n throw new Error(`workbuddy enterprise billing response carried no recognised quota field (expected limitNum/limit_num + credit/used_num; received ${describeShape(envelope.document)})`)\n }\n // `-1` is the upstream's \"no cap\" marker, not a balance. Carried as an\n // explicit flag so no renderer can mistake it for a number. The used amount\n // is not part of an uncapped reading.\n if (limit === -1) {\n return {\n total: 0,\n accounts: [{ packageName: enterprisePackageName, remain: 0, size: 0, unlimited: true }],\n unlimited: true,\n ...resetTime === undefined ? {} : { cycleResetTime: resetTime },\n }\n }\n // A limit with no usable amount must fail rather than assume zero used.\n // Defaulting to 0 would render a confident \"full quota remaining\" from a\n // response we could not read \u2014 the same species of wrong-but-plausible\n // number as the bug this branch exists to fix.\n if (used === undefined) {\n throw new Error(`workbuddy enterprise billing response carried a quota limit but no recognised usage field (expected credit/used_num alongside limitNum/limit_num; received ${describeShape(envelope.document)})`)\n }\n let remain = limit - used\n if (remain < 0) remain = 0\n return {\n total: remain,\n accounts: [{ packageName: enterprisePackageName, remain, size: limit }],\n ...resetTime === undefined ? {} : { cycleResetTime: resetTime },\n }\n }\n\n\n /**\n * One probe request: a real streaming chat call carrying the effort under\n * test.\n *\n * Shares `chatHeaders` with the normal chat path on purpose \u2014 the plan\n * forbids probing through anything but the plugin's own credential handling,\n * so a result describes what a real message would experience.\n *\n * The caller aborts as soon as a parseable event arrives; the body is never\n * assembled into an answer. `reasoning_effort` is omitted entirely (rather\n * than sent empty) when `effort` is undefined, so the baseline case is a\n * genuinely bare request.\n *\n * Two international differences, both measured on 2026-09-11:\n *\n * - The gateway requires a leading `system` message (400/11128 otherwise), so\n * one is prepended for the global region only.\n * - `max_tokens: 1` is below some models' floor (the GPT-5.6 family rejects it\n * with 400/11133 `integer_below_min_value`), so the international probe asks\n * for a slightly larger minimum. This is a floor the plugin must clear, not\n * evidence about any model's effort support: a model still refusing that\n * minimum is reported as an incompatible request, never as \"effort\n * unsupported\", and the ceiling is never raised further to force an answer.\n */\n async probeEffort(\n credential: WorkBuddyCredential,\n model: string,\n effort: string | undefined,\n signal: AbortSignal,\n ): Promise<ProbeAttempt> {\n const international = regionOf(credential.domain) === 'global'\n // Same identity rule as the chat path \u2014 chat and its probe sibling must\n // never present two different clients, and a thrown resolver degrades to\n // the desktop fallback form exactly as in `chatStream`.\n let identity: ChatIdentity\n let userAgent: string\n try {\n identity = await this.resolveChatIdentity(international ? 'global' : 'cn')\n userAgent = chatUserAgent(identity, international ? 'global' : 'cn')\n } catch {\n identity = fallbackChatIdentity(international ? 'global' : 'cn')\n userAgent = chatUserAgent(identity, international ? 'global' : 'cn')\n }\n const payload: Record<string, unknown> = {\n model,\n stream: true,\n messages: [\n ...international ? [{ role: 'system', content: INTERNATIONAL_SYSTEM_PROMPT }] : [],\n { role: 'user', content: PROBE_PROMPT },\n ],\n max_tokens: international ? INTERNATIONAL_PROBE_MAX_TOKENS : PROBE_MAX_TOKENS,\n }\n if (effort !== undefined) payload['reasoning_effort'] = effort\n\n let response: Response\n try {\n response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {\n method: 'POST',\n headers: { ...chatHeaders(credential, userAgent, identity.clientVersion), 'Authorization': `Bearer ${credential.accessToken}` },\n body: JSON.stringify(payload),\n signal,\n })\n } catch (error: unknown) {\n return { status: 0, streamed: false, detail: `transport error: ${String(error)}` }\n }\n\n if (!response.ok) {\n const text = (await response.text()).slice(0, ERROR_BODY_LIMIT)\n return { status: response.status, streamed: false, ...errorCodeOf(text) }\n }\n\n // Read until the first parseable event, then hang up: the probe wants the\n // acceptance signal, not a completion.\n const streamed = await readFirstEvent(response)\n return { status: response.status, streamed }\n }\n}\n\n/** Pull `extError.code` out of an upstream error body, if it is shaped that way. */\nfunction errorCodeOf(text: string): { errorCode?: string; detail?: string } {\n try {\n const parsed: unknown = JSON.parse(text)\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n const wrapped = parsed as Record<string, unknown>\n const extError = wrapped['extError']\n if (typeof extError === 'object' && extError !== null && !Array.isArray(extError)) {\n const code = (extError as Record<string, unknown>)['code']\n if (typeof code === 'string') return { errorCode: code, detail: code }\n }\n }\n } catch {\n // Not JSON: fall through to a plain detail line.\n }\n return { detail: text.slice(0, 200) }\n}\n\n/**\n * Consume just enough of a streaming response to know it really streams.\n *\n * Returns true on the first chunk containing a data line. Cancels the body\n * afterwards; a stream that ends or errors before that counts as not streamed,\n * because an empty 200 is not evidence the effort was accepted.\n */\nasync function readFirstEvent(response: Response): Promise<boolean> {\n const body = response.body\n if (body === null) return false\n const reader = body.getReader()\n const decoder = new TextDecoder()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) return false\n const text = decoder.decode(value, { stream: true })\n if (text.includes('data:')) return true\n }\n } catch {\n return false\n } finally {\n await reader.cancel().catch(() => {})\n }\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\nfunction positive(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n/**\n * Parse either response shape after its envelope has been checked.\n *\n * Membership is the `cli` agent's roster, intersected with the rows that are\n * usable: the roster is what the client identity this plugin presents is\n * allowed to chat with, and joining rather than trusting it outright drops\n * both ids the roster has retired and rows the document lists but cannot\n * serve (no row, `disabled: true`, or non-positive caps all drop out here \u2014\n * a published-but-unservable id must never reach the picker).\n *\n * @param data - the unwrapped catalog/product document.\n * @param international - whether it is the international product document, whose\n * rows carry window objects and promotions.\n * @param promoBadges - badge tags by model id, read from the console document,\n * which is the only one that carries them.\n */\nexport function parseModelCatalog(\n data: Record<string, unknown>,\n international = false,\n promoBadges?: ReadonlyMap<string, readonly string[]>,\n): readonly WorkBuddyUpstreamModel[] {\n const rawModels = Array.isArray(data['models']) ? data['models'] : []\n const agents = Array.isArray(data['agents']) ? data['agents'] : []\n let cliIds: readonly string[] | undefined\n for (const agent of agents) {\n if (typeof agent === 'object' && agent !== null) {\n const wrapped = agent as Record<string, unknown>\n if (wrapped['name'] === 'cli' && Array.isArray(wrapped['models'])) {\n cliIds = wrapped['models'].filter((id): id is string => typeof id === 'string')\n break\n }\n }\n }\n if (cliIds === undefined || cliIds.length === 0) {\n throw new Error('workbuddy model catalog lists no cli agent models')\n }\n const byId = new Map<string, WorkBuddyUpstreamModel>()\n for (const model of rawModels) {\n if (typeof model !== 'object' || model === null) continue\n const wrapped = model as Record<string, unknown>\n const id = typeof wrapped['id'] === 'string' ? wrapped['id'] : ''\n if (id === '' || wrapped['disabled'] === true) continue\n const input = typeof wrapped['maxInputTokens'] === 'number' ? wrapped['maxInputTokens'] : 0\n const output = typeof wrapped['maxOutputTokens'] === 'number' ? wrapped['maxOutputTokens'] : 0\n if (input <= 0 || output <= 0) continue\n byId.set(id, {\n id,\n name: typeof wrapped['name'] === 'string' && wrapped['name'] !== '' ? wrapped['name'] : id,\n contextWindow: international && isObject(wrapped['contextWindow']) && positive(wrapped['contextWindow']['defaultLength'])\n ? wrapped['contextWindow']['defaultLength'] : input,\n ...(international ? {\n ...isObject(wrapped['contextWindow']) && positive(wrapped['contextWindow']['defaultLength'])\n ? { defaultContextWindow: wrapped['contextWindow']['defaultLength'] } : {},\n maxInputTokens: input,\n supportedContextWindows: isObject(wrapped['contextWindow']) && Array.isArray(wrapped['contextWindow']['supportedLengths'])\n ? wrapped['contextWindow']['supportedLengths'].filter(positive) : [],\n promotions: parsePromotions(data['modelPromotions'], id),\n } : {}),\n maxTokens: output,\n supportsImages: wrapped['supportsImages'] === true && wrapped['disabledMultimodal'] !== true,\n ...resolveUpstreamReasoning(wrapped),\n ...resolveUpstreamBilling(wrapped, promoBadges?.get(id)),\n })\n }\n const models = cliIds\n .map(id => byId.get(id))\n .filter((model): model is WorkBuddyUpstreamModel => model !== undefined)\n if (models.length === 0) throw new Error('workbuddy model catalog resolved to an empty list')\n return models\n}\n\n/**\n * One verified promotion entry.\n *\n * Only the shape actually observed in the international App document is\n * modelled \u2014 an enabled, time-boxed, `displayMode: \"replace\"` discount. An\n * entry that does not match is dropped rather than guessed at: rendering a\n * discount the plugin does not understand could understate what the user pays.\n */\nexport interface WorkBuddyPromotion {\n /** Window start, epoch ms, parsed from the document's offset timestamp. */\n start: number\n /** Window end, epoch ms. */\n end: number\n /** Badge text as the upstream wrote it, e.g. `Free now`. */\n label: string\n /** Multiplier applied to the model's rate; `0` replaces it outright. */\n factor: number\n /** Higher wins when several promotions cover one model. */\n priority: number\n}\n\n/** Extract the promotions covering `model` from the `modelPromotions` array. */\nfunction parsePromotions(value: unknown, model: string): WorkBuddyPromotion[] {\n if (!Array.isArray(value)) return []\n return value.flatMap(item => {\n if (!isObject(item) || item['enabled'] !== true) return []\n const modelIds = item['modelIds']\n if (!Array.isArray(modelIds) || !modelIds.includes(model)) return []\n const schedule = item['schedule']\n const discount = item['discount']\n const badge = item['badge']\n if (!isObject(schedule) || !isObject(discount) || !isObject(badge)) return []\n // Only a replacement discount has an unambiguous display rule; any other\n // display mode is left to the upstream's own client.\n if (discount['displayMode'] !== 'replace') return []\n const start = typeof schedule['validFrom'] === 'string' ? Date.parse(schedule['validFrom']) : Number.NaN\n const end = typeof schedule['validUntil'] === 'string' ? Date.parse(schedule['validUntil']) : Number.NaN\n const factor = discount['factor']\n if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return []\n if (typeof factor !== 'number' || !Number.isFinite(factor) || factor < 0) return []\n return [{\n start,\n end,\n factor,\n label: typeof badge['label'] === 'string' ? badge['label'] : '',\n priority: typeof item['priority'] === 'number' && Number.isFinite(item['priority']) ? item['priority'] : 0,\n }]\n })\n}\n\n/**\n * Re-evaluate a model's promotion against the current time.\n *\n * Promotions are time-boxed, and the catalog they arrive in is cached for the\n * life of the process. Frozen at parse time, a cached \"Free now\" would keep\n * claiming a discount after `validUntil` had passed, and would keep showing the\n * pre-discount rate as the discounted one. Re-deriving on every read means the\n * badge disappears on its own and the rate reverts, with no refresh needed.\n *\n * Non-destructive: the model's own `credits` and `badges` are the base, and the\n * promotion is layered onto a copy. A model with no live promotion is returned\n * as-is, so the common case allocates nothing.\n */\nexport function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now = Date.now()): WorkBuddyUpstreamModel {\n if (model.promotions === undefined || model.promotions.length === 0) return model\n const promotion = [...model.promotions]\n .sort((a, b) => b.priority - a.priority)\n .find(candidate => now >= candidate.start && now < candidate.end)\n if (promotion === undefined) {\n // This row arrives with promotions attached, but none is in force now. The\n // catalog is cached for the process's life, so the rate baked into the row\n // was read while a promotion *was* active \u2014 the upstream writes the\n // discounted value into `credits` itself (the international document's\n // free models ship `credits: \"x0.00\"`). Keeping that value would advertise\n // a discount that has ended, and `free: true` is the worst case of it: the\n // user would be told a model costs nothing when it does not.\n //\n // The original price is not recoverable from this row, so the honest answer\n // is to stop asserting one: the rate is dropped and any promo badge\n // removed. `rateUnknown` marks it so the card can say the price needs a\n // refresh rather than implying the model is free.\n const derivedFromPromotion = model.billing?.free === true\n || (model.billing?.badges?.length ?? 0) > 0\n || model.promotions.some(candidate => candidate.factor !== 1)\n if (!derivedFromPromotion) return model\n return {\n ...model,\n billing: {\n free: false,\n rateUnknown: true,\n },\n }\n }\n const rate = normalizeCredits(model.billing?.credits)\n const original = rate !== undefined && rate.startsWith('x') ? Number(rate.slice(1)) : Number.NaN\n // A replacement to zero is meaningful even when the base rate is unknown (the\n // App document's Auto row carries an empty rate string); any other multiplier\n // needs a number to scale, so it is skipped rather than invented.\n if (promotion.factor !== 0 && !Number.isFinite(original)) return model\n const value = promotion.factor === 0 ? 0 : original * promotion.factor\n return {\n ...model,\n billing: {\n ...model.billing,\n credits: `x${value.toFixed(2)}`,\n free: value === 0,\n badges: [\n ...(model.billing?.badges ?? []),\n ...promotion.label === '' ? [] : [promotion.label],\n ],\n },\n }\n}\n/**\n * Apply the international endpoint's extra chat requirement: the first message\n * must be a system prompt.\n *\n * The international gateway rejects a body whose first message is not `system`\n * with HTTP 400 code 11128 (\"first message is not system prompt\"). Note that\n * the *same* code means something else on the CN endpoint \u2014 there it reports a\n * rejected `developer` role \u2014 so the two are never branched on by code alone.\n *\n * The added prompt is deliberately empty of user content and prepended, never\n * merged: existing messages keep their order and wording. A body that is not a\n * JSON object is returned unchanged, exactly as {@link prepareChatBody} does,\n * so this is safe to run over an already-prepared-or-not body.\n */\n/**\n * Prepare a body for the **CN** endpoint.\n *\n * Until #87 the CN wire was the caller's body verbatim. The only difference\n * now is the shared {@link dropUnsupportedEffort} strip, whose region scope was\n * lifted once measurements showed this endpoint rejects `off` too.\n *\n * Deliberately **not** routed through {@link prepareChatBody}: the shim already\n * applies it before handing the body to {@link WorkBuddyUpstreamClient.chatStream},\n * and re-running it would be a second `stream`/`developer`/`tool_choice`\n * normalisation of an already-normalised body. Only the strip belongs here.\n *\n * A body that is not a JSON object is returned unchanged, so this is safe to\n * run over an already-prepared-or-not body.\n */\nexport function prepareCnChatBody(source: string): string {\n let body: unknown\n try {\n body = JSON.parse(source)\n } catch {\n // Not JSON: nothing to strip, and the upstream will reject it anyway.\n return source\n }\n if (!isObject(body)) return source\n dropUnsupportedEffort(body)\n return JSON.stringify(body)\n}\n\nexport function prepareInternationalChatBody(source: string): string {\n const prepared = prepareChatBody(source)\n let body: unknown\n try {\n body = JSON.parse(prepared)\n } catch {\n // Not JSON: nothing to prepend to, and the upstream will reject it anyway.\n return prepared\n }\n if (!isObject(body)) return prepared\n // Shared with the CN path since #87: both endpoints reject the adapter's own\n // `off` spelling, so the strip is no longer region-scoped.\n dropUnsupportedEffort(body)\n const messages = body['messages']\n if (!Array.isArray(messages)) return JSON.stringify(body)\n const first = messages[0]\n if (isObject(first) && first['role'] === 'system') return JSON.stringify(body)\n // Unshift, so every caller-supplied message keeps its position and content.\n messages.unshift({ role: 'system', content: INTERNATIONAL_SYSTEM_PROMPT })\n return JSON.stringify(body)\n}\n\n/**\n * Remove the adapter's own `off` effort spelling from the wire, on every region.\n *\n * `thinkingLevelMap.off` is pinned to the literal `'off'` for models that\n * declare `canDisableThinking`, so that the level stays selectable. pi-ai\n * sends that value for any request carrying no explicit level \u2014 which is why\n * this covers **Default and Off alike**: the two produce the same body, so a\n * fix that only removed the menu entry would leave Default failing.\n *\n * Both endpoints reject it, which is why this is no longer region-scoped\n * (#87): the international GPT family answers HTTP 400 `11133` /\n * `extError.param === 'reasoning.effort'` (issue #49), and the CN endpoint\n * rejects it on models that declare `onlyReasoning` \u2014 measured on\n * `deepseek-v4.1-flash` with 400 / `11150`, while `low`/`high`/`max` pass\n * (issue #87). Omission is the only form measured good on every such model; a\n * literal `'none'` is *not* a safe substitute \u2014 accepted by the GPT-5.6 family\n * and GLM, rejected by `gpt-6-astra`.\n *\n * Consequences, stated honestly: the picker still offers Off for models where\n * the upstream declares `canDisableThinking`, but selecting it now means \"the\n * field is omitted\" \u2014 the model's actual behaviour is decided upstream and is\n * *not* guaranteed to disable thinking or to match the catalog's\n * `defaultEffort`. Declared spellings (`low`/`medium`/`high`/`xhigh`/`max`)\n * and an explicit `none` pass through untouched.\n *\n * The CN strip replaces a scope decision whose premise did not hold: the\n * earlier note here said that endpoint \"has accepted this spelling in every\n * measurement so far\". Measurements since (#87) show it rejects `off` on\n * validating models \u2014 the earlier runs had simply not hit one.\n */\nfunction dropUnsupportedEffort(obj: Record<string, unknown>): void {\n if (obj['reasoning_effort'] === 'off') delete obj['reasoning_effort']\n}\n\n/**\n * The system prompt injected when the international endpoint receives a body\n * with none.\n *\n * Minimal on purpose: it exists to satisfy a gateway precondition, not to\n * steer the model. The plugin is not the place to invent a persona, and the\n * normal path never reaches this \u2014 pi-ai already sends the harness's system\n * prompt, so this only covers a caller that omitted one.\n */\nconst INTERNATIONAL_SYSTEM_PROMPT = 'You are a helpful assistant.'\n\n/**\n * Output ceiling for an international probe request.\n *\n * Above the smallest value that the strictest observed model accepts (the\n * GPT-5.6 family rejects `1` with 11133), while still being far too small to\n * produce a real answer. See {@link WorkBuddyUpstreamClient.probeEffort}.\n */\nconst INTERNATIONAL_PROBE_MAX_TOKENS = 16\n", "/**\n * The international desktop app's version, used as the `/v3/config` UA.\n *\n * The App-shaped catalog is served only to a User-Agent carrying the product\n * name (see `docs/workbuddy-ai-international-research-2026-09-11.md` \u00A72.7).\n * That document's conclusion recommended the space form `WorkBuddy AI/<v>`;\n * re-measured on 2026-09-11 the *space* form is rejected (HTTP 400, code\n * 12403) while the terse `WorkBuddyAI/<v>` form \u2014 with or without the space\n * removed \u2014 returns the 21-model App document. The UA is therefore built from\n * the form verified in code, not from the earlier prose.\n *\n * The version feeds the international catalog UA and the chat identity's\n * `X-IDE-Version`: a missing App, an unreadable plist, or a bad cached value\n * degrades to the last saved value and finally to a compiled-in constant, and\n * never blocks credential use or the provider.\n *\n * @module dsh-workbuddy-connect/app-version\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'\nimport { resolveDshHome } from '@deepseek-ai/dsh-home-paths'\n\n/**\n * Last-resort UA version.\n *\n * The gateway ignores the version number when splitting the UA (research \u00A72.7.2\n * item 2: `CLI/1.0.0`, `CLI/99.0.0` and the real version all return the same\n * document), so this constant is a shape requirement rather than a currency\n * claim. It is *not* used to infer anything about model capabilities.\n */\nexport const FALLBACK_APP_VERSION = '5.5.2'\n\n/** Basename of the saved version under `$DSH_HOME`. */\nexport const WORKBUDDY_APP_VERSION_FILENAME = '.workbuddy-ai-version.json'\n\n/** Where the version came from, for `doctor` output. */\nexport type WorkBuddyAppVersionSource = 'installed' | 'saved' | 'fallback'\n\n/** Resolved version plus provenance. */\nexport interface AppVersionInfo {\n version: string\n source: WorkBuddyAppVersionSource\n /** Basename of the App bundle the version was read from, when installed. */\n bundle?: string\n}\n\n/**\n * Whether a string is safe to interpolate into an HTTP header.\n *\n * Strict on purpose: the value reaches a header, so anything that could split\n * the request (CR, LF, spaces beyond the separator) or inject a second UA\n * token must never pass. The App's own version is always `N.N.N` or `N.N.N.N`.\n */\nexport function validAppVersion(value: unknown): value is string {\n return typeof value === 'string' && /^\\d{1,6}(?:\\.\\d{1,6}){1,3}$/u.test(value)\n}\n\n/** macOS App-bundle roots: system-wide first, then the user's own install. */\nfunction macAppRoots(): string[] {\n return ['/Applications', join(homedir(), 'Applications')]\n}\n\n/**\n * Read `CFBundleShortVersionString` out of an `Info.plist`.\n *\n * Parsed as XML rather than grepped, because the plist contains several\n * `<string>` values and a regex would be one unrelated key away from\n * returning the wrong one. A binary plist has no `<dict>` in its bytes and is\n * reported as unreadable (the saved value then applies) rather than guessed at.\n */\nexport async function readBundleVersion(plistPath: string): Promise<string | undefined> {\n let text: string\n try {\n text = await readFile(plistPath, 'utf8')\n } catch {\n return undefined\n }\n // `<key>CFBundleShortVersionString</key>` followed by the next `<string>`.\n const match = /<key>\\s*CFBundleShortVersionString\\s*<\\/key>\\s*<string>([^<]*)<\\/string>/u.exec(text)\n const version = match?.[1]?.trim()\n return validAppVersion(version) ? version : undefined\n}\n\n/**\n * The installed international App's version, or `undefined` when it is not\n * installed (or not readable).\n *\n * Windows and Linux have no verified bundle-metadata location yet, so this\n * returns `undefined` there and the saved/fallback value is used instead of\n * guessing a path \u2014 the same discipline the credential discovery follows.\n */\nexport async function installedAppVersion(): Promise<{ version: string; bundle: string } | undefined> {\n if (process.platform !== 'darwin') return undefined\n for (const root of macAppRoots()) {\n const bundle = join(root, 'WorkBuddy AI.app')\n const version = await readBundleVersion(join(bundle, 'Contents', 'Info.plist'))\n if (version !== undefined) return { version, bundle }\n }\n return undefined\n}\n\n/** Saved-version file path under the Harness home. */\nexport function appVersionPath(): string {\n return join(resolveDshHome(), WORKBUDDY_APP_VERSION_FILENAME)\n}\n\n/** Constructor dependencies; all injectable so tests never touch the real FS. */\nexport interface ResolveAppVersionOptions {\n /** Installed-version reader; defaults to {@link installedAppVersion}. */\n installed?: () => Promise<{ version: string; bundle: string } | undefined>\n /** Saved-version path; defaults to {@link appVersionPath}. */\n path?: string\n}\n\n/**\n * Resolve the UA version: installed App first, then the last saved value, then\n * the compiled-in fallback.\n *\n * A value read from the App is written back immediately, so an uninstalled App\n * or an unreadable plist later still has the last real version to fall back\n * on. The write is best-effort: failing to cache a version must never fail the\n * catalog request that asked for it.\n */\nexport async function resolveAppVersion(options: ResolveAppVersionOptions = {}): Promise<AppVersionInfo> {\n const path = options.path ?? appVersionPath()\n const installed = await (options.installed ?? installedAppVersion)()\n if (installed !== undefined && validAppVersion(installed.version)) {\n try {\n await writeFileAtomic(\n path,\n `${JSON.stringify({ version: installed.version, bundle: installed.bundle, observedAt: Date.now() }, null, 2)}\\n`,\n { mode: 0o600, dirMode: 0o700 },\n )\n } catch {\n // A read-only home is not a reason to serve no catalog.\n }\n return { version: installed.version, source: 'installed', bundle: installed.bundle }\n }\n try {\n const saved: unknown = JSON.parse(await readFile(path, 'utf8'))\n if (typeof saved === 'object' && saved !== null) {\n const version = (saved as Record<string, unknown>)['version']\n if (validAppVersion(version)) return { version, source: 'saved' }\n }\n } catch {\n // Absent or malformed cache: fall through to the compiled-in default.\n }\n return { version: FALLBACK_APP_VERSION, source: 'fallback' }\n}\n\n/**\n * Build the App-shaped User-Agent for catalog requests.\n *\n * `WorkBuddyAI/<version>` with no space is the form measured to reach the App\n * document; the space form is rejected with 400/12403. Throws on an invalid\n * version rather than sending a malformed header.\n */\nexport function appUserAgent(version: string): string {\n if (!validAppVersion(version)) throw new Error(`invalid WorkBuddy AI version for User-Agent: ${JSON.stringify(version)}`)\n return `WorkBuddyAI/${version}`\n}\n", "/**\n * The desktop-client identity chat requests present as (phase 1 of\n * `docs/upstream-identity-alignment-plan.md`).\n *\n * Chat and its probe sibling carry the User-Agent shape the official desktop\n * client composes \u2014 `WorkBuddy/<v> <product>/<v> CLI/<cli>` \u2014 where the\n * product token names the app that owns the region's requests: `WorkBuddy`\n * for CN, `WorkBuddy AI` for international. Versions come from the installed\n * App when it can be read, degrade to a per-region saved value, and finally\n * to a compiled-in constant. A CLI version that does not resolve drops the\n * `CLI/\u2026` token instead of inventing one (the official client's own rule for\n * a missing extension).\n *\n * Scope: chat and probe requests ONLY. Refresh, catalog, and billing keep\n * the headers they have always sent; the plan holds the blast radius to this\n * one variable so the live verification matrix stays readable.\n *\n * @module dsh-workbuddy-connect/client-identity\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'\nimport { resolveDshHome } from '@deepseek-ai/dsh-home-paths'\nimport {\n FALLBACK_APP_VERSION,\n readBundleVersion,\n resolveAppVersion,\n validAppVersion,\n type AppVersionInfo,\n} from './app-version.ts'\nimport type { WorkBuddyRegion } from './upstream.ts'\n\n/**\n * Compiled-in CN fallback for the `WorkBuddy/<v>` tokens.\n *\n * Observed on the CN desktop app installed here (research \u00A73.1, verified\n * 2026-09-11); like the international fallback it is a shape requirement,\n * not a currency claim \u2014 the gateway has not been observed to branch on it.\n */\nexport const FALLBACK_CN_APP_VERSION = '5.5.6'\n\n/**\n * Basename of the CN saved-version cache under `$DSH_HOME`.\n *\n * Deliberately not the international `.workbuddy-ai-version.json`: that file\n * feeds the international catalog's User-Agent, and a CN App writing its\n * version into it would relabel that request. The two caches stay isolated\n * the way the per-variant catalog files are.\n */\nexport const CN_APP_VERSION_FILENAME = '.workbuddy-app-version.json'\n\n/** The resolved identity a chat request presents as. */\nexport interface ChatIdentity {\n /** Desktop App version; drives the desktop UA and `X-IDE-Version`. */\n clientVersion: string\n /** Bundled agent-CLI version; absent drops the `CLI/\u2026` UA token. */\n cliVersion?: string\n}\n\n/**\n * Whether a value is a CLI version that may reach a header.\n *\n * Tolerates a prerelease suffix (`2.137.1-rc.1`) because the bundled CLI's\n * own metadata uses that spelling; anything with whitespace, CR or LF never\n * passes \u2014 the value is interpolated into an HTTP header.\n */\nexport function validCliVersion(value: unknown): value is string {\n return typeof value === 'string' && /^\\d{1,6}(?:\\.\\d{1,6}){1,3}(?:-[0-9A-Za-z.]+)?$/u.test(value)\n}\n\n/** macOS App-bundle roots, searched in the order `app-version.ts` uses. */\nfunction macAppRoots(): string[] {\n return ['/Applications', join(homedir(), 'Applications')]\n}\n\n/** Path of the bundled agent CLI's package.json inside an App bundle. */\nfunction cliPackagePath(bundle: string): string {\n return join(bundle, 'Contents', 'Resources', 'app.asar.unpacked', 'cli', 'package.json')\n}\n\n/**\n * The bundled agent CLI's real version, or `undefined` when it does not resolve.\n *\n * `cli/package.json` ships a `0.0.0` placeholder in `version` with the real\n * version in `publishConfig.customPackage.version`; a valid non-placeholder\n * `version` wins, otherwise the custom-package value applies, and unreadable\n * or invalid metadata yields `undefined` (the caller drops the `CLI/\u2026` UA\n * token rather than guessing).\n */\nexport async function readCliVersion(bundle: string): Promise<string | undefined> {\n let document: unknown\n try {\n document = JSON.parse(await readFile(cliPackagePath(bundle), 'utf8'))\n } catch {\n return undefined\n }\n if (typeof document !== 'object' || document === null || Array.isArray(document)) return undefined\n const pkg = document as Record<string, unknown>\n const declared = pkg['version']\n if (validCliVersion(declared) && declared !== '0.0.0') return declared\n const publishConfig = pkg['publishConfig']\n const customPackage = typeof publishConfig === 'object' && publishConfig !== null && !Array.isArray(publishConfig)\n ? publishConfig as Record<string, unknown>\n : undefined\n const custom = typeof customPackage?.['customPackage'] === 'object' && customPackage['customPackage'] !== null\n && !Array.isArray(customPackage['customPackage'])\n ? customPackage['customPackage'] as Record<string, unknown>\n : undefined\n const customVersion = custom?.['version']\n return validCliVersion(customVersion) ? customVersion : undefined\n}\n\n/**\n * Build the chat User-Agent for one region.\n *\n * Throws on an invalid version rather than interpolating one into a header;\n * `resolveChatIdentity` never produces such an identity, so the throw is a\n * last gate against future call-site mistakes, not an expected path.\n */\nexport function chatUserAgent(identity: ChatIdentity, region: WorkBuddyRegion): string {\n if (!validAppVersion(identity.clientVersion)) {\n throw new Error(`invalid client version for chat User-Agent: ${JSON.stringify(identity.clientVersion)}`)\n }\n if (identity.cliVersion !== undefined && !validCliVersion(identity.cliVersion)) {\n throw new Error(`invalid CLI version for chat User-Agent: ${JSON.stringify(identity.cliVersion)}`)\n }\n const product = region === 'global' ? 'WorkBuddy AI' : 'WorkBuddy'\n const parts = [`WorkBuddy/${identity.clientVersion}`, `${product}/${identity.clientVersion}`]\n if (identity.cliVersion !== undefined) parts.push(`CLI/${identity.cliVersion}`)\n return parts.join(' ')\n}\n\n/** Constructor dependencies; every reader is injectable so tests never touch a real App or home. */\nexport interface ResolveChatIdentityOptions {\n /** Installed CN desktop-bundle reader; defaults to the macOS probe. */\n installedCn?: () => Promise<{ version: string; bundle: string } | undefined>\n /** International version resolver; defaults to `app-version.ts`'s chain. */\n resolveIntl?: () => Promise<AppVersionInfo>\n /** CLI-version reader; defaults to reading the bundle's `cli/package.json`. */\n cliVersion?: (bundle: string) => Promise<string | undefined>\n /** CN saved-cache path; defaults to `$DSH_HOME/.workbuddy-app-version.json`. */\n cnSavedPath?: string\n}\n\n/** The installed CN desktop bundle, or `undefined` when it is not installed (or not readable). */\nasync function installedCnApp(): Promise<{ version: string; bundle: string } | undefined> {\n if (process.platform !== 'darwin') return undefined\n for (const root of macAppRoots()) {\n const bundle = join(root, 'WorkBuddy.app')\n const version = await readBundleVersion(join(bundle, 'Contents', 'Info.plist'))\n if (version !== undefined) return { version, bundle }\n }\n return undefined\n}\n\n/** Default CN saved-cache path. */\nfunction cnSavedVersionPath(): string {\n return join(resolveDshHome(), CN_APP_VERSION_FILENAME)\n}\n\n/**\n * Resolve the chat identity for one region: installed App \u2192 region's saved\n * value \u2192 compiled-in fallback. Never throws \u2014 a missing App, an unreadable\n * plist, a failed cache write, or a reader that throws outright all degrade\n * to {@link fallbackChatIdentity}; resolution never blocks a message.\n *\n * The production path caches per region (a message must not re-read the\n * install tree); any injected option bypasses the cache entirely so tests\n * with different readers cannot observe each other's resolutions.\n */\nexport async function resolveChatIdentity(\n region: WorkBuddyRegion,\n options: ResolveChatIdentityOptions = {},\n): Promise<ChatIdentity> {\n const injectable = options.installedCn !== undefined\n || options.resolveIntl !== undefined\n || options.cliVersion !== undefined\n || options.cnSavedPath !== undefined\n if (!injectable) {\n const cached = cache.get(region)\n if (cached !== undefined) return cached\n }\n let identity: ChatIdentity\n try {\n identity = region === 'global'\n ? await resolveGlobalIdentity(options)\n : await resolveCnIdentity(options)\n } catch {\n // A reader that throws (unexpected filesystem error, injected test\n // double) must not block a message and must not pin this degraded\n // answer for the process lifetime: return the built-in fallback\n // without caching it, so a later call can resolve properly again.\n return fallbackChatIdentity(region)\n }\n if (!injectable) cache.set(region, identity)\n return identity\n}\n\nconst cache = new Map<WorkBuddyRegion, ChatIdentity>()\n\n/**\n * The region's compiled-in fallback identity: the desktop form with the\n * built-in version and no `CLI/\u2026` segment. This is the single degraded\n * shape every failure path converges on \u2014 a thrown reader, an unreadable\n * bundle, or a missing cache all present this, never the legacy CLI UA.\n */\nexport function fallbackChatIdentity(region: WorkBuddyRegion): ChatIdentity {\n return { clientVersion: region === 'global' ? FALLBACK_APP_VERSION : FALLBACK_CN_APP_VERSION }\n}\n\n/** CN: installed `WorkBuddy.app` \u2192 CN saved cache \u2192 CN fallback. */\nasync function resolveCnIdentity(options: ResolveChatIdentityOptions): Promise<ChatIdentity> {\n const savedPath = options.cnSavedPath ?? cnSavedVersionPath()\n const installed = await (options.installedCn ?? installedCnApp)()\n if (installed !== undefined && validAppVersion(installed.version)) {\n const cliVersion = await (options.cliVersion ?? readCliVersion)(installed.bundle)\n const identity: ChatIdentity = {\n clientVersion: installed.version,\n ...(cliVersion !== undefined && validCliVersion(cliVersion) ? { cliVersion } : {}),\n }\n // Best-effort remember of the App version only: the CLI version is read\n // live from the bundle whenever the bundle exists, and once the App is\n // gone the plan's degradation omits the `CLI/\u2026` token instead of\n // continuing to claim a version whose source no longer exists.\n try {\n await writeFileAtomic(\n savedPath,\n `${JSON.stringify({ version: identity.clientVersion, observedAt: Date.now() }, null, 2)}\\n`,\n { mode: 0o600, dirMode: 0o700 },\n )\n } catch {\n // Identity resolved; losing the memory of it is not a failure.\n }\n return identity\n }\n try {\n const saved: unknown = JSON.parse(await readFile(savedPath, 'utf8'))\n if (typeof saved === 'object' && saved !== null && !Array.isArray(saved)) {\n const document = saved as Record<string, unknown>\n if (validAppVersion(document['version'])) {\n return { clientVersion: document['version'] }\n }\n }\n } catch {\n // Absent or malformed cache: fall through to the compiled-in constant.\n }\n return fallbackChatIdentity('cn')\n}\n\n/**\n * International: reuse `app-version.ts`'s installed \u2192 saved \u2192 fallback chain\n * (its cache format and the catalog's version source stay untouched). The\n * CLI version is read only when that chain reports the installed bundle; a\n * saved or fallback resolution has no bundle path and drops the `CLI/\u2026` token.\n * The CN cache is never read or written on this path.\n */\nasync function resolveGlobalIdentity(options: ResolveChatIdentityOptions): Promise<ChatIdentity> {\n const info = await (options.resolveIntl ?? resolveAppVersion)()\n const clientVersion = validAppVersion(info.version) ? info.version : FALLBACK_APP_VERSION\n let cliVersion: string | undefined\n if (info.bundle !== undefined) {\n const read = await (options.cliVersion ?? readCliVersion)(info.bundle)\n if (read !== undefined && validCliVersion(read)) cliVersion = read\n }\n return { clientVersion, ...cliVersion === undefined ? {} : { cliVersion } }\n}\n", "/**\n * The reasoning-effort probe: decide whether a model's `reasoning_effort`\n * parameter is actually validated, and if so which canonical values it accepts.\n *\n * Implements `docs/reasoning-effort-probe-plan.md` \u00A74. The order matters and is\n * not an optimization:\n *\n * 1. **Baseline** (no `reasoning_effort`) proves the model, credential, and\n * request shape work at all, so a later rejection can be attributed.\n * 2. **Sentinel** (a fresh random, impossible-to-collide value) answers the one\n * question a per-level sweep cannot: does the upstream validate the field?\n * A model that accepts the sentinel answers 200 to *everything*, so its\n * per-level results would be uniformly false positives.\n * 3. **Levels**, only after the sentinel was refused.\n *\n * The result is an observation, never a capability claim. Even a fully\n * successful sweep means \"the upstream accepted these spellings\", not \"these\n * spellings change how the model thinks\".\n *\n * @module dsh-workbuddy-connect/probe\n */\n\nimport { randomBytes } from 'node:crypto'\nimport type { WorkBuddyEffort } from './upstream.ts'\n\n/**\n * The canonical values a probe tests, in a fixed order.\n *\n * `minimal` is absent: it appears in no upstream vocabulary. `off` is absent\n * by policy \u2014 disabling thinking is a separate capability the upstream must\n * declare through `canDisableThinking`, never something probing may infer.\n */\nexport const PROBE_EFFORT_CANDIDATES: readonly WorkBuddyEffort[] = ['low', 'medium', 'high', 'xhigh', 'max']\n\n/** Prompt body used by every probe request; carries nothing user-specific. */\nexport const PROBE_PROMPT = 'ping'\n\n/** Output ceiling for a probe request; the answer itself is never read. */\nexport const PROBE_MAX_TOKENS = 1\n\n/** Per-request ceiling so one hung probe cannot stall the queue. */\nexport const PROBE_REQUEST_TIMEOUT_MS = 30_000\n\n/** Sentinel generator; injectable so tests get deterministic values. */\nexport type SentinelFactory = () => string\n\n/** Default sentinel: unmistakably non-canonical, different on every call. */\nexport function randomSentinel(): string {\n return `probe_sentinel_${randomBytes(12).toString('hex')}`\n}\n\n/**\n * One response as the probe sees it, split into the only distinctions the\n * attribution rule needs.\n */\nexport interface ProbeAttempt {\n /** HTTP status, or 0 for a transport failure. */\n status: number\n /** True when a parseable SSE event arrived. */\n streamed: boolean\n /** `extError.code` from a JSON error body, when present. */\n errorCode?: string\n /** Free-form detail for logs; never shown as a capability claim. */\n detail?: string\n}\n\n/** How one attempt is performed; the caller owns credentials and HTTP. */\nexport type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>\n\n/** The outcome of probing one model. */\nexport type ProbeOutcome =\n | { validation: 'validating'; efforts: readonly WorkBuddyEffort[]; requests: number }\n | { validation: 'non-validating'; efforts: readonly []; requests: number }\n | { validation: 'unknown'; efforts: readonly []; requests: number; reason: string }\n\n/** Which endpoint's rejection vocabulary a probe interprets. */\nexport type ProbeRegion = 'cn' | 'global'\n\n/**\n * The upstream's \"this effort value is not supported\" codes, per region, as\n * measured on each live endpoint. The sets are kept separate so a code only\n * ever widens detection for the endpoint it was measured on.\n *\n * - `invalid_reasoning_effort` \u2014 the China endpoint, measured 2026-09-11\n * (plan \u00A74.2). Also kept for `global` as a fallback spelling.\n * - `model_param_invalid` \u2014 the global endpoint, measured 2026-10-01. A\n * non-canonical value is answered `400` / `11133` with this code, which names\n * no field (`extError.param` is empty), instead of one that names the effort.\n * It is generic enough to be readable here only because the baseline step has\n * already proved the *same* request without `reasoning_effort` succeeds,\n * leaving the sentinel as the only difference between the two attempts. A\n * sibling code in the same `11133` envelope that names another parameter\n * (`integer_below_min_value`, the `max_tokens` floor) and a body carrying no\n * `extError` at all (`11102`, unknown model) are therefore not mistaken for\n * it, and neither is a level-sweep answer. It is *not* added to `cn`: that\n * endpoint was measured answering the specific code, and reading a generic\n * code there would widen attribution beyond what was observed.\n *\n * Neither is treated as a permanent protocol promise: anything unrecognized\n * still degrades to `unknown` rather than to a capability conclusion.\n */\nconst INVALID_EFFORT_CODES: Record<ProbeRegion, ReadonlySet<string>> = {\n cn: new Set(['invalid_reasoning_effort']),\n global: new Set(['invalid_reasoning_effort', 'model_param_invalid']),\n}\n\n/** Whether an attempt is an attributable rejection of the effort value. */\nfunction isEffortRejection(attempt: ProbeAttempt, region: ProbeRegion): boolean {\n const codes = INVALID_EFFORT_CODES[region]\n return attempt.status === 400 && attempt.errorCode !== undefined && codes.has(attempt.errorCode)\n}\n\n/** Whether an attempt shows the upstream accepted the request and streamed. */\nfunction isAcceptance(attempt: ProbeAttempt): boolean {\n return attempt.status === 200 && attempt.streamed\n}\n\n/** Why an attempt ended in `unknown`, phrased for a log line. */\nfunction unknownReason(stage: string, attempt: ProbeAttempt): string {\n const code = attempt.errorCode === undefined ? '' : ` (${attempt.errorCode})`\n const detail = attempt.detail === undefined ? '' : `: ${attempt.detail}`\n return `${stage} status ${attempt.status}${code}${detail}`\n}\n\n/**\n * Probe one model.\n *\n * `options.candidates` exists so tests can shorten the sweep; production always\n * uses {@link PROBE_EFFORT_CANDIDATES}. `options.region` selects which\n * endpoint's rejection vocabulary is read; it defaults to `cn`, which is also\n * the production default for the China app.\n */\nexport async function probeModel(options: {\n send: ProbeSender\n sentinel?: SentinelFactory\n candidates?: readonly WorkBuddyEffort[]\n timeoutMs?: number\n region?: ProbeRegion\n}): Promise<ProbeOutcome> {\n const sentinel = options.sentinel ?? randomSentinel\n const candidates = options.candidates ?? PROBE_EFFORT_CANDIDATES\n const timeoutMs = options.timeoutMs ?? PROBE_REQUEST_TIMEOUT_MS\n const region = options.region ?? 'cn'\n let requests = 0\n\n const attempt = async (effort: string | undefined): Promise<ProbeAttempt> => {\n requests += 1\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n return await options.send(effort, controller.signal)\n } catch (error: unknown) {\n return { status: 0, streamed: false, detail: `transport error: ${String(error)}` }\n } finally {\n clearTimeout(timer)\n }\n }\n\n // 1. Baseline: proves the request path itself is sound. Without this a 400\n // on the sentinel could just as well be an unrelated request problem.\n const baseline = await attempt(undefined)\n if (!isAcceptance(baseline)) {\n return { validation: 'unknown', efforts: [], requests, reason: unknownReason('baseline', baseline) }\n }\n\n // 2. Sentinel: the only step that can distinguish \"validates the field\" from\n // \"accepts anything\".\n const probe = sentinel()\n const sentinelAttempt = await attempt(probe)\n if (isAcceptance(sentinelAttempt)) {\n // The upstream took a value that cannot exist. Any per-level acceptance\n // would be equally meaningless, so stop here rather than spend more\n // requests producing false positives.\n return { validation: 'non-validating', efforts: [], requests }\n }\n if (!isEffortRejection(sentinelAttempt, region)) {\n return { validation: 'unknown', efforts: [], requests, reason: unknownReason('sentinel', sentinelAttempt) }\n }\n\n // 3. Levels: meaningful only because the sentinel was refused.\n const accepted: WorkBuddyEffort[] = []\n for (const effort of candidates) {\n const levelAttempt = await attempt(effort)\n if (isAcceptance(levelAttempt)) {\n accepted.push(effort)\n continue\n }\n if (isEffortRejection(levelAttempt, region)) continue\n // A non-decisive answer mid-sweep: the partial list is not a finding, so\n // report the whole run as unknown rather than under-claiming a model.\n return {\n validation: 'unknown',\n efforts: [],\n requests,\n reason: unknownReason(`level ${effort}`, levelAttempt),\n }\n }\n\n return { validation: 'validating', efforts: accepted, requests }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["readFile", "homedir", "join", "writeFileAtomic", "resolveDshHome", "macAppRoots", "join", "homedir", "readFile", "resolveDshHome", "writeFileAtomic"]
|
|
7
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
atRestKeyProviderFor,
|
|
6
6
|
readTraeIdentity,
|
|
7
7
|
traeStorageCandidates
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FLL3DUVP.js";
|
|
9
9
|
import {
|
|
10
10
|
regionOfCredential
|
|
11
11
|
} from "./chunk-S5KMFAQM.js";
|
|
@@ -133,7 +133,7 @@ async function importAccount({ provider, source, credentialFile, identityFile, e
|
|
|
133
133
|
credential = await store.current();
|
|
134
134
|
}
|
|
135
135
|
if (!text(credential?.accessToken) || !text(credential.uid) || !Number.isFinite(credential.expiresAtMs) || credential.expiresAtMs <= 0 || typeof credential.domain !== "string" || /[\r\n]/.test(credential.domain)) throw new Error("No valid WorkBuddy credential in source");
|
|
136
|
-
const { regionOf } = await import("./upstream-
|
|
136
|
+
const { regionOf } = await import("./upstream-VP6J7LU2.js");
|
|
137
137
|
if (regionOf(credential.domain) === "global" !== (provider === "workbuddy-ai")) throw new Error("WorkBuddy credential region does not match provider");
|
|
138
138
|
await writePrivateJson(credentialFile, { version: 1, credential: { ...credential, source: "dsh" } });
|
|
139
139
|
} else if (provider.startsWith("trae")) {
|
|
@@ -178,4 +178,4 @@ export {
|
|
|
178
178
|
readPortableCredential,
|
|
179
179
|
importAccount
|
|
180
180
|
};
|
|
181
|
-
//# sourceMappingURL=chunk-
|
|
181
|
+
//# sourceMappingURL=chunk-PI3XU3SB.js.map
|
|
@@ -6,17 +6,17 @@ import {
|
|
|
6
6
|
refreshWorkBuddyCheckinCredential,
|
|
7
7
|
supportsCheckin,
|
|
8
8
|
workbuddyToken
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6GEU5EGE.js";
|
|
10
10
|
import {
|
|
11
11
|
discoverLocalAccounts,
|
|
12
12
|
importAccount,
|
|
13
13
|
localAccountKey,
|
|
14
14
|
normalizeLocalAccountKey,
|
|
15
15
|
readPortableCredential
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PI3XU3SB.js";
|
|
17
17
|
import {
|
|
18
18
|
modelWithCurrentPromotion
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-KWXLQJS5.js";
|
|
20
20
|
import {
|
|
21
21
|
ApiService
|
|
22
22
|
} from "./chunk-GQEKAQXE.js";
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
45
45
|
|
|
46
46
|
// package.json
|
|
47
|
-
var version = "0.6.
|
|
47
|
+
var version = "0.6.6";
|
|
48
48
|
|
|
49
49
|
// src/manager.mjs
|
|
50
50
|
import { readFile as readFile4, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
|
|
@@ -2048,4 +2048,4 @@ export {
|
|
|
2048
2048
|
Manager,
|
|
2049
2049
|
managementHandler
|
|
2050
2050
|
};
|
|
2051
|
-
//# sourceMappingURL=chunk-
|
|
2051
|
+
//# sourceMappingURL=chunk-VDDKRMZK.js.map
|