dsh-connect-workbuddy 4.4.5 → 4.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +82 -6
- package/lib/{host-heartbeat-Bj7JvSYJ.js → host-heartbeat-B2jrUDhg.js} +20 -2
- package/lib/index.d.ts +119 -3
- package/lib/index.js +333 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.6 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **面板的「测试」只测一个模型,却把结果当成账号级结论:账号池说「可用」,一切到实际在用的模型就撞 `429 / 6004`。**(issue #61 报告者报告,维护者在 issue 下确认「测试的是某一个模型……不能换其他模型」)
|
|
8
|
+
- **现象与证据**:报告者同一时刻、同一脚本测两个国内账号 —— `hy4-preview-f`(当时的 `pool.targetModelId`,面板测的就是它)两个号都可用,而 `step-5-preview`(当时实际在用的模型)两个号都 `429 / 6004`。池文件里这两个号还被更早的一次批量测试记成 `outcome=ok source=test-batch`,而池开着时那份「ok」正是选号依据。**`6004` 是「账号 × 模型」维度的条件**(本项目 10-13 也在三个国际账号上量到 `deepseek-v4.1-flash` 挂、其余模型全通),所以单一模型探测在结构上无法回答「这个账号能不能服务」。
|
|
9
|
+
- **根因(报告者给出代码位置,本地逐条复核属实)**:`resolveTargetModel()` 只解析出**一个** `modelId`(来自区域级 `pool.targetModelId`),一条路径同时服务批量与逐行测试;而 `probeModel()` 的三个返回分支**早就带着 `modelId`**,落盘时被丢掉了 —— 记录里只有 `outcome/atMs/source/retryAtMs/message`,没有任何字段说明这条 `ok` 是**哪个模型**上的。
|
|
10
|
+
- **修法(三处,各自成立)**:
|
|
11
|
+
1. **测量记录记住模型**:`WorkBuddyPoolProbe` 新增可选 `modelId`,两个写入方(批量测试、实时聊天失败)在拿到模型的那一刻写下它。
|
|
12
|
+
2. **每个账号可单独指定测试模型**:新增设置 `pool.targetModelIdByAccount`(账号 id → 模型 id),读取时先查 map、查不到回落 `targetModelId`。**批量测试与逐行测试走同一条解析**(报告者指出这正是「改一处两处同时生效」的地方),所以「测试所有账号」也会按每个号自己的模型去测。解析仍然过 `resolveTargetModel`:指向本区域已下架模型时**报 `stale`、绝不静默换成另一个模型**(换一个等于花积分测一个用户没选的模型,还把它当作这个号的结论)。
|
|
13
|
+
3. **卡片显示适用范围**:池表每行的测量结论旁写上「模型 {model}」(记录里没有该字段的旧数据写「未记录模型」,不猜);每行新增一个测试模型下拉框(留空 = 跟随区域目标),随「保存」一起写入。
|
|
14
|
+
- **为什么 `modelId` 落盘时不继承**:`accountName` 与 `retryAtMs` 描述的是**账号**(后来者看不到的名字仍然是真的,未来的刷新时刻仍然在未来),而模型描述的是**这一次测量**。把旧的 model id 继承到一次新测量上,等于用不是它的模型给它贴标签 —— 正是这个字段要终结的那种混淆。所以**缺失即「范围未知」,不是猜测**。
|
|
15
|
+
- **配置向后兼容**:`targetModelIdByAccount` 默认空对象,且**只有非空时才写入** settings(与 `probeInputTokens` 同一条规则)—— 从未用过这个设置的 profile 写出来的槽位与升级前逐字节相同,升级不会看起来像用户改过设置。
|
|
16
|
+
- **守卫**:`tests/issue61-probe-model-scope.spec.ts`(9 例)覆盖:逐账号优先/回落、空串视为「跟随区域目标」、override 下架时解析为 `stale` 且不带 `modelId`、批量按各账号自己的模型探测、单一模型的旧调用形态不变、解析不出模型时产出 `unavailable` 行并指名那个模型(**没有**回退到别的模型)、批量写入 `modelId`、**不继承**、新测量覆盖旧模型。`tests/pool-route.spec.ts` 新增一条路由级断言:批量路由交给 Host 的是**逐账号解析器**(`a` 用区域目标、`b` 用 `b` 自己的)。**变异验证**:记录不再写 `modelId` → 1 例红;合并改成继承 → 1 例红;批量改回「整批一个模型」→ 1 例红;路由改回传单一 modelId → 路由断言红。
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
- **新增「上游请求体构成」诊断(`WORKBUDDY_BODY_REPORT`),用于定案 issue #59:两份请求体到底是不是同一份历史。**(issue #59)
|
|
21
|
+
- **为什么需要**:报告里同一份历史被国内网关算约 70.8 万 token、国际网关算约 137.4 万(1.94 倍),切区域即 `code 11115 prompt is too long`。两条独立测量已经把范围压到唯一一个问题:报告者在**两个区域**跑本仓库的内容类别探针,逐字节相同的散文/代码/JSON 六格比值全在 **0.98~1.06**(同一段字节两边计价一致);代码链路核对又表明**历史 reasoning 根本没进请求体**(pi-ai 只在 `compat.requiresThinkingAsText` 为真时把 `thinking` 当文本转发,本插件从未设置;`reasoning_details` 那条路需要流式解析从不写出的签名)。**剩下唯一可能是两份请求体不同**,而这件事**不需要任何区域有积分**就能查:请求体是本地拼好的,`14018 Credits exhausted` 是发出去之后才可能发生的。
|
|
22
|
+
- **做法**:`src/body-report.ts` 在 shim 把 body 交给上游**之前**记录它的**构成** —— 每条消息的 `role`、逐块的类型(`system`/`text`/`tool-call`/`tool-result`/`reasoning`/`image`)、字符数与其中**中日韩字符**数(实测中文 0.669、英文 0.224 token/字符,差 3 倍,混在一起看不出来)、`tools` 数组、`messages.length` 与 body 总字节数。按区域写 `<dir>/<region>.jsonl`,一次请求一行(一轮对话有很多次上游调用,**最后一次**才是要比的那份)。
|
|
23
|
+
- **只记计数、不记文本**:不写任何消息内容、账号名或令牌 —— 一个要被贴进公开 issue 的诊断不能有泄漏用户会话的能力。`tests/issue59-body-report.spec.ts` 里有一条专门的断言:用带特征串的 body 生成报告后,序列化结果**不含**该特征串(变异验证:让 `block()` 把 `text` 一起带上 → 该例与 reasoning 那例同时变红)。
|
|
24
|
+
- **开关**:环境变量 `WORKBUDDY_BODY_REPORT` 未设置或为空白即**关闭**;`1`/`true` 写到 `<DSH home>/body-reports/`;其他值按**目录路径**解释。写文件失败只走日志,**绝不影响聊天请求**(有专门断言)。
|
|
25
|
+
- **守卫**:`tests/issue59-body-report.spec.ts`(16 例)。
|
|
26
|
+
- **配套**:`scripts/probe-region-token-accounting.mjs` 新增 `--language en|zh` 与 `--kind prose|code|json`(六种组合确定性填充、跨区域逐字节相同),并补上国内侧实测基线:prose 214.0 / code 287.0 / json 288.3(en),prose 671.0 / code 480.7 / json 397.3(zh),单位 tokens/1k chars。
|
|
27
|
+
|
|
3
28
|
## 4.4.5 (2026-10-14)
|
|
4
29
|
|
|
5
30
|
### Fixes
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient,
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-B2jrUDhg.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
package/lib/client.js
CHANGED
|
@@ -661,6 +661,8 @@ window.__ModuleLoader__.load({
|
|
|
661
661
|
targetModelId: preferences.targetModelId,
|
|
662
662
|
memberAccountIds: preferences.memberAccountIds
|
|
663
663
|
};
|
|
664
|
+
const overrides = Object.entries(preferences.targetModelIdByAccount ?? {}).filter(([, modelId]) => modelId !== "");
|
|
665
|
+
if (overrides.length > 0) slot["targetModelIdByAccount"] = Object.fromEntries(overrides);
|
|
664
666
|
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
665
667
|
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
666
668
|
await writeField(scope, "regions", region, next, (readBack) => {
|
|
@@ -672,7 +674,12 @@ window.__ModuleLoader__.load({
|
|
|
672
674
|
const stored = pool["memberAccountIds"];
|
|
673
675
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
674
676
|
const wanted = new Set(preferences.memberAccountIds);
|
|
675
|
-
|
|
677
|
+
if (!stored.every((id) => typeof id === "string" && wanted.has(id))) return false;
|
|
678
|
+
const storedModels = pool["targetModelIdByAccount"];
|
|
679
|
+
if (overrides.length === 0) return storedModels === void 0 || typeof storedModels === "object" && storedModels !== null && !Array.isArray(storedModels) && Object.keys(storedModels).length === 0;
|
|
680
|
+
if (typeof storedModels !== "object" || storedModels === null || Array.isArray(storedModels)) return false;
|
|
681
|
+
const storedMap = storedModels;
|
|
682
|
+
return Object.keys(storedMap).length === overrides.length && overrides.every(([accountId, modelId]) => storedMap[accountId] === modelId);
|
|
676
683
|
});
|
|
677
684
|
}
|
|
678
685
|
//#endregion
|
|
@@ -1259,6 +1266,21 @@ window.__ModuleLoader__.load({
|
|
|
1259
1266
|
const wanted = new Set(right);
|
|
1260
1267
|
return left.every((id) => wanted.has(id));
|
|
1261
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Whether two per-account test-model maps are equal (issue #61).
|
|
1271
|
+
*
|
|
1272
|
+
* By CONTENT, not by size: moving an account from one model to another keeps the
|
|
1273
|
+
* count and changes the meaning, and clearing an entry is a change a size
|
|
1274
|
+
* comparison would miss entirely when another account was added at the same
|
|
1275
|
+
* time. Order is irrelevant for the same reason it is in {@link sameIds} — the
|
|
1276
|
+
* map is written by key, not by click sequence.
|
|
1277
|
+
*/
|
|
1278
|
+
function sameModelOverrides(left, right) {
|
|
1279
|
+
const leftKeys = Object.keys(left);
|
|
1280
|
+
const rightKeys = Object.keys(right);
|
|
1281
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1282
|
+
return leftKeys.every((key) => left[key] === right[key]);
|
|
1283
|
+
}
|
|
1262
1284
|
/** Whether a probe outcome means the model answered. */
|
|
1263
1285
|
function outcomeOk(outcome) {
|
|
1264
1286
|
return outcome === "ok";
|
|
@@ -1345,7 +1367,8 @@ window.__ModuleLoader__.load({
|
|
|
1345
1367
|
enabled: pool?.enabled ?? false,
|
|
1346
1368
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1347
1369
|
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
1348
|
-
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1370
|
+
memberAccountIds: pool?.memberAccountIds ?? [],
|
|
1371
|
+
targetModelIdByAccount: { ...pool?.targetModelIdByAccount }
|
|
1349
1372
|
};
|
|
1350
1373
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1351
1374
|
/**
|
|
@@ -1366,7 +1389,7 @@ window.__ModuleLoader__.load({
|
|
|
1366
1389
|
* the pending edit.
|
|
1367
1390
|
*/
|
|
1368
1391
|
const poolRunning = saved.enabled;
|
|
1369
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1392
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameModelOverrides(draft.targetModelIdByAccount, saved.targetModelIdByAccount) || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1370
1393
|
/**
|
|
1371
1394
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
1372
1395
|
*
|
|
@@ -1527,6 +1550,25 @@ window.__ModuleLoader__.load({
|
|
|
1527
1550
|
};
|
|
1528
1551
|
});
|
|
1529
1552
|
}, [editDraft]);
|
|
1553
|
+
/**
|
|
1554
|
+
* Set ONE account's test model (issue #61).
|
|
1555
|
+
*
|
|
1556
|
+
* `''` REMOVES the entry rather than storing an empty string: "follow the
|
|
1557
|
+
* region target" is the absence of a choice, which is the same shape the host
|
|
1558
|
+
* reads and the same shape an untouched profile has — so nothing about an
|
|
1559
|
+
* older profile changes when this setting is never used.
|
|
1560
|
+
*/
|
|
1561
|
+
const setRowModel = (0, react.useCallback)((accountId, modelId) => {
|
|
1562
|
+
editDraft((current) => {
|
|
1563
|
+
const next = { ...current.targetModelIdByAccount };
|
|
1564
|
+
if (modelId === "") delete next[accountId];
|
|
1565
|
+
else next[accountId] = modelId;
|
|
1566
|
+
return {
|
|
1567
|
+
...current,
|
|
1568
|
+
targetModelIdByAccount: next
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1571
|
+
}, [editDraft]);
|
|
1530
1572
|
/** Check every listed account, or none of them. */
|
|
1531
1573
|
const selectAll = (0, react.useCallback)((next) => {
|
|
1532
1574
|
editDraft((current) => ({
|
|
@@ -1847,7 +1889,10 @@ window.__ModuleLoader__.load({
|
|
|
1847
1889
|
},
|
|
1848
1890
|
onRemove: () => {
|
|
1849
1891
|
runRowRemove(account.accountId, account.accountName);
|
|
1850
|
-
}
|
|
1892
|
+
},
|
|
1893
|
+
modelOverride: active.targetModelIdByAccount[account.accountId] ?? "",
|
|
1894
|
+
catalog: pool.catalog ?? [],
|
|
1895
|
+
onModelChange: (modelId) => setRowModel(account.accountId, modelId)
|
|
1851
1896
|
}))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1852
1897
|
className: "dsm-workbuddy-pool-warn",
|
|
1853
1898
|
role: "status"
|
|
@@ -1915,6 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1915
1960
|
/** One account's row. */
|
|
1916
1961
|
function renderAccountRow(input) {
|
|
1917
1962
|
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
1963
|
+
const { modelOverride, catalog, onModelChange } = input;
|
|
1918
1964
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1919
1965
|
const excluded = exclusionText(t, account.excludedBy);
|
|
1920
1966
|
const probe = account.probe;
|
|
@@ -1952,10 +1998,23 @@ window.__ModuleLoader__.load({
|
|
|
1952
1998
|
}), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShort(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShort(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
|
|
1953
1999
|
className: "dsm-workbuddy-pool-probe",
|
|
1954
2000
|
...probe?.message === void 0 ? {} : { title: probe.message }
|
|
1955
|
-
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
2001
|
+
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age), probe === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
1956
2002
|
className: "dsm-workbuddy-pool-unknown",
|
|
1957
2003
|
title: t("row.poolNeverTested")
|
|
1958
|
-
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("
|
|
2004
|
+
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("select", {
|
|
2005
|
+
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-row-model",
|
|
2006
|
+
value: modelOverride,
|
|
2007
|
+
disabled: !canEdit,
|
|
2008
|
+
title: t("row.poolRowModelHint"),
|
|
2009
|
+
"aria-label": t("row.poolRowModelAria", { accountName: name }),
|
|
2010
|
+
onChange: (event) => onModelChange(event.currentTarget.value)
|
|
2011
|
+
}, (0, react.createElement)("option", { value: "" }, t("row.poolRowModelInherit")), modelOverride !== "" && !catalog.some((model) => model.id === modelOverride) ? (0, react.createElement)("option", {
|
|
2012
|
+
value: modelOverride,
|
|
2013
|
+
disabled: true
|
|
2014
|
+
}, t("row.poolTargetStaleOption", { model: modelOverride })) : null, ...catalog.map((model) => (0, react.createElement)("option", {
|
|
2015
|
+
value: model.id,
|
|
2016
|
+
key: model.id
|
|
2017
|
+
}, model.name))), (0, react.createElement)("button", {
|
|
1959
2018
|
type: "button",
|
|
1960
2019
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1961
2020
|
disabled: busy || testDisabled,
|
|
@@ -3140,6 +3199,10 @@ window.__ModuleLoader__.load({
|
|
|
3140
3199
|
.dsm-workbuddy-pool-prov-unknown{background:var(--dsw-alias-bg-layer-3,#2a2c33);border:1px solid var(--dsw-alias-border-l1,#2c2d31);color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3141
3200
|
.dsm-workbuddy-pool-prov-sep{color:var(--dsw-alias-label-dimmed,#9aa0a6);opacity:.7}
|
|
3142
3201
|
.dsm-workbuddy-pool-prov-age{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap}
|
|
3202
|
+
/* WHICH model a verdict is about (issue #61). Deliberately quiet: it is scope
|
|
3203
|
+
information on the provenance line, not a status of its own, so an ok row and
|
|
3204
|
+
a rate-limited row must not look different because of it. */
|
|
3205
|
+
.dsm-workbuddy-pool-prov-model{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}
|
|
3143
3206
|
.dsm-workbuddy-pool-members{display:flex;flex-direction:column;gap:10px}
|
|
3144
3207
|
.dsm-workbuddy-pool-members-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
3145
3208
|
.dsm-workbuddy-pool-members-title{margin:0 0 3px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
@@ -3193,6 +3256,9 @@ window.__ModuleLoader__.load({
|
|
|
3193
3256
|
/* Per-row actions (test / remove), right side of each pool row. */
|
|
3194
3257
|
.dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
|
|
3195
3258
|
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
3259
|
+
/* This account's own test model (issue #61). Narrower than the section-level
|
|
3260
|
+
picker so a long model name cannot push the two buttons out of the row. */
|
|
3261
|
+
.dsm-workbuddy-pool-row-model{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3196
3262
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
3197
3263
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
3198
3264
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -5571,6 +5637,11 @@ window.__ModuleLoader__.load({
|
|
|
5571
5637
|
"row.poolProbeSourceTest": "tested",
|
|
5572
5638
|
"row.poolProbeSourceLive": "hit by a live request",
|
|
5573
5639
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
5640
|
+
"row.poolProbeModel": "model {model}",
|
|
5641
|
+
"row.poolProbeModelUnknown": "model not recorded",
|
|
5642
|
+
"row.poolRowModelHint": "Which model this account's test uses. Leave it on \"region target\" to follow the pool's model; pick one when this account must be tested on the model you actually chat with.",
|
|
5643
|
+
"row.poolRowModelAria": "Test model for {accountName}",
|
|
5644
|
+
"row.poolRowModelInherit": "region target",
|
|
5574
5645
|
"row.poolSettingsTitle": "Account pool settings",
|
|
5575
5646
|
"row.poolSettingsHint": "These are preferences: they take effect when you save (same as the model list). Test results are written automatically and are not affected by this button.",
|
|
5576
5647
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -5837,6 +5908,11 @@ window.__ModuleLoader__.load({
|
|
|
5837
5908
|
"row.poolProbeSourceTest": "测得",
|
|
5838
5909
|
"row.poolProbeSourceLive": "聊天撞上",
|
|
5839
5910
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
5911
|
+
"row.poolProbeModel": "模型 {model}",
|
|
5912
|
+
"row.poolProbeModelUnknown": "未记录模型",
|
|
5913
|
+
"row.poolRowModelHint": "这个账号测试用哪个模型。留「区域目标」就跟随账号池的目标模型;当某个号必须用你实际在聊的模型来测时,在这里单独指定。",
|
|
5914
|
+
"row.poolRowModelAria": "{accountName} 的测试模型",
|
|
5915
|
+
"row.poolRowModelInherit": "区域目标",
|
|
5840
5916
|
"row.poolSettingsTitle": "账号池设置",
|
|
5841
5917
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
5842
5918
|
"row.poolEnabled": "启用账号池",
|
|
@@ -1220,6 +1220,24 @@ function ensureSystemHead(obj) {
|
|
|
1220
1220
|
* `tool_choice` (the upstream's field is a string; object forms return 400),
|
|
1221
1221
|
* and guarantee a leading system message.
|
|
1222
1222
|
*/
|
|
1223
|
+
/**
|
|
1224
|
+
* The model id a prepared body asks for, when it carries one (issue #61).
|
|
1225
|
+
*
|
|
1226
|
+
* Lives beside {@link prepareChatBody} because it reads the same shape: the
|
|
1227
|
+
* serialized OpenAI body, after this plugin's rewrites. Never throws — a
|
|
1228
|
+
* malformed body is the upstream's to reject, not a reason to lose the failure
|
|
1229
|
+
* measurement that this id is needed for.
|
|
1230
|
+
*/
|
|
1231
|
+
function modelIdOfBody(bodyJson) {
|
|
1232
|
+
try {
|
|
1233
|
+
const parsed = JSON.parse(bodyJson);
|
|
1234
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
1235
|
+
const model = parsed["model"];
|
|
1236
|
+
return typeof model === "string" && model !== "" ? model : void 0;
|
|
1237
|
+
} catch {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1223
1241
|
function prepareChatBody(source) {
|
|
1224
1242
|
let body;
|
|
1225
1243
|
try {
|
|
@@ -3807,7 +3825,7 @@ function generationOf(version) {
|
|
|
3807
3825
|
//#endregion
|
|
3808
3826
|
//#region src/version.ts
|
|
3809
3827
|
/** The npm package version this build was produced from. */
|
|
3810
|
-
const WORKBUDDY_CONNECT_VERSION = "4.4.
|
|
3828
|
+
const WORKBUDDY_CONNECT_VERSION = "4.4.6";
|
|
3811
3829
|
//#endregion
|
|
3812
3830
|
//#region src/host-heartbeat.ts
|
|
3813
3831
|
/**
|
|
@@ -3965,4 +3983,4 @@ async function clearHostHeartbeat() {
|
|
|
3965
3983
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
3966
3984
|
}
|
|
3967
3985
|
//#endregion
|
|
3968
|
-
export {
|
|
3986
|
+
export { isEncryptedFieldWrapper as $, workbuddyAccountId as A, modelIdOfBody as B, defaultDesktopAuthDirs as C, isVaultAccountId as D, isEncryptedCredentialError as E, WorkBuddyUpstreamClient as F, regionOf as G, parseReasoning as H, classifyUpstreamError as I, deriveAtRestKey as J, WORKBUDDY_APP_EXECUTABLE_ENV as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, WorkBuddyCredentialRejectedError as P, findWorkbuddyAppExecutableWithSource as Q, isCredentialRejectedError as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, parseUpstreamModel as U, parseCreditMultiplier as V, prepareChatBody as W, fetchAtRestKeyPayload as X, deriveAtRestKeyId as Y, findWorkbuddyAppExecutable as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, workbuddyAppExecutableCandidates as at, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, withFileLock as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, isWorkbuddyBundle as et, WorkBuddyCatalog as f, ENCRYPTED_CREDENTIAL_CODE as g, fallbackModelsFor as h, processStartTimeMs as i, readAtRestKey as it, workbuddyOwnAuthPath as j, parseWorkBuddyAuth as k, piAiRuntimeInfo as l, writeFileAtomic as lt, deriveCatalog as m, clearHostHeartbeat as n, macosNestedAppBundles as nt, workbuddyHostHeartbeatPath as o, hostPeersInfo as ot, applyContextBudgets as p, clearAtRestKeyCache as q, isHeartbeatProcessAlive as r, openEncryptedField as rt, writeHostHeartbeat as s, resolveDshHome as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, macosBundleExecutable as tt, FALLBACK_WORKBUDDY_MODELS as u, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, knownRegionOf as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -798,6 +798,15 @@ declare function knownRegionOf(domain: string): WorkBuddyRegion | undefined;
|
|
|
798
798
|
* `tool_choice` (the upstream's field is a string; object forms return 400),
|
|
799
799
|
* and guarantee a leading system message.
|
|
800
800
|
*/
|
|
801
|
+
/**
|
|
802
|
+
* The model id a prepared body asks for, when it carries one (issue #61).
|
|
803
|
+
*
|
|
804
|
+
* Lives beside {@link prepareChatBody} because it reads the same shape: the
|
|
805
|
+
* serialized OpenAI body, after this plugin's rewrites. Never throws — a
|
|
806
|
+
* malformed body is the upstream's to reject, not a reason to lose the failure
|
|
807
|
+
* measurement that this id is needed for.
|
|
808
|
+
*/
|
|
809
|
+
declare function modelIdOfBody(bodyJson: string): string | undefined;
|
|
801
810
|
declare function prepareChatBody(source: string): string;
|
|
802
811
|
/** Marker carried by {@link WorkBuddyCredentialRejectedError}; survives bundling. */
|
|
803
812
|
declare const CREDENTIAL_REJECTED_CODE = "WORKBUDDY_CREDENTIAL_REJECTED";
|
|
@@ -1107,7 +1116,29 @@ interface WorkBuddyShimOptions {
|
|
|
1107
1116
|
status: number;
|
|
1108
1117
|
kind: UpstreamErrorKind;
|
|
1109
1118
|
message: string;
|
|
1119
|
+
/**
|
|
1120
|
+
* The model that request asked for (issue #61).
|
|
1121
|
+
*
|
|
1122
|
+
* Carried because the failure is remembered against the ACCOUNT, and `6004`
|
|
1123
|
+
* is an account × model condition: without this the record cannot say which
|
|
1124
|
+
* model the measurement belongs to, so the card has to present a
|
|
1125
|
+
* single-model result as account-level availability.
|
|
1126
|
+
*/
|
|
1127
|
+
modelId?: string;
|
|
1110
1128
|
}) => void;
|
|
1129
|
+
/**
|
|
1130
|
+
* Called with the FINAL serialized body of every chat request, just before it
|
|
1131
|
+
* goes upstream (issue #59).
|
|
1132
|
+
*
|
|
1133
|
+
* It exists so the plugin can record what was actually sent without the shim
|
|
1134
|
+
* having to know where that record goes: the region belongs to whoever wires
|
|
1135
|
+
* this up, and the report is a plugin concern. The body handed over is the
|
|
1136
|
+
* post-`prepareChatBody` one, because THAT is what the upstream counts.
|
|
1137
|
+
*
|
|
1138
|
+
* Fired without awaiting: a request must never wait on a diagnostic. The
|
|
1139
|
+
* callee must treat the string as read-only.
|
|
1140
|
+
*/
|
|
1141
|
+
onRequestBody?: (bodyJson: string) => void;
|
|
1111
1142
|
}
|
|
1112
1143
|
/**
|
|
1113
1144
|
* Start the loopback endpoint. Requests must carry the shim's shared secret;
|
|
@@ -1944,6 +1975,31 @@ interface WorkBuddyPoolProbe {
|
|
|
1944
1975
|
* old probe files stay readable rather than being discarded.
|
|
1945
1976
|
*/
|
|
1946
1977
|
message?: string;
|
|
1978
|
+
/**
|
|
1979
|
+
* WHICH MODEL this measurement is about (issue #61).
|
|
1980
|
+
*
|
|
1981
|
+
* The pool's "test all accounts" action probes ONE model — the region's
|
|
1982
|
+
* `targetModelId` — while `6004` is an ACCOUNT × MODEL condition: the reporter
|
|
1983
|
+
* measured two accounts where `hy4-preview-f` answered and `step-5-preview`
|
|
1984
|
+
* returned `429 / 6004` at the same moment. A row rendered as plain "可用"
|
|
1985
|
+
* therefore claims more than the measurement says, and the pool's own routing
|
|
1986
|
+
* reads that `ok` as an account-level fact.
|
|
1987
|
+
*
|
|
1988
|
+
* Storing the model id is what makes the claim checkable: the card can say
|
|
1989
|
+
* WHICH model an `ok` came from, and a reader can see that a different model
|
|
1990
|
+
* was never tested at all. `probeModel()` has always returned this field; the
|
|
1991
|
+
* write simply dropped it.
|
|
1992
|
+
*
|
|
1993
|
+
* Deliberately NOT inherited by {@link mergePoolProbe} when an update omits it
|
|
1994
|
+
* — the opposite of `accountName` and `retryAtMs`. Those two describe the
|
|
1995
|
+
* ACCOUNT (a name that a later writer could not see is still true, and a
|
|
1996
|
+
* future refresh time is still in the future), whereas the model describes
|
|
1997
|
+
* THIS measurement. Carrying an older model id forward would relabel a fresh
|
|
1998
|
+
* measurement with a model it was not taken on, which is exactly the confusion
|
|
1999
|
+
* this field exists to end. ABSENT therefore means "unknown scope", never a
|
|
2000
|
+
* guess.
|
|
2001
|
+
*/
|
|
2002
|
+
modelId?: string;
|
|
1947
2003
|
/**
|
|
1948
2004
|
* The account's display name AT THE MOMENT IT WAS MEASURED (issue #60).
|
|
1949
2005
|
*
|
|
@@ -2483,7 +2539,13 @@ interface WorkBuddyWebPoolAccount {
|
|
|
2483
2539
|
expiringSoon?: number;
|
|
2484
2540
|
/** When the nearest package expires, in ms. */
|
|
2485
2541
|
nearestExpiryMs?: number;
|
|
2486
|
-
/**
|
|
2542
|
+
/**
|
|
2543
|
+
* The last test of this account's target model, absent when never tested.
|
|
2544
|
+
*
|
|
2545
|
+
* The model is recorded INSIDE the probe (issue #61) rather than assumed from
|
|
2546
|
+
* the row's current setting: the setting can be changed after the test, and a
|
|
2547
|
+
* result must keep naming what it actually measured.
|
|
2548
|
+
*/
|
|
2487
2549
|
probe?: {
|
|
2488
2550
|
outcome: WorkBuddyWebProbeOutcome;
|
|
2489
2551
|
atMs: number;
|
|
@@ -2495,6 +2557,16 @@ interface WorkBuddyWebPoolAccount {
|
|
|
2495
2557
|
* guess which of the two it was.
|
|
2496
2558
|
*/
|
|
2497
2559
|
source?: 'test-batch' | 'live-request';
|
|
2560
|
+
/**
|
|
2561
|
+
* WHICH model this measurement is about (issue #61).
|
|
2562
|
+
*
|
|
2563
|
+
* `6004` is an account × model condition, so an `ok` without a model reads as
|
|
2564
|
+
* account-level availability — which is exactly the false positive the
|
|
2565
|
+
* reporter hit: the card said 可用 for two accounts whose actual model
|
|
2566
|
+
* returned `429 / 6004`. ABSENT for records written before the field existed;
|
|
2567
|
+
* the card must then say the scope is unknown rather than assume it.
|
|
2568
|
+
*/
|
|
2569
|
+
modelId?: string;
|
|
2498
2570
|
/** Only when the upstream named a time; never invented locally. */
|
|
2499
2571
|
retryAtMs?: number;
|
|
2500
2572
|
/**
|
|
@@ -2527,6 +2599,17 @@ interface WorkBuddyWebPoolAccount {
|
|
|
2527
2599
|
* result the user just watched succeed.
|
|
2528
2600
|
*/
|
|
2529
2601
|
checkedInToday?: boolean;
|
|
2602
|
+
/**
|
|
2603
|
+
* The model a TEST of this row would use (issue #61).
|
|
2604
|
+
*
|
|
2605
|
+
* Per row, because a test model can be chosen per account: the card shows it
|
|
2606
|
+
* beside the row it applies to rather than implying one model for the table.
|
|
2607
|
+
*/
|
|
2608
|
+
testModelId?: string;
|
|
2609
|
+
/** Where this row's test target came from (see {@link WorkBuddyWebPool.targetModelSource}). */
|
|
2610
|
+
testModelSource?: 'preferred' | 'free' | 'none' | 'stale';
|
|
2611
|
+
/** This row's saved override that the region no longer offers. */
|
|
2612
|
+
staleTestModelId?: string;
|
|
2530
2613
|
}
|
|
2531
2614
|
/**
|
|
2532
2615
|
* The pool's state for one region.
|
|
@@ -2556,6 +2639,14 @@ interface WorkBuddyWebPool {
|
|
|
2556
2639
|
* would run against a model the upstream does not offer.
|
|
2557
2640
|
*/
|
|
2558
2641
|
staleTargetModelId?: string;
|
|
2642
|
+
/**
|
|
2643
|
+
* The SAVED per-account test models, keyed by account id (issue #61).
|
|
2644
|
+
*
|
|
2645
|
+
* Sent verbatim rather than as the effective set: the card edits this map, and
|
|
2646
|
+
* an id whose model has left the catalog must survive a save so the user's
|
|
2647
|
+
* record of what they chose is not silently destroyed.
|
|
2648
|
+
*/
|
|
2649
|
+
targetModelIdByAccount?: Readonly<Record<string, string>>;
|
|
2559
2650
|
/**
|
|
2560
2651
|
* Where the target came from. `'stale'` means the SAVED id is no longer in
|
|
2561
2652
|
* the catalog — a distinct state from `'none'` (no free model exists),
|
|
@@ -2840,6 +2931,8 @@ interface WorkBuddyPoolDeps {
|
|
|
2840
2931
|
preferences(region: WorkBuddyRegion): {
|
|
2841
2932
|
enabled: boolean;
|
|
2842
2933
|
targetModelId: string;
|
|
2934
|
+
/** Per-account override of `targetModelId` (issue #61). */
|
|
2935
|
+
targetModelIdByAccount?: Readonly<Record<string, string>>;
|
|
2843
2936
|
/**
|
|
2844
2937
|
* Tokens each test probe sends as saved; `0` when the user never chose.
|
|
2845
2938
|
*
|
|
@@ -2935,7 +3028,18 @@ interface WorkBuddyPoolDeps {
|
|
|
2935
3028
|
* the route handles both, because a missing reporter must degrade to the
|
|
2936
3029
|
* previous behaviour rather than to no rows at all.
|
|
2937
3030
|
*/
|
|
2938
|
-
test?(region: WorkBuddyRegion,
|
|
3031
|
+
test?(region: WorkBuddyRegion,
|
|
3032
|
+
/**
|
|
3033
|
+
* One model for the whole batch, or a per-account resolver (issue #61).
|
|
3034
|
+
*
|
|
3035
|
+
* The resolver form is what lets a batch honour a per-account test model; a
|
|
3036
|
+
* resolver that names no model yields an `unavailable` row for that account
|
|
3037
|
+
* rather than silently testing the region default.
|
|
3038
|
+
*/
|
|
3039
|
+
modelId: string | ((accountId: string) => {
|
|
3040
|
+
modelId?: string;
|
|
3041
|
+
staleModelId?: string;
|
|
3042
|
+
}), onRow?: (row: WorkBuddyPoolTestRow) => void): Promise<readonly WorkBuddyPoolTestRow[]>;
|
|
2939
3043
|
/** The region's live catalog, used to resolve the free target model. */
|
|
2940
3044
|
catalog?(region: WorkBuddyRegion): readonly WorkBuddyModelInfo[];
|
|
2941
3045
|
}
|
|
@@ -3045,6 +3149,18 @@ interface WorkBuddyPoolPreferences {
|
|
|
3045
3149
|
enabled?: boolean;
|
|
3046
3150
|
/** Model id to test; `''` means auto-pick a free model from this region. */
|
|
3047
3151
|
targetModelId?: string;
|
|
3152
|
+
/**
|
|
3153
|
+
* Per-account override of {@link targetModelId}, keyed by account id (#61).
|
|
3154
|
+
*
|
|
3155
|
+
* `6004` is an ACCOUNT × MODEL condition, so "can this account serve" has no
|
|
3156
|
+
* single answer: the reporter measured two accounts where one model answered
|
|
3157
|
+
* and another returned `429 / 6004` in the same second. The region-wide target
|
|
3158
|
+
* stays the default — this map only says "for THIS account, test THAT model",
|
|
3159
|
+
* which is what makes the pool's verdict match the model the user is actually
|
|
3160
|
+
* chatting with. An id that is no longer in the catalog is reported as stale
|
|
3161
|
+
* rather than replaced, so no credits are spent on a model nobody chose.
|
|
3162
|
+
*/
|
|
3163
|
+
targetModelIdByAccount?: Record<string, string>;
|
|
3048
3164
|
/**
|
|
3049
3165
|
* How much input a probe sends, in tokens — the pool's biggest single knob.
|
|
3050
3166
|
*
|
|
@@ -3216,4 +3332,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
3216
3332
|
*/
|
|
3217
3333
|
declare function apply(ctx: Context, config: Config): void;
|
|
3218
3334
|
//#endregion
|
|
3219
|
-
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
3335
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, modelIdOfBody, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-B2jrUDhg.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -1776,6 +1776,7 @@ function createWorkBuddyShim(options) {
|
|
|
1776
1776
|
const failoverAccount = options.failoverAccount;
|
|
1777
1777
|
const prepareAccount = options.prepareAccount;
|
|
1778
1778
|
const onAccountFailure = options.onAccountFailure;
|
|
1779
|
+
const onRequestBody = options.onRequestBody;
|
|
1779
1780
|
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
1780
1781
|
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
1781
1782
|
function bearerOk(req) {
|
|
@@ -1886,6 +1887,12 @@ function createWorkBuddyShim(options) {
|
|
|
1886
1887
|
}
|
|
1887
1888
|
const raw = (await readBody(req)).toString("utf8");
|
|
1888
1889
|
const prepared = prepareChatBody(raw);
|
|
1890
|
+
const requestModelId = modelIdOfBody(prepared);
|
|
1891
|
+
if (onRequestBody !== void 0) try {
|
|
1892
|
+
onRequestBody(prepared);
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
logger?.warn("dsh-connect-workbuddy: body report failed", error);
|
|
1895
|
+
}
|
|
1889
1896
|
const controller = new AbortController();
|
|
1890
1897
|
res.on("close", () => controller.abort());
|
|
1891
1898
|
let result = await client.chatStream(credential, prepared, controller.signal);
|
|
@@ -1929,7 +1936,8 @@ function createWorkBuddyShim(options) {
|
|
|
1929
1936
|
onAccountFailure(entry.accountId, {
|
|
1930
1937
|
status: entry.failure.status,
|
|
1931
1938
|
kind: entry.failure.kind,
|
|
1932
|
-
message: entry.failure.message
|
|
1939
|
+
message: entry.failure.message,
|
|
1940
|
+
...requestModelId === void 0 ? {} : { modelId: requestModelId }
|
|
1933
1941
|
});
|
|
1934
1942
|
} catch (error) {
|
|
1935
1943
|
logger?.warn("dsh-connect-workbuddy: recording an account failure failed", error);
|
|
@@ -2042,6 +2050,246 @@ function createWorkBuddyShim(options) {
|
|
|
2042
2050
|
};
|
|
2043
2051
|
}
|
|
2044
2052
|
//#endregion
|
|
2053
|
+
//#region src/body-report.ts
|
|
2054
|
+
/**
|
|
2055
|
+
* Body-composition report: WHAT was sent upstream, in counts (issue #59).
|
|
2056
|
+
*
|
|
2057
|
+
* WHY THIS EXISTS. Issue #59's reporter measured the SAME conversation charged
|
|
2058
|
+
* ~708K tokens by the CN gateway and ~1.374M by the international one (1.94×),
|
|
2059
|
+
* which turns a region switch into an instant `code 11115 prompt is too long`.
|
|
2060
|
+
* Two worlds explain that, and they have opposite owners:
|
|
2061
|
+
*
|
|
2062
|
+
* - the gateways count identical bytes differently (upstream's business); or
|
|
2063
|
+
* - the two requests did not carry the same messages (ours to fix).
|
|
2064
|
+
*
|
|
2065
|
+
* The second world was left standing by two independent measurements. The
|
|
2066
|
+
* reporter ran this project's `--kind`/`--language` probe on BOTH regions and
|
|
2067
|
+
* got the same tokens-per-1000-chars on byte-identical prose, code and JSON
|
|
2068
|
+
* (ratios 0.98–1.06 across six cells), so identical bytes really do bill
|
|
2069
|
+
* identically. And a code trace showed historical `reasoning` never reaches the
|
|
2070
|
+
* wire at all: pi-ai only forwards `thinking` blocks as text when
|
|
2071
|
+
* `compat.requiresThinkingAsText` is set — this plugin never sets it — and the
|
|
2072
|
+
* `reasoning_details` route needs a signature the stream parser never writes.
|
|
2073
|
+
* With the counting difference excluded and reasoning excluded, the remaining
|
|
2074
|
+
* candidate is that the two bodies differ in the first place.
|
|
2075
|
+
*
|
|
2076
|
+
* This module exists to settle exactly that, and it can do it WITHOUT either
|
|
2077
|
+
* region having credit: the body is assembled locally, before the upstream ever
|
|
2078
|
+
* gets a chance to answer `14018 Credits exhausted`. Capture one composition per
|
|
2079
|
+
* region in the same session and diff them.
|
|
2080
|
+
*
|
|
2081
|
+
* It records COUNTS ONLY — never a byte of message text, no account name, no
|
|
2082
|
+
* token. That is a hard rule, not a default: the point is to compare shape
|
|
2083
|
+
* (`messages.length`, per-block type, CJK vs non-CJK characters, total bytes),
|
|
2084
|
+
* and every one of those is answerable arithmetically. `tests/issue59-body-report.spec.ts`
|
|
2085
|
+
* asserts the report cannot leak a fixture's text.
|
|
2086
|
+
*/
|
|
2087
|
+
/**
|
|
2088
|
+
* Environment variable that enables the report.
|
|
2089
|
+
*
|
|
2090
|
+
* Unset or blank means OFF — a diagnostic that writes files must be opt-in.
|
|
2091
|
+
* `1`/`true` writes to `<DSH home>/body-reports/`; anything else is read as a
|
|
2092
|
+
* directory path, so a user can point it at an existing folder (and, on
|
|
2093
|
+
* Windows, one with spaces).
|
|
2094
|
+
*/
|
|
2095
|
+
const WORKBUDDY_BODY_REPORT_ENV = "WORKBUDDY_BODY_REPORT";
|
|
2096
|
+
/**
|
|
2097
|
+
* Character ranges that tokenize (and therefore bill) like CJK.
|
|
2098
|
+
*
|
|
2099
|
+
* Deliberately generous: the caller's question is "how much of this is the ~3×
|
|
2100
|
+
* kind", and the measured spread is 0.669 vs 0.224 tokens/char, so a character
|
|
2101
|
+
* class that is borderline costs far less than mis-attributing a whole bucket.
|
|
2102
|
+
* Ranges follow Unicode blocks, plus the compatibility forms and the two
|
|
2103
|
+
* extension planes where rare Han characters live.
|
|
2104
|
+
*/
|
|
2105
|
+
const CJK_RANGES = [
|
|
2106
|
+
[11904, 12031],
|
|
2107
|
+
[12288, 12351],
|
|
2108
|
+
[12352, 12543],
|
|
2109
|
+
[13312, 19903],
|
|
2110
|
+
[19968, 40959],
|
|
2111
|
+
[63744, 64255],
|
|
2112
|
+
[65072, 65103],
|
|
2113
|
+
[65280, 65519],
|
|
2114
|
+
[131072, 195103]
|
|
2115
|
+
];
|
|
2116
|
+
/** Whether one code point bills like CJK. */
|
|
2117
|
+
function isCjkCodePoint(code) {
|
|
2118
|
+
return CJK_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
2119
|
+
}
|
|
2120
|
+
/** Count characters (code points) and how many of them are CJK. */
|
|
2121
|
+
function countChars(text) {
|
|
2122
|
+
let chars = 0;
|
|
2123
|
+
let cjkChars = 0;
|
|
2124
|
+
for (const character of text) {
|
|
2125
|
+
chars += 1;
|
|
2126
|
+
const code = character.codePointAt(0);
|
|
2127
|
+
if (code !== void 0 && isCjkCodePoint(code)) cjkChars += 1;
|
|
2128
|
+
}
|
|
2129
|
+
return {
|
|
2130
|
+
chars,
|
|
2131
|
+
cjkChars
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
/** Build one block report from already-extracted text. */
|
|
2135
|
+
function block(type, text) {
|
|
2136
|
+
return {
|
|
2137
|
+
type,
|
|
2138
|
+
...countChars(text)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
/** Whether a value is a plain object (not null, not an array). */
|
|
2142
|
+
function isRecord(value) {
|
|
2143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Describe one message's blocks.
|
|
2147
|
+
*
|
|
2148
|
+
* The role decides what a content string IS, and that is not cosmetic: a
|
|
2149
|
+
* `tool` message's payload is a tool result, so counting it as `text` would hide
|
|
2150
|
+
* the very bucket a long agentic session is mostly made of.
|
|
2151
|
+
*/
|
|
2152
|
+
function describeMessage(message) {
|
|
2153
|
+
const role = typeof message["role"] === "string" ? message["role"] : "unknown";
|
|
2154
|
+
const blocks = [];
|
|
2155
|
+
const content = message["content"];
|
|
2156
|
+
/**
|
|
2157
|
+
* The block type a plain string carries, by the ROLE carrying it.
|
|
2158
|
+
*
|
|
2159
|
+
* A `tool` message's string is a tool RESULT and a `system` one is the system
|
|
2160
|
+
* prompt; calling both `text` would fold the biggest bucket of a long agentic
|
|
2161
|
+
* session into the smallest one, which is the opposite of what a composition
|
|
2162
|
+
* report is for.
|
|
2163
|
+
*/
|
|
2164
|
+
const stringType = role === "tool" ? "tool-result" : role === "system" || role === "developer" ? "system" : "text";
|
|
2165
|
+
if (typeof content === "string") {
|
|
2166
|
+
if (content !== "") blocks.push(block(stringType, content));
|
|
2167
|
+
} else if (Array.isArray(content)) for (const part of content) {
|
|
2168
|
+
if (!isRecord(part)) continue;
|
|
2169
|
+
const type = typeof part["type"] === "string" ? part["type"] : "unknown";
|
|
2170
|
+
if (type === "text") {
|
|
2171
|
+
const text = part["text"];
|
|
2172
|
+
if (typeof text === "string" && text !== "") blocks.push(block(stringType, text));
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
blocks.push({
|
|
2176
|
+
type: type === "image_url" ? "image" : type,
|
|
2177
|
+
chars: 0,
|
|
2178
|
+
cjkChars: 0
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
const toolCalls = message["tool_calls"];
|
|
2182
|
+
if (Array.isArray(toolCalls)) for (const call of toolCalls) {
|
|
2183
|
+
if (!isRecord(call)) continue;
|
|
2184
|
+
const fn = call["function"];
|
|
2185
|
+
const name = isRecord(fn) && typeof fn["name"] === "string" ? fn["name"] : "";
|
|
2186
|
+
const args = isRecord(fn) && typeof fn["arguments"] === "string" ? fn["arguments"] : "";
|
|
2187
|
+
blocks.push(block("tool-call", `${name}${args}`));
|
|
2188
|
+
}
|
|
2189
|
+
for (const field of [
|
|
2190
|
+
"reasoning_content",
|
|
2191
|
+
"reasoning",
|
|
2192
|
+
"reasoning_text"
|
|
2193
|
+
]) {
|
|
2194
|
+
const value = message[field];
|
|
2195
|
+
if (typeof value === "string" && value !== "") blocks.push(block("reasoning", value));
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
role,
|
|
2199
|
+
chars: blocks.reduce((sum, entry) => sum + entry.chars, 0),
|
|
2200
|
+
cjkChars: blocks.reduce((sum, entry) => sum + entry.cjkChars, 0),
|
|
2201
|
+
blocks
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Describe one serialized request body.
|
|
2206
|
+
*
|
|
2207
|
+
* Returns `undefined` when the body is not a JSON object — a diagnostic must
|
|
2208
|
+
* never be the reason a chat request fails, so every caller treats `undefined`
|
|
2209
|
+
* as "nothing to report" rather than an error.
|
|
2210
|
+
*/
|
|
2211
|
+
function describeBody(bodyJson, region, nowMs) {
|
|
2212
|
+
let parsed;
|
|
2213
|
+
try {
|
|
2214
|
+
parsed = JSON.parse(bodyJson);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (!isRecord(parsed)) return void 0;
|
|
2219
|
+
const rawMessages = parsed["messages"];
|
|
2220
|
+
const messages = Array.isArray(rawMessages) ? rawMessages.filter(isRecord).map(describeMessage) : [];
|
|
2221
|
+
const rawTools = parsed["tools"];
|
|
2222
|
+
const tools = Array.isArray(rawTools) ? block("tools", rawTools.map((tool) => isRecord(tool) ? JSON.stringify(tool) : "").join("")) : {
|
|
2223
|
+
type: "tools",
|
|
2224
|
+
chars: 0,
|
|
2225
|
+
cjkChars: 0
|
|
2226
|
+
};
|
|
2227
|
+
const chars = messages.reduce((sum, message) => sum + message.chars, 0) + tools.chars;
|
|
2228
|
+
const cjkChars = messages.reduce((sum, message) => sum + message.cjkChars, 0) + tools.cjkChars;
|
|
2229
|
+
return {
|
|
2230
|
+
at: new Date(nowMs).toISOString(),
|
|
2231
|
+
region,
|
|
2232
|
+
...typeof parsed["model"] === "string" ? { model: parsed["model"] } : {},
|
|
2233
|
+
bodyBytes: Buffer.byteLength(bodyJson, "utf8"),
|
|
2234
|
+
messageCount: messages.length,
|
|
2235
|
+
chars,
|
|
2236
|
+
cjkChars,
|
|
2237
|
+
tools,
|
|
2238
|
+
messages
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Resolve the directory the report writes to, or `undefined` when disabled.
|
|
2243
|
+
*
|
|
2244
|
+
* Exported for the same reason the other env-var resolvers in this plugin are:
|
|
2245
|
+
* the behaviour is worth a unit test, and the test must not have to set a real
|
|
2246
|
+
* process-wide variable to get one.
|
|
2247
|
+
*/
|
|
2248
|
+
function bodyReportDirectory(env = process.env) {
|
|
2249
|
+
const configured = env[WORKBUDDY_BODY_REPORT_ENV];
|
|
2250
|
+
if (configured === void 0) return void 0;
|
|
2251
|
+
const trimmed = configured.trim();
|
|
2252
|
+
if (trimmed === "") return void 0;
|
|
2253
|
+
if (trimmed === "1" || trimmed.toLowerCase() === "true") return join(resolveDshHome(), "body-reports");
|
|
2254
|
+
return trimmed;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Append one composition to `<dir>/<region>.jsonl`.
|
|
2258
|
+
*
|
|
2259
|
+
* One file per region is the whole point: the reporter captures a session, then
|
|
2260
|
+
* diffs `cn.jsonl`'s last line against `global.jsonl`'s. JSON Lines rather than
|
|
2261
|
+
* one file per request because a single turn makes many upstream calls (one per
|
|
2262
|
+
* tool step) and the LAST one is the interesting one.
|
|
2263
|
+
*
|
|
2264
|
+
* Read-modify-write is avoided on purpose — a long session would rewrite a
|
|
2265
|
+
* growing file on every step. The lock keeps two concurrent requests from
|
|
2266
|
+
* interleaving a line.
|
|
2267
|
+
*/
|
|
2268
|
+
async function appendBodyReport(directory, report) {
|
|
2269
|
+
const file = join(directory, `${report.region}.jsonl`);
|
|
2270
|
+
await mkdir(directory, { recursive: true });
|
|
2271
|
+
const line = `${JSON.stringify(report)}\n`;
|
|
2272
|
+
await withFileLock(file, () => appendFile(file, line, "utf8"));
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Record one outgoing body, if the report is enabled.
|
|
2276
|
+
*
|
|
2277
|
+
* NEVER THROWS and never awaits long: this is called on the request path, and a
|
|
2278
|
+
* chat request must not fail (or stall) because a diagnostic could not write a
|
|
2279
|
+
* file. A failure is reported through `onError` so the caller can log it.
|
|
2280
|
+
*/
|
|
2281
|
+
async function recordBodyReport(region, bodyJson, options = {}) {
|
|
2282
|
+
const directory = bodyReportDirectory(options.env);
|
|
2283
|
+
if (directory === void 0) return;
|
|
2284
|
+
try {
|
|
2285
|
+
const report = describeBody(bodyJson, region, options.nowMs ?? Date.now());
|
|
2286
|
+
if (report === void 0) return;
|
|
2287
|
+
await appendBodyReport(directory, report);
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
options.onError?.(error);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
//#endregion
|
|
2045
2293
|
//#region src/probe.ts
|
|
2046
2294
|
/**
|
|
2047
2295
|
* How long to bench a MODEL that answered 14003 with no stated time.
|
|
@@ -2504,7 +2752,22 @@ async function testAllAccounts(targets, modelId, deps, onRow) {
|
|
|
2504
2752
|
const rows = [];
|
|
2505
2753
|
for (const [index, target] of targets.entries()) {
|
|
2506
2754
|
if (index > 0) await (deps.wait ?? defaultWait)(400);
|
|
2507
|
-
const
|
|
2755
|
+
const wanted = typeof modelId === "function" ? modelId(target.accountId) : { modelId };
|
|
2756
|
+
if (wanted.modelId === void 0) {
|
|
2757
|
+
const row = {
|
|
2758
|
+
accountId: target.accountId,
|
|
2759
|
+
accountName: target.accountName,
|
|
2760
|
+
result: {
|
|
2761
|
+
modelId: wanted.staleModelId ?? "",
|
|
2762
|
+
outcome: "unavailable",
|
|
2763
|
+
message: wanted.staleModelId === void 0 ? "this region offers no model to test" : `the test model "${wanted.staleModelId}" is no longer offered in this region`
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
rows.push(row);
|
|
2767
|
+
onRow?.(row);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const row = await testOne(target, wanted.modelId, deps);
|
|
2508
2771
|
rows.push(row);
|
|
2509
2772
|
onRow?.(row);
|
|
2510
2773
|
}
|
|
@@ -2567,6 +2830,7 @@ function probeUpdatesOf(rows) {
|
|
|
2567
2830
|
outcome: row.result.outcome,
|
|
2568
2831
|
atMs,
|
|
2569
2832
|
source: "test-batch",
|
|
2833
|
+
modelId: row.result.modelId,
|
|
2570
2834
|
...row.accountName === "" ? {} : { accountName: row.accountName },
|
|
2571
2835
|
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
2572
2836
|
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
@@ -2810,6 +3074,30 @@ function pickFreeModel(catalog) {
|
|
|
2810
3074
|
* That is the safe direction: no credits are spent while the roster is unknown,
|
|
2811
3075
|
* and the card offers to switch back to automatic.
|
|
2812
3076
|
*/
|
|
3077
|
+
/**
|
|
3078
|
+
* The model ONE account's test should target (issue #61).
|
|
3079
|
+
*
|
|
3080
|
+
* The region-wide `targetModelId` is one value, but `6004` is an ACCOUNT × MODEL
|
|
3081
|
+
* condition: the reporter measured two accounts where `hy4-preview-f` answered
|
|
3082
|
+
* `ok` and `step-5-preview` returned `429 / 6004` at the same moment. Testing
|
|
3083
|
+
* only the region's model therefore cannot answer "can this account serve the
|
|
3084
|
+
* model I actually use", which is the question the pool's test button is read as
|
|
3085
|
+
* answering.
|
|
3086
|
+
*
|
|
3087
|
+
* A per-account override is the smallest thing that makes the question
|
|
3088
|
+
* answerable, and it is additive: an account with no entry keeps using
|
|
3089
|
+
* `targetModelId`, so every existing profile behaves identically.
|
|
3090
|
+
*
|
|
3091
|
+
* Returns a PREFERENCE, not a resolved target — `resolveTargetModel` still
|
|
3092
|
+
* validates it against the catalog, so an override pointing at a model this
|
|
3093
|
+
* region no longer offers is reported as stale rather than silently swapped for
|
|
3094
|
+
* one the user did not choose.
|
|
3095
|
+
*/
|
|
3096
|
+
function targetModelPreference(preferences, accountId) {
|
|
3097
|
+
const perAccount = accountId === void 0 ? void 0 : preferences.targetModelIdByAccount?.[accountId];
|
|
3098
|
+
if (perAccount !== void 0 && perAccount !== "") return perAccount;
|
|
3099
|
+
return preferences.targetModelId;
|
|
3100
|
+
}
|
|
2813
3101
|
function resolveTargetModel(catalog, preferredId) {
|
|
2814
3102
|
if (preferredId !== void 0 && preferredId !== "") return catalog.some((model) => model.id === preferredId) ? {
|
|
2815
3103
|
modelId: preferredId,
|
|
@@ -4014,7 +4302,8 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4014
4302
|
const preferences = pool.preferences(region);
|
|
4015
4303
|
const members = await pool.members(region);
|
|
4016
4304
|
const ranked = rankPool(members, Date.now());
|
|
4017
|
-
const
|
|
4305
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4306
|
+
const target = resolveTargetModel(catalog, preferences.targetModelId);
|
|
4018
4307
|
const currentAccountId = await pool.currentAccountId?.(region);
|
|
4019
4308
|
const byId = new Map(members.map((member) => [member.account.id, member]));
|
|
4020
4309
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
@@ -4053,8 +4342,17 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4053
4342
|
atMs: measured.probe.atMs,
|
|
4054
4343
|
...measured.probe.source === void 0 ? {} : { source: measured.probe.source },
|
|
4055
4344
|
...measured.probe.retryAtMs === void 0 ? {} : { retryAtMs: measured.probe.retryAtMs },
|
|
4056
|
-
...measured.probe.message === void 0 ? {} : { message: measured.probe.message }
|
|
4345
|
+
...measured.probe.message === void 0 ? {} : { message: measured.probe.message },
|
|
4346
|
+
...measured.probe.modelId === void 0 ? {} : { modelId: measured.probe.modelId }
|
|
4057
4347
|
} },
|
|
4348
|
+
...(() => {
|
|
4349
|
+
const perRow = resolveTargetModel(catalog, targetModelPreference(preferences, account.id));
|
|
4350
|
+
return {
|
|
4351
|
+
...perRow.modelId === void 0 ? {} : { testModelId: perRow.modelId },
|
|
4352
|
+
testModelSource: perRow.source,
|
|
4353
|
+
...perRow.staleModelId === void 0 ? {} : { staleTestModelId: perRow.staleModelId }
|
|
4354
|
+
};
|
|
4355
|
+
})(),
|
|
4058
4356
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
4059
4357
|
current: account.id === currentAccountId,
|
|
4060
4358
|
member,
|
|
@@ -4067,6 +4365,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4067
4365
|
...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
|
|
4068
4366
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
4069
4367
|
targetModelSource: target.source,
|
|
4368
|
+
targetModelIdByAccount: preferences.targetModelIdByAccount ?? {},
|
|
4070
4369
|
memberAccountIds: preferences.memberAccountIds,
|
|
4071
4370
|
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
4072
4371
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
@@ -4310,7 +4609,20 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4310
4609
|
reason: "pool-unavailable",
|
|
4311
4610
|
error: "pool test unavailable"
|
|
4312
4611
|
});
|
|
4313
|
-
const
|
|
4612
|
+
const preferences = pool.preferences(region);
|
|
4613
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4614
|
+
/**
|
|
4615
|
+
* The model ONE member is tested on (issue #61).
|
|
4616
|
+
*
|
|
4617
|
+
* Per account, because a test model is a per-account choice: `6004` is
|
|
4618
|
+
* an account × model condition, so a batch that tested every member on
|
|
4619
|
+
* one model would report "可用" for an account whose own model is the
|
|
4620
|
+
* one that is limited. Resolution still runs through
|
|
4621
|
+
* `resolveTargetModel`, so a stale override is reported, never
|
|
4622
|
+
* silently swapped.
|
|
4623
|
+
*/
|
|
4624
|
+
const targetFor = (accountId) => resolveTargetModel(catalog, targetModelPreference(preferences, accountId));
|
|
4625
|
+
const target = resolveTargetModel(catalog, targetModelPreference(preferences, void 0));
|
|
4314
4626
|
if (target.modelId === void 0) return json(res, 409, {
|
|
4315
4627
|
action: "test",
|
|
4316
4628
|
modelId: void 0,
|
|
@@ -4332,7 +4644,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4332
4644
|
modelId: target.modelId
|
|
4333
4645
|
});
|
|
4334
4646
|
try {
|
|
4335
|
-
await pool.test(region,
|
|
4647
|
+
await pool.test(region, targetFor, (row) => {
|
|
4336
4648
|
writeLine({ row });
|
|
4337
4649
|
});
|
|
4338
4650
|
writeLine({ done: true });
|
|
@@ -4670,6 +4982,7 @@ const modelConfig = z.object({
|
|
|
4670
4982
|
const poolConfig = z.object({
|
|
4671
4983
|
enabled: z.boolean().default(false).description("Whether this region's account pool is active (opt-in). While on, a failed chat request is retried against the other usable pool members before the error is reported."),
|
|
4672
4984
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
4985
|
+
targetModelIdByAccount: z.dict(z.string()).default({}).description("Per-account test model, keyed by account id; an account with no entry uses targetModelId. Use this when an account must be tested on the model you actually chat with, because 6004 is an account-by-model condition."),
|
|
4673
4986
|
/**
|
|
4674
4987
|
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
4675
4988
|
*
|
|
@@ -4927,6 +5240,9 @@ function apply(ctx, config) {
|
|
|
4927
5240
|
* traffic once the provider is registered, which happens after this whole
|
|
4928
5241
|
* setup completes.
|
|
4929
5242
|
*/
|
|
5243
|
+
onRequestBody: (body) => {
|
|
5244
|
+
recordBodyReport(region, body, { onError: (error) => ctx.logger.warn("dsh-connect-workbuddy: body report failed", error) });
|
|
5245
|
+
},
|
|
4930
5246
|
prepareAccount: () => applyPoolSelection(region),
|
|
4931
5247
|
failoverAccount: (triedAccountIds) => failoverAccountFor(region, triedAccountIds),
|
|
4932
5248
|
onAccountFailure: (accountId, failure) => {
|
|
@@ -5260,7 +5576,7 @@ function apply(ctx, config) {
|
|
|
5260
5576
|
* rather than pretending a test ran.
|
|
5261
5577
|
*/
|
|
5262
5578
|
testOne: async (region, accountId) => {
|
|
5263
|
-
const target = regionTargetModel(region);
|
|
5579
|
+
const target = regionTargetModel(region, accountId);
|
|
5264
5580
|
if (target === void 0) return void 0;
|
|
5265
5581
|
const store = stacks[region].store;
|
|
5266
5582
|
const credential = await store.credentialFor(accountId).catch(() => void 0);
|
|
@@ -5535,6 +5851,7 @@ function apply(ctx, config) {
|
|
|
5535
5851
|
outcome,
|
|
5536
5852
|
atMs: Date.now(),
|
|
5537
5853
|
source: "live-request",
|
|
5854
|
+
...failure.modelId === void 0 ? {} : { modelId: failure.modelId },
|
|
5538
5855
|
...retryAtMs === void 0 ? {} : { retryAtMs },
|
|
5539
5856
|
...message === "" ? {} : { message }
|
|
5540
5857
|
} });
|
|
@@ -5625,11 +5942,12 @@ function apply(ctx, config) {
|
|
|
5625
5942
|
/**
|
|
5626
5943
|
* The model a per-row test would use right now: the SAME resolution the
|
|
5627
5944
|
* batch route performs (`resolveTargetModel` over the region's displayed
|
|
5628
|
-
* catalog and the saved preference
|
|
5629
|
-
*
|
|
5945
|
+
* catalog and the saved preference — including a per-account override,
|
|
5946
|
+
* issue #61), so a row test and a batch test never measure different models.
|
|
5947
|
+
* Undefined mirrors the route's 409 states.
|
|
5630
5948
|
*/
|
|
5631
|
-
const regionTargetModel = (region) => {
|
|
5632
|
-
return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region)
|
|
5949
|
+
const regionTargetModel = (region, accountId) => {
|
|
5950
|
+
return resolveTargetModel(displayModels(current(), region), targetModelPreference(poolPreferencesOf(current(), region), accountId)).modelId;
|
|
5633
5951
|
};
|
|
5634
5952
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
5635
5953
|
const poolPreferencesOf = (config, region) => {
|
|
@@ -5638,6 +5956,7 @@ function apply(ctx, config) {
|
|
|
5638
5956
|
return {
|
|
5639
5957
|
enabled: pool?.enabled === true,
|
|
5640
5958
|
targetModelId: pool?.targetModelId ?? "",
|
|
5959
|
+
targetModelIdByAccount: { ...pool?.targetModelIdByAccount },
|
|
5641
5960
|
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
5642
5961
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
5643
5962
|
};
|
|
@@ -5812,4 +6131,4 @@ function apply(ctx, config) {
|
|
|
5812
6131
|
});
|
|
5813
6132
|
}
|
|
5814
6133
|
//#endregion
|
|
5815
|
-
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
6134
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, modelIdOfBody, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "4.4.
|
|
5
|
+
"version": "4.4.6",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|