dsh-connect-workbuddy 5.0.1 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +208 -7
- package/lib/{host-heartbeat-DtMzOwZr.js → host-heartbeat-Icz67WtG.js} +55 -2
- package/lib/index.d.ts +54 -1
- package/lib/index.js +394 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.2.1 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **每一次成功请求前都要先浪费一次被拒请求(自查发现)** —— 重试阶梯是「先发原文→被 11128 拒→再发扰动版」。第一次这样是对的,但**之后每一次都重复这个失败**,而一个会话里提示词的形状并不变。实测每次多付 **209ms + 一次被拒请求**。
|
|
8
|
+
- 修法:扰动版成功过一次之后,记住它,**下一次直接从扰动版开始**。实测:第 1 次请求用 2 次上游调用,之后每次 1 次。
|
|
9
|
+
- **只记在内存里,不落盘**:上游的指纹名单是可以**撤销**的,不只是增加。落盘的标志会活过它成立的条件,继续扰动一个已经不再拒绝的网关。
|
|
10
|
+
- **而且会自我纠正**:扰动版一旦也被拒,立刻清掉标志、退回原文重新探测 —— 所以规则被撤销、或同一网关后面换了别的模型,都不会让插件卡在一个不再有用的形状上。
|
|
11
|
+
- 只在**成功**时记忆。一次也被拒的扰动没有教会我们任何关于网关想要什么的信息。
|
|
12
|
+
|
|
13
|
+
### 关于用户反馈的那份「改提示词」方案
|
|
14
|
+
|
|
15
|
+
有用户报告用 `dsm-prompt-easymanager` 取消勾选 `harness:identity` 并把它改成 `DSH` 就能解决。**我实测了,结论是部分有效、但不是根本解法**:
|
|
16
|
+
|
|
17
|
+
| 做法 | 实测 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 只取消勾选 `harness:identity` | **400 / 11128**(不够) |
|
|
20
|
+
| 只把 `identity` 改成 `DSH`,另两段原文 | **400 / 11128**(不够) |
|
|
21
|
+
| 只改 `source` + `web`,`identity` 原文 | **400 / 11128**(不够) |
|
|
22
|
+
| **三段全改成 `DSH`** | **200** ✅ |
|
|
23
|
+
| 插件当前(零宽扰动) | **200** ✅ |
|
|
24
|
+
|
|
25
|
+
也就是说**必须三段全部改**才有效,只改一段不够 —— 这份方案之所以在那个用户的机器上「可用」,是因为他改了三段。而插件的扰动方案不需要用户改任何东西。
|
|
26
|
+
|
|
27
|
+
顺带定位到了一处更精确的事实:门 B 的触发条件不是「含 `DeepSeek Harness`」,而是**特定句子组合**。单独 `source` 段 200、单独 `web` 首句 200、单独 `web` 次句 200,但**两段一起**就 400;而 `web` 次句末尾的**句号**决定成败(改成感叹号则 200)。所以这是一条**内容指纹**规则,不是长度规则 —— 也再次确认 #105 里那个「需要 15,196 字符」的结论不成立。
|
|
28
|
+
|
|
29
|
+
## 5.2.0 (2026-10-22)
|
|
30
|
+
|
|
31
|
+
> 新功能:实测输出速度(issue #92)。默认不跑,点了才花积分。
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
- **一键测速 / 单模型测速:输出速度(issue #92)** —— 倍率不再是选模型的唯一依据了。DSH 会把模型当**子代理并行派发**,每个子任务还有多轮往返,所以**输出速度会乘进整个任务的实际耗时**:一个便宜但慢的模型,总耗时可能超过贵但快的。issue 报告者实测同倍率(x0.08)的四个模型,中位输出速度差 **3.8 倍**。
|
|
36
|
+
- **已有探针为什么给不出这个数**:现有可用性探测发 25,000 输入 token、输出上限 **1 个**(`PROBE_MAX_TOKENS = 1`),它的 `elapsedMs` 是**首字延迟**,没有生成跨度可除。测速需要相反的请求形状:极小输入 + 长输出。所以这是**独立的路由**,不是给老路由加字段。
|
|
37
|
+
- **成本比可用性探测低一个数量级**:这个网关按**输入**计价。可用性探测实测 `credit 0.72`;测速实测 **0.01–0.15**(取决于模型倍率)。每次 3 轮,也就是单模型约 0.03–0.45。
|
|
38
|
+
- **3 次取中位数,不是均值**:实测 `glm-5.3-flash` 五次是 110 / 105 / **177** / 103 / 109 tok/s —— 均值 121 被离群值拉高,而四次都在 103–110。中位数 109 才是它真实的位置。所以轮数取**奇数**(偶数中位数要平均两个中间值,等于把离群值的影响又请回来)。
|
|
39
|
+
- **失败的轮次丢弃,不记 0**:一个模型两次跑到 400 tok/s,不会因为第三次超时就变慢;差距体现在 `samples`("3 次"变"2 次")而不是速率上。
|
|
40
|
+
- **测不出就报测不出,绝不编**:分块太少(整个答案一次到达)→ 报「无法测速:分块太少」;上游没报 token 数 → 报「上游没报 token 数」。绝不拿 `elapsedMs` 倒推一个速率。
|
|
41
|
+
- **结果带时间戳**:速度随负载变,一个过期的数字比没有更糟。行里显示 `· 10-22 02:31 测`。
|
|
42
|
+
- **不影响可用性判定**:独立状态、独立路由、行里独立一行显示 —— 一个「快但不可用」的模型不会被渲染成一个不存在的合成状态。
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
|
|
46
|
+
- **测速的首字延迟一开始是错的(自查发现,未发布)** —— 第一版在 `probeChat` resolve 之后才取时钟,也就是「响应头到达」之后才计时,于是报的是「响应头→首块」而不是「发请求→首块」。实测三个模型里两个报 `timeToFirstTokenMs: 0`,正是这个错误的签名:读时钟时首块已经在缓冲区里了。
|
|
47
|
+
- **速率不受影响**(它是首块→末块),这恰恰是为什么容易漏掉:主数字一直是对的,副数字是垃圾。
|
|
48
|
+
- 已修,并补了一条测试:模拟服务端 120ms 才开始应答,断言 TTFT ≥ 100ms。时钟一旦被挪回响应头之后,这条会立刻红。
|
|
49
|
+
|
|
50
|
+
### Tests
|
|
51
|
+
|
|
52
|
+
- `tests/tps.spec.ts`(21 条,新增):请求形状(输出上限远大于 1、输入极小、要求低推理档)、流式读取真的在测间隔(已知到达节奏下断言跨度)、只计 content 不计 reasoning、畸形/分片帧不抛、单块流报「无法测速」、中位数抗离群、丢失败轮不记 0、区分「无 token 数」与「分块太少」、积分求和去浮点噪声、真实循环驱动、单轮失败仍继续、全失败不抛、帧语法单测、以及**TTFT 时钟位置**的回归测试。
|
|
53
|
+
- `tests/web-status.spec.ts`:路由清单断言补上测速路由。
|
|
54
|
+
- 全量:**109 文件 / 1794 条全绿**;两个 tsconfig 均 exit 0;build 成功。
|
|
55
|
+
|
|
56
|
+
## 5.1.0 (2026-10-22)
|
|
57
|
+
|
|
58
|
+
> 版本号是 **minor**:修了两个行为缺陷(不是接口变更),但其中第一个会改变每一轮请求实际发出去的内容,所以不用 patch。
|
|
59
|
+
|
|
60
|
+
### Fixes
|
|
61
|
+
|
|
62
|
+
- **global 区的历史思考不再被降级成正文(issue #102)** —— 这是本仓库最严重的一个 bug,而且它被一条假绿的测试盖了四个版本。pi-ai 只在「记这条消息的 provider / api / model 三者都等于当前请求」时才把历史 thinking 重放成 thinking,否则整块摊平成 `text`。DSH 的 agent loop 写消息时用的是**路由名**(`source.provider = request.provider` → `workbuddy-global`),而插件给模型盖的章是**共享 identity**(`workbuddy`),两者永远不等。后果:global 会话**每一轮**都把模型自己的历史推理翻成正文,模型看到「我以前的思考全是正文」,于是把新推理也写进正文——这就是报告里「reasoning 只有 1 个字、text 里全是工作笔记」的形状。
|
|
63
|
+
- 国内区之所以正常,只是因为它**恰好**路由名 == 章名(`workbuddy` === `workbuddy`)。
|
|
64
|
+
- 修法:新增 `alignAssistantIdentities()`,在 transcript 交给 pi-ai 之前把历史 assistant 消息的 `provider` 改写成 identity。**只改 `provider`,不动 `api`**——`dsh-foreign` 消息的 api 是插件从不会发的形状,声称 `openai-completions` 等于断言一个它从未按之构建的线上格式;而 `isSameModel` 仍然要求 model id 匹配,所以真正的换模型照样降级(有测试钉住)。
|
|
65
|
+
- **测试先改红再改绿**:`tests/issue59-cross-region-thinking.spec.ts` 里那条「双向」用例原本是 `for (...) { void recorded; void replayed; ... }`——两个循环变量被直接丢弃,两次迭代断言的是同一件事,从未测过跨区域。现在按**真实形状**喂(`source.provider` = 路由名 + 请求侧 = identity),修复前必红。
|
|
66
|
+
- 新增 5 条边界用例:user / toolResult 不动(原对象返回,请求路径上不分配)、已对齐时不分配、只改 provider、不同 model id 仍降级、畸形条目不抛。
|
|
67
|
+
|
|
68
|
+
- **`11115` 让宿主认不出「上下文溢出」,丢掉了自愈能力(issue #101)** —— 上游这个码有两种报文:形态 A 带 `prompt is too long: N tokens > M maximum`,形态 B 只有一句中文「本次输入加上历史对话超过了模型上限」。而宿主是靠**英文正则**(25 条 pattern,第一条就是 `prompt (?:is )?too long`)判断要不要跑压缩救援的——中文一句都不命中。于是形态 B 既不压缩也不重试,直接把 400 甩给用户,连它自己那句「请精简内容」的建议宿主都无法执行。
|
|
69
|
+
- 修法:错误文案前面加一句 `prompt is too long (upstream code 11115)`,上游原文紧跟在后面。机器拿到信号,人仍然看到网关自己的建议。
|
|
70
|
+
- **不编造 token 数字**:形态 B 根本没给数字,写一个「N tokens > M」正是这个仓库一直在避免的那种「自信的错误诊断」。
|
|
71
|
+
- `isContextTooLongBody()` 用带边界的正则,`111150` / `1111` 不会被误判成溢出——误判会让宿主跑一轮毫无用处的压缩。
|
|
72
|
+
- 分类不变:仍是 `client`,仍然不换号(溢出是请求的问题,换账号只会每个账号烧一次)。
|
|
73
|
+
|
|
74
|
+
### Tests
|
|
75
|
+
|
|
76
|
+
- `tests/issue101-context-overflow.spec.ts`(8 条,新增):两种形态都识别、相邻码不误伤、marker 命中宿主 pattern 且不被误认为限流、不编造数字、分类不变;外加一条**端到端**用例真的把一次 11115 请求驱过 shim,读宿主会看到的那句文案,确认 marker 在、网关原文在、requestId 在。
|
|
77
|
+
- `tests/issue59-cross-region-thinking.spec.ts`(12 条,改写 1 条 + 新增 5 条)。
|
|
78
|
+
- 全量 `npx vitest run` = **108 文件 / 1773 测试全绿**;`tsc` 两个 project 均 exit 0;`npm run build` 成功。
|
|
79
|
+
|
|
3
80
|
## 5.0.1 (2026-10-22)
|
|
4
81
|
|
|
5
82
|
> 版本号是 **patch**:改卡面上的一个字符串(标题)、去掉模型列表的一条 CSS 规则与两行路由项,以及几处描述它们的文档。没有接口或配置结构的变更——但如果你的配置里存着 `auto` / `default-model`,升级后它不再出现在列表里。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthCandidates,
|
|
2
|
+
import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-Icz67WtG.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
|
@@ -62,6 +62,17 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
*/
|
|
63
63
|
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
64
64
|
/**
|
|
65
|
+
* Plugin-owned OUTPUT-SPEED measurement endpoint (issue #92).
|
|
66
|
+
*
|
|
67
|
+
* Separate from {@link WORKBUDDY_PROBE_PATH} because the two ask opposite
|
|
68
|
+
* questions and cost different amounts: the availability probe sends 25,000
|
|
69
|
+
* input tokens and caps output at one (measured `credit 0.72`), while this one
|
|
70
|
+
* sends ~35 input tokens and caps output at 256 (measured `credit 0.01`–`0.15`).
|
|
71
|
+
* Sharing the route would mean sharing its one-model-per-request cap and its
|
|
72
|
+
* cost wording, and the two would then be wrong for each other.
|
|
73
|
+
*/
|
|
74
|
+
const WORKBUDDY_TPS_PATH = "/plugins/dsh-connect-workbuddy/tps";
|
|
75
|
+
/**
|
|
65
76
|
* Plugin-owned account-pool endpoint: one batch action per call.
|
|
66
77
|
*
|
|
67
78
|
* `?action=checkin` claims the daily reward for every account of a region;
|
|
@@ -4294,10 +4305,12 @@ window.__ModuleLoader__.load({
|
|
|
4294
4305
|
meta line, because a cooldown sentence ("rate limited - the upstream gave no
|
|
4295
4306
|
time") does not fit next to the context/output chips, and truncating it would
|
|
4296
4307
|
hide exactly the part the user needs. */
|
|
4297
|
-
/*
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4308
|
+
/* FOUR buttons now live here (refresh + two availability sweeps + the speed
|
|
4309
|
+
sweep added for issue #92), and the labels are long in Chinese
|
|
4310
|
+
(一键测速启用模型). flex:none alone let the group run past the header on a
|
|
4311
|
+
narrow card, so the group may WRAP and each label refuses to break mid-word —
|
|
4312
|
+
the same failure the pool row's action buttons had. The wrap is what makes a
|
|
4313
|
+
fourth button safe; without it the header would need a scroll. */
|
|
4301
4314
|
.dsm-workbuddy-models-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
4302
4315
|
.dsm-workbuddy-models-head-actions .dsm-btn{white-space:nowrap}
|
|
4303
4316
|
/* The sweep buttons keep the outline weight of the refresh button beside them, so
|
|
@@ -5215,6 +5228,37 @@ window.__ModuleLoader__.load({
|
|
|
5215
5228
|
const remaining = remainingText(t, untilMs, nowMs);
|
|
5216
5229
|
return remaining === "" ? "" : ` · ${remaining}`;
|
|
5217
5230
|
}
|
|
5231
|
+
/**
|
|
5232
|
+
* Render one measured speed, or why there is none (issue #92).
|
|
5233
|
+
*
|
|
5234
|
+
* The timestamp is PART of the value, not decoration: speed changes with load,
|
|
5235
|
+
* and a number without its moment is indistinguishable from a fresh one. When the
|
|
5236
|
+
* measurement could not be timed the row says so and names the reason — never a
|
|
5237
|
+
* zero, and never a guess extrapolated from `elapsedMs`, which for some runs is
|
|
5238
|
+
* time-to-first-token and for others a whole-request duration.
|
|
5239
|
+
*/
|
|
5240
|
+
function tpsResultView(result, t, measuredAtMs) {
|
|
5241
|
+
const stamp = measuredAtMs === void 0 ? "" : ` · ${t("row.tpsMeasuredAt", { at: formatDate(measuredAtMs) })}`;
|
|
5242
|
+
if (result.tokensPerSecond === void 0) {
|
|
5243
|
+
const reason = result.reason === "few-chunks" ? "row.tpsNoSample" : result.reason === "no-usage" ? "row.tpsNoUsage" : "row.tpsAllFailed";
|
|
5244
|
+
return {
|
|
5245
|
+
text: result.samples > 0 && result.reason !== void 0 ? `${t(reason)}${stamp}` : `${t("row.tpsAllFailed")}${stamp}`,
|
|
5246
|
+
tone: "bad"
|
|
5247
|
+
};
|
|
5248
|
+
}
|
|
5249
|
+
return {
|
|
5250
|
+
text: `${result.samples < TPS_EXPECTED_RUNS ? t("row.tpsPartial", {
|
|
5251
|
+
tps: String(result.tokensPerSecond),
|
|
5252
|
+
samples: String(result.samples)
|
|
5253
|
+
}) : result.timeToFirstTokenMs === void 0 ? t("row.tpsValue", { tps: String(result.tokensPerSecond) }) : t("row.tpsValueTtft", {
|
|
5254
|
+
tps: String(result.tokensPerSecond),
|
|
5255
|
+
ttft: (result.timeToFirstTokenMs / 1e3).toFixed(1)
|
|
5256
|
+
})}${result.credit === void 0 ? "" : ` · ${t("row.tpsCredit", { credit: String(result.credit) })}`}${stamp}`,
|
|
5257
|
+
tone: result.samples < TPS_EXPECTED_RUNS ? "warn" : "ok"
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
5260
|
+
/** Runs a complete measurement takes; below this the row says "n runs". */
|
|
5261
|
+
const TPS_EXPECTED_RUNS = 3;
|
|
5218
5262
|
function probeResultView(result, t, nowMs = Date.now()) {
|
|
5219
5263
|
switch (result.outcome) {
|
|
5220
5264
|
case "ok": {
|
|
@@ -5347,6 +5391,26 @@ window.__ModuleLoader__.load({
|
|
|
5347
5391
|
const probesRef = (0, react.useRef)({});
|
|
5348
5392
|
/** Progress of an in-flight sweep, or undefined when none is running. */
|
|
5349
5393
|
const [sweeping, setSweeping] = (0, react.useState)(void 0);
|
|
5394
|
+
/**
|
|
5395
|
+
* Measured output speed per model (issue #92), keyed by model id.
|
|
5396
|
+
*
|
|
5397
|
+
* Kept in its own map, NOT merged into `probes`: a speed run says nothing about
|
|
5398
|
+
* availability and an availability run says nothing about speed, so one map
|
|
5399
|
+
* holding both would let a fresh speed reading overwrite a stale availability
|
|
5400
|
+
* verdict (or the reverse) with no field to tell them apart.
|
|
5401
|
+
*
|
|
5402
|
+
* Deliberately survives a region switch's re-render but is NOT persisted: a
|
|
5403
|
+
* speed number is only meaningful with the moment it was taken, and a stored
|
|
5404
|
+
* one would be indistinguishable from a fresh measurement. The row prints the
|
|
5405
|
+
* time beside it for the same reason.
|
|
5406
|
+
*/
|
|
5407
|
+
const [tpsResults, setTpsResults] = (0, react.useState)({});
|
|
5408
|
+
/** Which speed runs are in flight, so the row and the sweep button can spin. */
|
|
5409
|
+
const [tpsProbing, setTpsProbing] = (0, react.useState)({});
|
|
5410
|
+
/** Progress of a speed sweep, so the button can show "n/m" instead of hanging. */
|
|
5411
|
+
const [tpsSweeping, setTpsSweeping] = (0, react.useState)(void 0);
|
|
5412
|
+
/** When each model's speed was measured, in ms; printed beside the number. */
|
|
5413
|
+
const [tpsMeasuredAt, setTpsMeasuredAt] = (0, react.useState)({});
|
|
5350
5414
|
/** The last sweep's tally, cleared when the next one starts. */
|
|
5351
5415
|
const [sweepSummary, setSweepSummary] = (0, react.useState)(void 0);
|
|
5352
5416
|
const mounted = (0, react.useRef)(true);
|
|
@@ -6008,6 +6072,83 @@ window.__ModuleLoader__.load({
|
|
|
6008
6072
|
sweepModels(visibleModels.filter((model) => activeEnabledIds.has(model.id)).map((model) => model.id));
|
|
6009
6073
|
};
|
|
6010
6074
|
/**
|
|
6075
|
+
* Measure OUTPUT SPEED for one or more models (issue #92).
|
|
6076
|
+
*
|
|
6077
|
+
* One model per request, like every other spending route: the upstream route
|
|
6078
|
+
* refuses more than one id, and that cap is what keeps a stale bundle from
|
|
6079
|
+
* turning a click into an unaccounted spend over a whole roster. The loop for a
|
|
6080
|
+
* sweep therefore lives here, not on the Host.
|
|
6081
|
+
*
|
|
6082
|
+
* A failed model does NOT abort the sweep. Unlike an availability sweep — where
|
|
6083
|
+
* a rejected credential answers identically for every remaining model and
|
|
6084
|
+
* continuing only spends credit to hear the same answer — a speed failure is
|
|
6085
|
+
* specific to that model, so stopping would hide the one thing the sweep was
|
|
6086
|
+
* asked to find out.
|
|
6087
|
+
*/
|
|
6088
|
+
const measureTpsFor = async (modelIds) => {
|
|
6089
|
+
if (modelIds.length === 0) return;
|
|
6090
|
+
setTpsProbing((previous) => {
|
|
6091
|
+
const next = { ...previous };
|
|
6092
|
+
for (const id of modelIds) next[id] = true;
|
|
6093
|
+
return next;
|
|
6094
|
+
});
|
|
6095
|
+
try {
|
|
6096
|
+
for (const [index, modelId] of modelIds.entries()) {
|
|
6097
|
+
if (!mounted.current) return;
|
|
6098
|
+
try {
|
|
6099
|
+
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_TPS_PATH, activeRegion), {
|
|
6100
|
+
method: "POST",
|
|
6101
|
+
headers: {
|
|
6102
|
+
"content-type": "application/json",
|
|
6103
|
+
accept: "application/json"
|
|
6104
|
+
},
|
|
6105
|
+
credentials: "same-origin",
|
|
6106
|
+
body: JSON.stringify({ modelIds: [modelId] })
|
|
6107
|
+
});
|
|
6108
|
+
const body = await response.json().catch(() => void 0);
|
|
6109
|
+
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
6110
|
+
if (!mounted.current) return;
|
|
6111
|
+
const landed = body.results[0];
|
|
6112
|
+
if (landed !== void 0) {
|
|
6113
|
+
setTpsResults((previous) => ({
|
|
6114
|
+
...previous,
|
|
6115
|
+
[landed.modelId]: landed
|
|
6116
|
+
}));
|
|
6117
|
+
setTpsMeasuredAt((previous) => ({
|
|
6118
|
+
...previous,
|
|
6119
|
+
[landed.modelId]: Date.now()
|
|
6120
|
+
}));
|
|
6121
|
+
}
|
|
6122
|
+
} catch (error) {
|
|
6123
|
+
if (!mounted.current) return;
|
|
6124
|
+
setProbeError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
6125
|
+
} finally {
|
|
6126
|
+
if (mounted.current) setTpsProbing((previous) => {
|
|
6127
|
+
const next = { ...previous };
|
|
6128
|
+
delete next[modelId];
|
|
6129
|
+
return next;
|
|
6130
|
+
});
|
|
6131
|
+
}
|
|
6132
|
+
if (mounted.current) setTpsSweeping({
|
|
6133
|
+
done: index + 1,
|
|
6134
|
+
total: modelIds.length
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
6137
|
+
} finally {
|
|
6138
|
+
if (mounted.current) setTpsSweeping(void 0);
|
|
6139
|
+
}
|
|
6140
|
+
};
|
|
6141
|
+
/**
|
|
6142
|
+
* Sweep speed for the models this region has ENABLED.
|
|
6143
|
+
*
|
|
6144
|
+
* Enabled-only rather than the whole catalog: a speed run is three real
|
|
6145
|
+
* requests, so covering a 20-model roster would cost 60 requests to learn
|
|
6146
|
+
* something about 18 models the user does not use.
|
|
6147
|
+
*/
|
|
6148
|
+
const sweepTpsEnabledModels = () => {
|
|
6149
|
+
measureTpsFor(visibleModels.filter((model) => activeEnabledIds.has(model.id)).map((model) => model.id));
|
|
6150
|
+
};
|
|
6151
|
+
/**
|
|
6011
6152
|
* Sweep EVERY model in the region's live catalog, enabled or not.
|
|
6012
6153
|
*
|
|
6013
6154
|
* Deliberately the whole VISIBLE catalog and nothing else: those are the ids
|
|
@@ -6469,6 +6610,17 @@ window.__ModuleLoader__.load({
|
|
|
6469
6610
|
done: sweeping.done,
|
|
6470
6611
|
total: sweeping.total
|
|
6471
6612
|
})
|
|
6613
|
+
}),
|
|
6614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6615
|
+
type: "button",
|
|
6616
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
|
|
6617
|
+
disabled: busy || tpsSweeping !== void 0 || tpsProbing !== void 0 || activeEnabledIds.size === 0,
|
|
6618
|
+
title: t("row.tpsSweepEnabledHint"),
|
|
6619
|
+
onClick: sweepTpsEnabledModels,
|
|
6620
|
+
children: tpsSweeping === void 0 ? t("row.tpsSweepEnabled") : t("row.tpsTesting", {
|
|
6621
|
+
done: tpsSweeping.done,
|
|
6622
|
+
total: tpsSweeping.total
|
|
6623
|
+
})
|
|
6472
6624
|
})
|
|
6473
6625
|
]
|
|
6474
6626
|
})]
|
|
@@ -6579,9 +6731,9 @@ window.__ModuleLoader__.load({
|
|
|
6579
6731
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) })] })
|
|
6580
6732
|
]
|
|
6581
6733
|
}),
|
|
6582
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
6734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6583
6735
|
className: "dsm-workbuddy-model-actions-buttons",
|
|
6584
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6736
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6585
6737
|
type: "button",
|
|
6586
6738
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-model-probe",
|
|
6587
6739
|
disabled: probing[model.id] === true || status.status !== "signed-in",
|
|
@@ -6590,7 +6742,16 @@ window.__ModuleLoader__.load({
|
|
|
6590
6742
|
probeModels([model.id]);
|
|
6591
6743
|
},
|
|
6592
6744
|
children: probing[model.id] === true ? t("row.probing") : t("row.probe")
|
|
6593
|
-
})
|
|
6745
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6746
|
+
type: "button",
|
|
6747
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-model-probe",
|
|
6748
|
+
disabled: tpsProbing[model.id] === true || status.status !== "signed-in",
|
|
6749
|
+
title: t("row.tpsHint"),
|
|
6750
|
+
onClick: () => {
|
|
6751
|
+
measureTpsFor([model.id]);
|
|
6752
|
+
},
|
|
6753
|
+
children: tpsProbing[model.id] === true ? t("row.tpsMeasuring") : t("row.tpsMeasure")
|
|
6754
|
+
})]
|
|
6594
6755
|
})
|
|
6595
6756
|
]
|
|
6596
6757
|
}),
|
|
@@ -6616,6 +6777,16 @@ window.__ModuleLoader__.load({
|
|
|
6616
6777
|
...result.message === void 0 ? {} : { title: result.message },
|
|
6617
6778
|
children: [view.text, reason === void 0 ? null : " · " + reason]
|
|
6618
6779
|
});
|
|
6780
|
+
})(),
|
|
6781
|
+
(() => {
|
|
6782
|
+
const measured = tpsResults[model.id];
|
|
6783
|
+
if (measured === void 0) return null;
|
|
6784
|
+
const view = tpsResultView(measured, t, tpsMeasuredAt[model.id]);
|
|
6785
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6786
|
+
className: `dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-${view.tone}`,
|
|
6787
|
+
role: "status",
|
|
6788
|
+
children: view.text
|
|
6789
|
+
});
|
|
6619
6790
|
})()
|
|
6620
6791
|
]
|
|
6621
6792
|
}, model.id))
|
|
@@ -7885,6 +8056,21 @@ window.__ModuleLoader__.load({
|
|
|
7885
8056
|
"row.modelsTesting": "Testing {done}/{total}…",
|
|
7886
8057
|
"row.modelsTestEnabledHint": "Tests every model you have ENABLED, one request each — each probe sends a real-volume request, so this spends real credit (about 0.01 per model at the default 25000-token size). Rows show their verdict as it lands. Stops early if the credential is rejected.",
|
|
7887
8058
|
"row.modelsTestAllHint": "Tests EVERY model in this region's catalog, including the ones you have NOT enabled — so it costs noticeably more than \"test enabled models\". Same per-probe cost, same early stop on a rejected credential.",
|
|
8059
|
+
"row.tpsSweepEnabled": "Measure speed (enabled)",
|
|
8060
|
+
"row.tpsSweepAll": "Measure speed (all)",
|
|
8061
|
+
"row.tpsSweepEnabledHint": "Measures OUTPUT SPEED for every model you have ENABLED: three runs each, taking the median. Unlike the availability probes above, this sends a tiny prompt and asks for 256 output tokens back, so it costs far less per model (measured 0.01–0.15 credit against 0.72), because this gateway prices by input. Speed matters because DSH dispatches models as parallel sub-agents, so a slow model costs wall-clock time across the whole run.",
|
|
8062
|
+
"row.tpsTesting": "Measuring {done}/{total}…",
|
|
8063
|
+
"row.tpsMeasure": "Speed",
|
|
8064
|
+
"row.tpsMeasuring": "Measuring…",
|
|
8065
|
+
"row.tpsHint": "Measure this model's output speed: three short requests, median taken. Uses about 256 output tokens per run and costs far less than an availability probe (measured 0.01–0.15 credit). The result is shown with the time it was taken, because speed changes with load and an old number is worse than none.",
|
|
8066
|
+
"row.tpsValue": "{tps} tok/s",
|
|
8067
|
+
"row.tpsValueTtft": "{tps} tok/s · first token {ttft}s",
|
|
8068
|
+
"row.tpsPartial": "{tps} tok/s ({samples} runs)",
|
|
8069
|
+
"row.tpsNoSample": "Could not time this model (too few stream chunks)",
|
|
8070
|
+
"row.tpsNoUsage": "Could not time this model (upstream reported no token count)",
|
|
8071
|
+
"row.tpsAllFailed": "Speed measurement failed",
|
|
8072
|
+
"row.tpsCredit": "cost {credit} credit",
|
|
8073
|
+
"row.tpsMeasuredAt": "measured {at}",
|
|
7888
8074
|
"row.modelsTestDone": "Done: {available} of {total} available",
|
|
7889
8075
|
"row.modelsTestAuth": "Stopped: the credential was rejected, so every remaining model would answer the same way. Sign in again, then re-run.",
|
|
7890
8076
|
"row.probe": "Test",
|
|
@@ -8229,6 +8415,21 @@ window.__ModuleLoader__.load({
|
|
|
8229
8415
|
"row.modelsTesting": "测试中 {done}/{total}…",
|
|
8230
8416
|
"row.modelsTestEnabledHint": "逐个测试你**已启用**的模型,一个模型一次请求 —— 每次探测都发真实体积的请求,真花积分(默认 25000 token 体积下约 0.01/个)。每行的结果一出来就显示。凭据被拒时提前停止。",
|
|
8231
8417
|
"row.modelsTestAllHint": "测试本区域目录里的**每一个**模型,含**未启用**的 —— 所以比「仅测试启用模型」明显更贵。单次探测成本相同,凭据被拒时同样提前停止。",
|
|
8418
|
+
"row.tpsSweepEnabled": "一键测速启用模型",
|
|
8419
|
+
"row.tpsSweepAll": "一键测速所有模型",
|
|
8420
|
+
"row.tpsSweepEnabledHint": "测**已启用**模型的**输出速度**:每个模型跑 3 次取中位数。和上面的可用性探测不同,它只发一个极小的提示、要求返回 256 个输出 token —— 这个网关按**输入**计价,所以单模型成本低得多(实测 0.01–0.15 积分,可用性探测是 0.72)。速度之所以重要:DSH 会把模型当子代理并行派发,慢模型拖的是整个任务的实际耗时。",
|
|
8421
|
+
"row.tpsTesting": "测速中 {done}/{total}…",
|
|
8422
|
+
"row.tpsMeasure": "测速",
|
|
8423
|
+
"row.tpsMeasuring": "测速中…",
|
|
8424
|
+
"row.tpsHint": "测量该模型的输出速度:3 次短请求取中位数。每次约 256 个输出 token,成本远低于可用性探测(实测 0.01–0.15 积分)。结果旁边会标出**测量时间** —— 速度随负载变化,一个过期的数字比没有更糟。",
|
|
8425
|
+
"row.tpsValue": "{tps} tok/s",
|
|
8426
|
+
"row.tpsValueTtft": "{tps} tok/s · 首字 {ttft}s",
|
|
8427
|
+
"row.tpsPartial": "{tps} tok/s({samples} 次)",
|
|
8428
|
+
"row.tpsNoSample": "无法测速:流里的分块太少,算不出速率",
|
|
8429
|
+
"row.tpsNoUsage": "无法测速:上游没报 token 数",
|
|
8430
|
+
"row.tpsAllFailed": "测速失败",
|
|
8431
|
+
"row.tpsCredit": "花费 {credit} 积分",
|
|
8432
|
+
"row.tpsMeasuredAt": "{at} 测",
|
|
8232
8433
|
"row.modelsTestDone": "完成:{total} 个里 {available} 个可用",
|
|
8233
8434
|
"row.modelsTestAuth": "已停止:凭据被拒,剩下的模型都会得到同一个答案。请重新登录后再跑一次。",
|
|
8234
8435
|
"row.probe": "测试",
|
|
@@ -1997,6 +1997,59 @@ const MODEL_BUSY_CODE = "\"code\":14003";
|
|
|
1997
1997
|
*/
|
|
1998
1998
|
const CHANNEL_REJECT_CODE = "\"code\":11128";
|
|
1999
1999
|
/**
|
|
2000
|
+
* Business code for "the request exceeded the model's context window" (issue #101).
|
|
2001
|
+
*
|
|
2002
|
+
* The gateway answers `11115` in TWO shapes, and only one of them is
|
|
2003
|
+
* self-describing:
|
|
2004
|
+
*
|
|
2005
|
+
* A. `prompt is too long: 100001 tokens > 100000 maximum` — carries numbers;
|
|
2006
|
+
* B. the localized sentence only (「本次输入加上历史对话超过了模型上限…」), no numbers.
|
|
2007
|
+
*
|
|
2008
|
+
* Shape B is the one that hurt. The HOST decides whether to run its
|
|
2009
|
+
* context-overflow rescue by matching the error text against a list of ENGLISH
|
|
2010
|
+
* patterns (`prompt (?:is )?too long`, `exceeds the context window`,
|
|
2011
|
+
* `context[_ ]length[_ ]exceeded`, …25 of them), and a Chinese sentence matches
|
|
2012
|
+
* none of them. So on shape B the host neither compacts nor retries: the user
|
|
2013
|
+
* gets a bare 400 whose own advice ("请精简内容") the host cannot act on.
|
|
2014
|
+
*
|
|
2015
|
+
* {@link contextOverflowMarker} is the fix: the plugin adds an English clause
|
|
2016
|
+
* the host recognises, and keeps the upstream's own sentence beside it.
|
|
2017
|
+
*/
|
|
2018
|
+
/**
|
|
2019
|
+
* The business code, in either of the two spellings the plugin ever holds.
|
|
2020
|
+
*
|
|
2021
|
+
* Two forms are in circulation and BOTH reach this predicate:
|
|
2022
|
+
* - the raw upstream body: `{"code":11115,...}`;
|
|
2023
|
+
* - the reply this plugin already composed, which renders it as
|
|
2024
|
+
* `(code 11115,requestId …)` — the form the retry path re-reads.
|
|
2025
|
+
*
|
|
2026
|
+
* The trailing `(?!\d)` matters: `111150` and `1111` both contain the digits
|
|
2027
|
+
* after a naive substring test, and misreading a DIFFERENT refusal as an
|
|
2028
|
+
* overflow would send the host into a compaction pass that cannot help.
|
|
2029
|
+
*/
|
|
2030
|
+
const CONTEXT_TOO_LONG_CODE = /"code"\s*:\s*11115(?!\d)|code\s+11115(?!\d)/u;
|
|
2031
|
+
/**
|
|
2032
|
+
* Whether this body is the context-length refusal, in EITHER of its shapes.
|
|
2033
|
+
*
|
|
2034
|
+
* Shape B carries no number and no English phrase, so the business code is the
|
|
2035
|
+
* only reliable handle — the same reasoning as {@link isChannelRejectBody}'s
|
|
2036
|
+
* code-plus-wording pair, except that here the wording is what VARIES.
|
|
2037
|
+
*/
|
|
2038
|
+
function isContextTooLongBody(body) {
|
|
2039
|
+
return CONTEXT_TOO_LONG_CODE.test(body);
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* An English clause the HOST's overflow detector recognises, for issue #101.
|
|
2043
|
+
*
|
|
2044
|
+
* `prompt is too long` is the first entry of the host's pattern list and the
|
|
2045
|
+
* phrasing Anthropic-compatible gateways already use, so it is the cheapest way
|
|
2046
|
+
* to say "this is an overflow" to a detector that only reads English. The
|
|
2047
|
+
* numbers are omitted deliberately: the upstream never gave them in shape B, and
|
|
2048
|
+
* inventing them would be the exact confident-wrong-diagnosis this plugin spends
|
|
2049
|
+
* its comments avoiding.
|
|
2050
|
+
*/
|
|
2051
|
+
const CONTEXT_OVERFLOW_MARKER = "prompt is too long (upstream code 11115)";
|
|
2052
|
+
/**
|
|
2000
2053
|
* Wording that identifies the CHANNEL half of `11128` (the other halves being
|
|
2001
2054
|
* `conversation id missing` and `first message is not system prompt`).
|
|
2002
2055
|
*
|
|
@@ -5799,7 +5852,7 @@ function generationOf(version) {
|
|
|
5799
5852
|
//#endregion
|
|
5800
5853
|
//#region src/version.ts
|
|
5801
5854
|
/** The npm package version this build was produced from. */
|
|
5802
|
-
const WORKBUDDY_CONNECT_VERSION = "5.
|
|
5855
|
+
const WORKBUDDY_CONNECT_VERSION = "5.2.1";
|
|
5803
5856
|
//#endregion
|
|
5804
5857
|
//#region src/host-heartbeat.ts
|
|
5805
5858
|
/**
|
|
@@ -5957,4 +6010,4 @@ async function clearHostHeartbeat() {
|
|
|
5957
6010
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
5958
6011
|
}
|
|
5959
6012
|
//#endregion
|
|
5960
|
-
export {
|
|
6013
|
+
export { regionOf as $, parseWorkBuddyAuth as A, workbuddyAppExecutableCandidates as At, forceNeutralHead as B, defaultDesktopAuthCandidates as C, findWorkbuddyAppExecutableWithSource as Ct, isEncryptedCredentialError as D, macosNestedAppBundles as Dt, hasEncryptedCredentialFields as E, macosBundleExecutable as Et, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as F, knownRegionOf as G, isConversationIdGateBody as H, WorkBuddyCredentialRejectedError as I, parseCreditMultiplier as J, modelIdOfBody as K, WorkBuddyUpstreamClient as L, workbuddyOwnAuthPath as M, resolveDshHome as Mt, CONTEXT_OVERFLOW_MARKER as N, withFileLock as Nt, isVaultAccountId as O, openEncryptedField as Ot, CREDENTIAL_REJECTED_CODE as P, writeFileAtomic as Pt, prepareChatBody as Q, classifyUpstreamError as R, authFileName as S, findWorkbuddyAppExecutable as St, defaultDesktopAuthPath as T, isWorkbuddyBundle as Tt, isCredentialRejectedError as U, isContextTooLongBody as V, isFirstMessageGateBody as W, parseUpstreamModel as X, parseReasoning as Y, perturbChannelFingerprints as Z, ENCRYPTED_CREDENTIAL_CODE as _, WORKBUDDY_APP_EXECUTABLE_ENV as _t, readHostHeartbeat as a, offDefaultFor as at, WorkBuddyCredentialStore as b, deriveAtRestKeyId as bt, WORKBUDDY_CONNECT_VERSION as c, withEffectiveOff as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, pickImageModel as dt, attachBusyLevels as et, WorkBuddyCatalog as f, parseQueueAnswer as ft, withoutRouters as g, nativeModalityOf as gt, fallbackModelsFor as h, imageDefaultFor as ht, processStartTimeMs as i, lastRefusalFor as it, workbuddyAccountId as j, hostPeersInfo as jt, legacyWorkbuddyOwnAuthPath as k, readAtRestKey as kt, piAiRuntimeInfo as l, DEFAULT_IMAGE_SIZE as lt, deriveCatalog as m, NATIVE_MODALITY_BY_MODEL_ID as mt, clearHostHeartbeat as n, REFUSES_OFF_MODEL_IDS as nt, workbuddyHostHeartbeatPath as o, offRefused as ot, applyContextBudgets as p, queueNoticeText as pt, openingEvidenceOfBody as q, isHeartbeatProcessAlive as r, defaultOffModelIds as rt, writeHostHeartbeat as s, recordRefusal as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, busyDisplaySuffix as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyImageError as ut, WORKBUDDY_AUTH_FILENAME as v, clearAtRestKeyCache as vt, defaultDesktopAuthDirs as w, isEncryptedFieldWrapper as wt, WorkBuddyEncryptedCredentialError as x, fetchAtRestKeyPayload as xt, WORKBUDDY_AUTH_FILE_ENV as y, deriveAtRestKey as yt, declaredTools as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -2770,6 +2770,17 @@ declare const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
|
2770
2770
|
* browser half.
|
|
2771
2771
|
*/
|
|
2772
2772
|
declare const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
2773
|
+
/**
|
|
2774
|
+
* Plugin-owned OUTPUT-SPEED measurement endpoint (issue #92).
|
|
2775
|
+
*
|
|
2776
|
+
* Separate from {@link WORKBUDDY_PROBE_PATH} because the two ask opposite
|
|
2777
|
+
* questions and cost different amounts: the availability probe sends 25,000
|
|
2778
|
+
* input tokens and caps output at one (measured `credit 0.72`), while this one
|
|
2779
|
+
* sends ~35 input tokens and caps output at 256 (measured `credit 0.01`–`0.15`).
|
|
2780
|
+
* Sharing the route would mean sharing its one-model-per-request cap and its
|
|
2781
|
+
* cost wording, and the two would then be wrong for each other.
|
|
2782
|
+
*/
|
|
2783
|
+
declare const WORKBUDDY_TPS_PATH = "/plugins/dsh-connect-workbuddy/tps";
|
|
2773
2784
|
/**
|
|
2774
2785
|
* One entry of the transfer PREVIEW: display fields only, never token
|
|
2775
2786
|
* material. `hasToken` is the import decision input — the import step skips
|
|
@@ -3265,6 +3276,33 @@ interface WorkBuddyWebProbeResult {
|
|
|
3265
3276
|
/** Which of those four sources supplied {@link retryAtMs}. */
|
|
3266
3277
|
retrySource?: 'retry-after' | 'upstream-message' | 'quota-refresh' | 'upstream-tips';
|
|
3267
3278
|
}
|
|
3279
|
+
/**
|
|
3280
|
+
* One model's output-speed result, as the card renders it (issue #92).
|
|
3281
|
+
*
|
|
3282
|
+
* Deliberately its own type rather than extra fields on
|
|
3283
|
+
* {@link WorkBuddyWebProbeResult}: the two runs answer different questions, and
|
|
3284
|
+
* a shared type would let a card render an availability verdict with a speed on
|
|
3285
|
+
* it, or vice versa, with nothing to stop it.
|
|
3286
|
+
*/
|
|
3287
|
+
interface WorkBuddyWebTpsResult {
|
|
3288
|
+
modelId: string;
|
|
3289
|
+
/** `ok` when at least one run answered; the speed fields say what was learned. */
|
|
3290
|
+
outcome: 'ok' | 'unavailable' | 'credential-rejected' | 'failed';
|
|
3291
|
+
/** Median output tokens/second over the usable runs. */
|
|
3292
|
+
tokensPerSecond?: number;
|
|
3293
|
+
/** Median time to first content chunk, in ms. */
|
|
3294
|
+
timeToFirstTokenMs?: number;
|
|
3295
|
+
/** Output tokens the upstream counted for the fastest usable run. */
|
|
3296
|
+
outputTokens?: number;
|
|
3297
|
+
/** How many runs contributed; below the planned count when runs were lost. */
|
|
3298
|
+
samples: number;
|
|
3299
|
+
/** Why {@link tokensPerSecond} is absent, when it is. */
|
|
3300
|
+
reason?: 'few-chunks' | 'no-usage' | 'all-failed';
|
|
3301
|
+
/** Total credit the runs reported, rounded to 2dp. */
|
|
3302
|
+
credit?: number;
|
|
3303
|
+
/** Upstream's own words when the run failed outright. */
|
|
3304
|
+
message?: string;
|
|
3305
|
+
}
|
|
3268
3306
|
/** Why one account is currently out of the pool. */
|
|
3269
3307
|
type WorkBuddyWebPoolExclusion = 'unusable' | 'rate-limited' |
|
|
3270
3308
|
/**
|
|
@@ -3605,6 +3643,21 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
3605
3643
|
probeModels?(region: WorkBuddyRegion, modelIds: readonly string[], options?: {
|
|
3606
3644
|
signal?: AbortSignal;
|
|
3607
3645
|
}): Promise<readonly WorkBuddyWebProbeResult[]>;
|
|
3646
|
+
/**
|
|
3647
|
+
* Measure OUTPUT SPEED for each named model of a region (issue #92).
|
|
3648
|
+
*
|
|
3649
|
+
* Separate from {@link probeModels} because it answers the opposite question
|
|
3650
|
+
* with the opposite request shape: tiny input, long output. The availability
|
|
3651
|
+
* probe's `max_tokens: 1` means its `elapsedMs` is time-to-first-token and
|
|
3652
|
+
* carries no rate information at all.
|
|
3653
|
+
*
|
|
3654
|
+
* Costs a fraction of a probe per model (measured `credit 0.01`–`0.15` against
|
|
3655
|
+
* `0.72`), because this gateway prices by INPUT. It still spends real credit,
|
|
3656
|
+
* so it stays one model per click like every other spending route.
|
|
3657
|
+
*/
|
|
3658
|
+
measureTps?(region: WorkBuddyRegion, modelIds: readonly string[], options?: {
|
|
3659
|
+
signal?: AbortSignal;
|
|
3660
|
+
}): Promise<readonly WorkBuddyWebTpsResult[]>;
|
|
3608
3661
|
/**
|
|
3609
3662
|
* The region's next monthly quota refresh, when the upstream declares one.
|
|
3610
3663
|
*
|
|
@@ -4263,4 +4316,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
4263
4316
|
*/
|
|
4264
4317
|
declare function apply(ctx: Context, config: Config): void;
|
|
4265
4318
|
//#endregion
|
|
4266
|
-
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 };
|
|
4319
|
+
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_TPS_PATH, 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,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as regionOf, A as parseWorkBuddyAuth, At as workbuddyAppExecutableCandidates, B as forceNeutralHead, C as defaultDesktopAuthCandidates, D as isEncryptedCredentialError, Dt as macosNestedAppBundles, E as hasEncryptedCredentialFields, Et as macosBundleExecutable, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, G as knownRegionOf, H as isConversationIdGateBody, I as WorkBuddyCredentialRejectedError, J as parseCreditMultiplier, K as modelIdOfBody, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as resolveDshHome, N as CONTEXT_OVERFLOW_MARKER, Nt as withFileLock, O as isVaultAccountId, Ot as openEncryptedField, P as CREDENTIAL_REJECTED_CODE, Pt as writeFileAtomic, Q as prepareChatBody, R as classifyUpstreamError, S as authFileName, St as findWorkbuddyAppExecutable, T as defaultDesktopAuthPath, Tt as isWorkbuddyBundle, U as isCredentialRejectedError, V as isContextTooLongBody, W as isFirstMessageGateBody, X as parseUpstreamModel, Y as parseReasoning, Z as perturbChannelFingerprints, _ as ENCRYPTED_CREDENTIAL_CODE, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, at as offDefaultFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKeyId, c as WORKBUDDY_CONNECT_VERSION, ct as withEffectiveOff, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as pickImageModel, et as attachBusyLevels, f as WorkBuddyCatalog, ft as parseQueueAnswer, g as withoutRouters, gt as nativeModalityOf, h as fallbackModelsFor, ht as imageDefaultFor, i as processStartTimeMs, it as lastRefusalFor, j as workbuddyAccountId, k as legacyWorkbuddyOwnAuthPath, kt as readAtRestKey, l as piAiRuntimeInfo, lt as DEFAULT_IMAGE_SIZE, m as deriveCatalog, mt as NATIVE_MODALITY_BY_MODEL_ID, n as clearHostHeartbeat, nt as REFUSES_OFF_MODEL_IDS, o as workbuddyHostHeartbeatPath, ot as offRefused, p as applyContextBudgets, pt as queueNoticeText, q as openingEvidenceOfBody, r as isHeartbeatProcessAlive, rt as defaultOffModelIds, s as writeHostHeartbeat, st as recordRefusal, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as busyDisplaySuffix, u as FALLBACK_WORKBUDDY_MODELS, ut as WorkBuddyImageError, v as WORKBUDDY_AUTH_FILENAME, vt as clearAtRestKeyCache, w as defaultDesktopAuthDirs, wt as isEncryptedFieldWrapper, x as WorkBuddyEncryptedCredentialError, xt as fetchAtRestKeyPayload, y as WORKBUDDY_AUTH_FILE_ENV, yt as deriveAtRestKey, z as declaredTools } from "./host-heartbeat-Icz67WtG.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -589,16 +589,80 @@ function foldModernContext(context) {
|
|
|
589
589
|
function transcript(context) {
|
|
590
590
|
return context;
|
|
591
591
|
}
|
|
592
|
-
|
|
592
|
+
/**
|
|
593
|
+
* Rewrite every historical assistant message to the identity this adapter stamps.
|
|
594
|
+
*
|
|
595
|
+
* ISSUE #102. pi-ai replays a thinking block as `thinking` only when the message
|
|
596
|
+
* that produced it names the same `provider` / `api` / `model` as the request in
|
|
597
|
+
* flight (`utils/transform-messages.js`, the `isSameModel` branch); everything
|
|
598
|
+
* else is flattened to `{type:'text', text: thinking}`.
|
|
599
|
+
*
|
|
600
|
+
* The durable transcript the HOST hands back does not carry the identity. The
|
|
601
|
+
* agent loop writes `source.provider = request.provider`, and `request.provider`
|
|
602
|
+
* is the ROUTE id (`workbuddy` / `workbuddy-global`), while this adapter stamps
|
|
603
|
+
* `provider: WORKBUDDY_MODEL_IDENTITY` on the model. So:
|
|
604
|
+
*
|
|
605
|
+
* - CN sessions survive by COINCIDENCE: its route id happens to equal the
|
|
606
|
+
* identity (`workbuddy` === `workbuddy`).
|
|
607
|
+
* - GLOBAL sessions never match (`workbuddy-global` !== `workbuddy`), so every
|
|
608
|
+
* historical thinking block is downgraded on EVERY turn — not only when the
|
|
609
|
+
* user switches regions, which is what #59's write-up assumed.
|
|
610
|
+
*
|
|
611
|
+
* The model then sees a context in which all of its own past reasoning was
|
|
612
|
+
* plain assistant text, and writes its new reasoning into `text` to match. That
|
|
613
|
+
* is the reported symptom: reasoning collapses to a character or two while the
|
|
614
|
+
* answer channel fills with working notes.
|
|
615
|
+
*
|
|
616
|
+
* WHY REWRITING IS SAFE. Every message here was produced by THIS plugin, in THIS
|
|
617
|
+
* region — the transcript never contains another provider's assistant turns
|
|
618
|
+
* (`dsh-llm` splits history by provider before the API sees it). The identity is
|
|
619
|
+
* already the field this adapter stamps on the models it serves, and it resolves
|
|
620
|
+
* as a registry key in both regions (issue #75). Nothing about routing, billing
|
|
621
|
+
* or the destination changes: `baseUrl` comes from the model, not from the
|
|
622
|
+
* message.
|
|
623
|
+
*
|
|
624
|
+
* Only `provider` is rewritten. `api` is left alone on purpose: a legacy
|
|
625
|
+
* (`dsh-foreign`) message carries an api this plugin never sends, and claiming
|
|
626
|
+
* `openai-completions` for it would assert a wire shape that message was never
|
|
627
|
+
* built for. The `provider` mismatch is what decides the downgrade, and fixing
|
|
628
|
+
* it is enough — `isSameModel` still requires the model id and api to match, so
|
|
629
|
+
* a genuinely different model keeps degrading (covered by a test).
|
|
630
|
+
*
|
|
631
|
+
* Returns the same object when nothing changed, so the common case (an
|
|
632
|
+
* already-normalized transcript) allocates nothing.
|
|
633
|
+
*/
|
|
634
|
+
function alignAssistantIdentities(context, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
635
|
+
const messages = context.messages;
|
|
636
|
+
if (!Array.isArray(messages) || messages.length === 0) return context;
|
|
637
|
+
let changed = false;
|
|
638
|
+
const next = messages.map((message) => {
|
|
639
|
+
if (typeof message !== "object" || message === null) return message;
|
|
640
|
+
const record = message;
|
|
641
|
+
if (record["role"] !== "assistant") return message;
|
|
642
|
+
if (record["provider"] === identity) return message;
|
|
643
|
+
changed = true;
|
|
644
|
+
return {
|
|
645
|
+
...record,
|
|
646
|
+
provider: identity
|
|
647
|
+
};
|
|
648
|
+
});
|
|
649
|
+
return changed ? {
|
|
650
|
+
...context,
|
|
651
|
+
messages: next
|
|
652
|
+
} : context;
|
|
653
|
+
}
|
|
654
|
+
function withLegacyContext(api, generation, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
655
|
+
/** One place so both branches and both entry points agree on the order. */
|
|
656
|
+
const prepare = (context) => alignAssistantIdentities(generation === "modern" ? foldModernContext(context) : adaptLegacyPiAiContext(context), identity);
|
|
593
657
|
if (generation === "modern") return {
|
|
594
658
|
...api,
|
|
595
|
-
stream: (model, context, options) => api.stream(model, transcript(
|
|
596
|
-
streamSimple: (model, context, options) => api.streamSimple(model, transcript(
|
|
659
|
+
stream: (model, context, options) => api.stream(model, transcript(prepare(context)), options),
|
|
660
|
+
streamSimple: (model, context, options) => api.streamSimple(model, transcript(prepare(context)), options)
|
|
597
661
|
};
|
|
598
662
|
return {
|
|
599
663
|
...api,
|
|
600
|
-
stream: (model, context, options) => api.stream(model, transcript(
|
|
601
|
-
streamSimple: (model, context, options) => api.streamSimple(model, transcript(
|
|
664
|
+
stream: (model, context, options) => api.stream(model, transcript(prepare(context)), options),
|
|
665
|
+
streamSimple: (model, context, options) => api.streamSimple(model, transcript(prepare(context)), options)
|
|
602
666
|
};
|
|
603
667
|
}
|
|
604
668
|
/**
|
|
@@ -776,7 +840,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
776
840
|
}
|
|
777
841
|
} },
|
|
778
842
|
models: buildModels(),
|
|
779
|
-
api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation), options.resolveContextRescue)
|
|
843
|
+
api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation, modelIdentity), options.resolveContextRescue)
|
|
780
844
|
}),
|
|
781
845
|
getModels: () => buildModels()
|
|
782
846
|
};
|
|
@@ -3193,6 +3257,34 @@ function createWorkBuddyShim(options) {
|
|
|
3193
3257
|
const onRequestBody = options.onRequestBody;
|
|
3194
3258
|
const onUpstreamUsage = options.onUpstreamUsage;
|
|
3195
3259
|
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
3260
|
+
/**
|
|
3261
|
+
* Whether this process's prompts have been PROVEN to need the invisible
|
|
3262
|
+
* perturbation before the gateway will accept them.
|
|
3263
|
+
*
|
|
3264
|
+
* WHY THIS EXISTS (measured 2026-10-22). The retry ladder below is
|
|
3265
|
+
* discover-by-failing: a request goes out unmodified, is refused with `11128`,
|
|
3266
|
+
* and the ladder re-sends a perturbed copy. That is correct the first time and
|
|
3267
|
+
* wasteful every time after — the prompt SHAPE that carries a fingerprinted
|
|
3268
|
+
* substring does not change between turns of a session, so the plugin was
|
|
3269
|
+
* paying one refused request (~209ms measured) on EVERY request, forever.
|
|
3270
|
+
*
|
|
3271
|
+
* So once the perturbation is what made a request succeed, the next request
|
|
3272
|
+
* starts from the perturbed form and skips the doomed first attempt. The
|
|
3273
|
+
* saving is one round trip plus one refused request per turn.
|
|
3274
|
+
*
|
|
3275
|
+
* WHY A PROCESS-LOCAL FLAG AND NOT A CONFIG. The gateway's fingerprint list is
|
|
3276
|
+
* a snapshot the upstream can change at any time, in either direction: a rule
|
|
3277
|
+
* can be withdrawn as easily as added. A persisted flag would therefore
|
|
3278
|
+
* outlive the condition that justified it and could keep perturbing prompts a
|
|
3279
|
+
* gateway no longer objects to. In memory it is forgotten at restart, which is
|
|
3280
|
+
* the natural expiry for "what this gateway refused recently".
|
|
3281
|
+
*
|
|
3282
|
+
* AND IT SELF-CORRECTS. If a perturbed body is EVER refused with the channel
|
|
3283
|
+
* gate, the flag is cleared and the request falls back to the plain ladder —
|
|
3284
|
+
* so a withdrawn rule, or a different model behind the same gateway, cannot
|
|
3285
|
+
* leave the plugin stuck sending a shape that no longer helps.
|
|
3286
|
+
*/
|
|
3287
|
+
let channelNeedsPerturbation = false;
|
|
3196
3288
|
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
3197
3289
|
function bearerOk(req) {
|
|
3198
3290
|
const header = req.headers.authorization;
|
|
@@ -3326,7 +3418,25 @@ function createWorkBuddyShim(options) {
|
|
|
3326
3418
|
}
|
|
3327
3419
|
const controller = new AbortController();
|
|
3328
3420
|
res.on("close", () => controller.abort());
|
|
3329
|
-
|
|
3421
|
+
/**
|
|
3422
|
+
* The body to send FIRST, which is the perturbed form once this process has
|
|
3423
|
+
* proven the gateway demands it.
|
|
3424
|
+
*
|
|
3425
|
+
* `perturbChannelFingerprints` is consulted only when the flag is set, so a
|
|
3426
|
+
* fresh process still pays the plain attempt — that is what discovers
|
|
3427
|
+
* whether the gate is on at all (see `channelNeedsPerturbation`).
|
|
3428
|
+
*
|
|
3429
|
+
* The perturbed form is a purely additive transform: it inserts zero-width
|
|
3430
|
+
* spaces and changes no word the model reads, so starting from it is not a
|
|
3431
|
+
* behaviour change, only a saved round trip.
|
|
3432
|
+
*/
|
|
3433
|
+
const preferred = channelNeedsPerturbation ? perturbChannelFingerprints(prepared) ?? prepared : prepared;
|
|
3434
|
+
let result = await client.chatStream(credential, preferred, controller.signal);
|
|
3435
|
+
if (channelNeedsPerturbation && !result.ok && result.kind === "channel_reject") {
|
|
3436
|
+
channelNeedsPerturbation = false;
|
|
3437
|
+
logger?.warn("dsh-connect-workbuddy: a perturbed body was refused too; clearing the learned preference and retrying from the plain form");
|
|
3438
|
+
result = await client.chatStream(credential, prepared, controller.signal);
|
|
3439
|
+
}
|
|
3330
3440
|
/**
|
|
3331
3441
|
* The retry ladder that does not depend on knowing the fingerprint
|
|
3332
3442
|
* (issue #98, both halves).
|
|
@@ -3374,6 +3484,7 @@ function createWorkBuddyShim(options) {
|
|
|
3374
3484
|
logger?.warn("dsh-connect-workbuddy: the channel gate refused the body again; retrying once with an invisible perturbation of the scanned text");
|
|
3375
3485
|
result = await client.chatStream(credential, rescued, controller.signal);
|
|
3376
3486
|
retriedWithPerturbation = true;
|
|
3487
|
+
if (result.ok) channelNeedsPerturbation = true;
|
|
3377
3488
|
}
|
|
3378
3489
|
}
|
|
3379
3490
|
}
|
|
@@ -3506,7 +3617,8 @@ function createWorkBuddyShim(options) {
|
|
|
3506
3617
|
const refusedTier = reasoningTierRefusal(result);
|
|
3507
3618
|
const receipt = refusedTier ?? detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
|
|
3508
3619
|
const advice = refusedTier !== void 0 ? "这不是账号或网络问题:该模型实测拒绝「off」推理档位,请到模型卡片里取消该行的「可关闭思考」勾选后重试" : result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
|
|
3509
|
-
const
|
|
3620
|
+
const overflow = isContextTooLongBody(result.message) ? `${CONTEXT_OVERFLOW_MARKER}。` : "";
|
|
3621
|
+
const body = siblingGate !== void 0 ? siblingGate : `${overflow}${advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`}`;
|
|
3510
3622
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${why}${stamp}。${body}`, hostErrorCode(result.kind));
|
|
3511
3623
|
return;
|
|
3512
3624
|
}
|
|
@@ -4206,6 +4318,199 @@ async function probeModel(input) {
|
|
|
4206
4318
|
};
|
|
4207
4319
|
}
|
|
4208
4320
|
/**
|
|
4321
|
+
* Smallest spacing between content chunks, in milliseconds, that still counts
|
|
4322
|
+
* as generation.
|
|
4323
|
+
*
|
|
4324
|
+
* Guards the divide-by-zero case where a fast model delivers every token in a
|
|
4325
|
+
* single burst: `chunks === 1` means the stream told us nothing about the RATE,
|
|
4326
|
+
* and the run reports no speed rather than an invented one. The same applies
|
|
4327
|
+
* when the first and last content chunk land in the same millisecond.
|
|
4328
|
+
*/
|
|
4329
|
+
const MIN_GENERATION_MS = 1;
|
|
4330
|
+
/** Build the request body for one speed run. */
|
|
4331
|
+
function tpsProbeRequestBody(modelId) {
|
|
4332
|
+
return JSON.stringify({
|
|
4333
|
+
model: modelId,
|
|
4334
|
+
messages: [{
|
|
4335
|
+
role: "system",
|
|
4336
|
+
content: "You are a throughput benchmark. Output a long, uninterrupted sequence of short words separated by spaces. No preamble, no explanation, no code block."
|
|
4337
|
+
}, {
|
|
4338
|
+
role: "user",
|
|
4339
|
+
content: "List the numbers from 1 to 400 in words, separated by single spaces."
|
|
4340
|
+
}],
|
|
4341
|
+
max_tokens: 256,
|
|
4342
|
+
...OFF_REASONING_FIELDS,
|
|
4343
|
+
stream: true
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Ask for the lowest reasoning effort available.
|
|
4348
|
+
*
|
|
4349
|
+
* A reasoning model asked to count spends its `max_tokens` inside the reasoning
|
|
4350
|
+
* channel; the stream then carries thinking deltas and little or no content, and
|
|
4351
|
+
* timing it would measure the wrong thing. `none` is the widest supported
|
|
4352
|
+
* spelling; gateways that do not know it ignore the field (measured: the CN
|
|
4353
|
+
* gateway answers 200 with it).
|
|
4354
|
+
*/
|
|
4355
|
+
const OFF_REASONING_FIELDS = { reasoning_effort: "none" };
|
|
4356
|
+
/**
|
|
4357
|
+
* Read one SSE stream INCREMENTALLY, timing each content deltas arrival.
|
|
4358
|
+
*
|
|
4359
|
+
* The incremental read is the measurement. `response.text()` buffers the whole
|
|
4360
|
+
* body and hands it over once, which erases the only signal a throughput number
|
|
4361
|
+
* is made of: the interval between the first and last content chunk. A version of
|
|
4362
|
+
* this file that parsed a buffered string reported the PARSING time, not the
|
|
4363
|
+
* generation time, and looked entirely plausible — which is why the reader takes
|
|
4364
|
+
* the response body and not a string.
|
|
4365
|
+
*
|
|
4366
|
+
* Only `choices[0].delta.content` is timed. A reasoning delta is not output:
|
|
4367
|
+
* counting it would report a reasoning model as fast because it produced a lot of
|
|
4368
|
+
* private thinking, and reasoning models are precisely the ones where the
|
|
4369
|
+
* distinction matters.
|
|
4370
|
+
*
|
|
4371
|
+
* `startedAt` is the caller's clock reading taken BEFORE the request went out.
|
|
4372
|
+
* Defaulting it to "now" would turn time-to-first-token into "time to start
|
|
4373
|
+
* reading" — a plausible-looking number wrong by the whole connect+TTFB.
|
|
4374
|
+
*
|
|
4375
|
+
* Never throws on a malformed frame: the upstream is allowed to send comments,
|
|
4376
|
+
* blank lines and `[DONE]`, and a stream that ends early still reports what it
|
|
4377
|
+
* timed.
|
|
4378
|
+
*/
|
|
4379
|
+
async function readTimedStream(body, startedAt) {
|
|
4380
|
+
const reader = body.getReader();
|
|
4381
|
+
const decoder = new TextDecoder();
|
|
4382
|
+
let pending = "";
|
|
4383
|
+
let firstAt;
|
|
4384
|
+
let lastAt;
|
|
4385
|
+
let usage;
|
|
4386
|
+
try {
|
|
4387
|
+
while (true) {
|
|
4388
|
+
const { done, value } = await reader.read();
|
|
4389
|
+
if (done) break;
|
|
4390
|
+
const arrival = Date.now();
|
|
4391
|
+
pending += decoder.decode(value, { stream: true });
|
|
4392
|
+
const lines = pending.split("\n");
|
|
4393
|
+
pending = lines.pop() ?? "";
|
|
4394
|
+
for (const line of lines) {
|
|
4395
|
+
const reading = timingsOfLine(line, arrival);
|
|
4396
|
+
if (reading.usage !== void 0) usage = reading.usage;
|
|
4397
|
+
if (reading.content !== void 0) {
|
|
4398
|
+
if (firstAt === void 0) firstAt = arrival;
|
|
4399
|
+
lastAt = arrival;
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
} catch {} finally {
|
|
4404
|
+
reader.cancel().catch(() => {});
|
|
4405
|
+
}
|
|
4406
|
+
const outputTokens = typeof usage?.["completion_tokens"] === "number" ? usage["completion_tokens"] : void 0;
|
|
4407
|
+
const credit = typeof usage?.["credit"] === "number" ? usage["credit"] : void 0;
|
|
4408
|
+
return {
|
|
4409
|
+
...outputTokens === void 0 ? {} : { outputTokens },
|
|
4410
|
+
...firstAt === void 0 ? {} : { firstContentMs: firstAt - startedAt },
|
|
4411
|
+
...firstAt === void 0 || lastAt === void 0 ? {} : { generationMs: lastAt - firstAt },
|
|
4412
|
+
...credit === void 0 ? {} : { credit }
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
/**
|
|
4416
|
+
* What one SSE line says: whether it carried content, and any usage frame.
|
|
4417
|
+
*
|
|
4418
|
+
* Split out so the frame grammar is testable without a stream. The `arrival`
|
|
4419
|
+
* timestamp is threaded in by the reader, because a parser that read the clock
|
|
4420
|
+
* itself would be timing the parse, not the arrival.
|
|
4421
|
+
*/
|
|
4422
|
+
function timingsOfLine(line, _arrival) {
|
|
4423
|
+
if (!line.startsWith("data:")) return {};
|
|
4424
|
+
const payload = line.slice(5).trim();
|
|
4425
|
+
if (payload === "" || payload === "[DONE]") return {};
|
|
4426
|
+
let frame;
|
|
4427
|
+
try {
|
|
4428
|
+
frame = JSON.parse(payload);
|
|
4429
|
+
} catch {
|
|
4430
|
+
return {};
|
|
4431
|
+
}
|
|
4432
|
+
if (typeof frame !== "object" || frame === null || Array.isArray(frame)) return {};
|
|
4433
|
+
const record = frame;
|
|
4434
|
+
const usageField = record["usage"];
|
|
4435
|
+
const usage = typeof usageField === "object" && usageField !== null && !Array.isArray(usageField) ? usageField : void 0;
|
|
4436
|
+
const choices = record["choices"];
|
|
4437
|
+
const first = Array.isArray(choices) ? choices[0] : void 0;
|
|
4438
|
+
const delta = typeof first === "object" && first !== null ? first["delta"] : void 0;
|
|
4439
|
+
const content = typeof delta === "object" && delta !== null ? delta["content"] : void 0;
|
|
4440
|
+
return {
|
|
4441
|
+
...typeof content === "string" && content !== "" ? { content } : {},
|
|
4442
|
+
...usage === void 0 ? {} : { usage }
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
/** The middle value; undefined for an empty list. */
|
|
4446
|
+
function median(values) {
|
|
4447
|
+
if (values.length === 0) return void 0;
|
|
4448
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4449
|
+
const middle = Math.floor(sorted.length / 2);
|
|
4450
|
+
return sorted.length % 2 === 1 ? sorted[middle] : Math.round((sorted[middle - 1] + sorted[middle]) / 2);
|
|
4451
|
+
}
|
|
4452
|
+
/**
|
|
4453
|
+
* Reduce several run readings to one sample.
|
|
4454
|
+
*
|
|
4455
|
+
* The median per quantity is taken SEPARATELY rather than picking one "winner"
|
|
4456
|
+
* run: a run whose stream was truncated may still have timed its first chunk
|
|
4457
|
+
* honestly, and throwing that away would lose information for no reason. A
|
|
4458
|
+
* quantity with no usable reading is omitted, never filled in.
|
|
4459
|
+
*/
|
|
4460
|
+
function summarizeTps(modelId, runs, attempted) {
|
|
4461
|
+
const complete = runs.length === attempted && attempted > 0;
|
|
4462
|
+
const speeds = [];
|
|
4463
|
+
const firsts = [];
|
|
4464
|
+
let outputTokens;
|
|
4465
|
+
let credit;
|
|
4466
|
+
for (const run of runs) {
|
|
4467
|
+
if (run.firstContentMs !== void 0) firsts.push(run.firstContentMs);
|
|
4468
|
+
if (run.generationMs !== void 0 && run.generationMs >= MIN_GENERATION_MS && run.outputTokens !== void 0 && run.outputTokens > 0) speeds.push(run.outputTokens / (run.generationMs / 1e3));
|
|
4469
|
+
if (run.outputTokens !== void 0) outputTokens = run.outputTokens;
|
|
4470
|
+
if (run.credit !== void 0) credit = (credit ?? 0) + run.credit;
|
|
4471
|
+
}
|
|
4472
|
+
const tokensPerSecond = median(speeds.map((speed) => Math.round(speed)));
|
|
4473
|
+
const reason = tokensPerSecond !== void 0 ? void 0 : runs.length === 0 ? "all-failed" : outputTokens === void 0 ? "no-usage" : "few-chunks";
|
|
4474
|
+
return {
|
|
4475
|
+
modelId,
|
|
4476
|
+
...tokensPerSecond === void 0 ? {} : { tokensPerSecond },
|
|
4477
|
+
...outputTokens === void 0 ? {} : { outputTokens },
|
|
4478
|
+
...median(firsts) === void 0 ? {} : { timeToFirstTokenMs: median(firsts) },
|
|
4479
|
+
samples: runs.length,
|
|
4480
|
+
...credit === void 0 ? {} : { credit: Math.round(credit * 100) / 100 },
|
|
4481
|
+
complete,
|
|
4482
|
+
...reason === void 0 ? {} : { reason }
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Measure one model's output speed.
|
|
4487
|
+
*
|
|
4488
|
+
* Runs {@link TPS_PROBE_RUNS} times and takes per-quantity medians (see
|
|
4489
|
+
* {@link summarizeTps} for why a median and not a mean). A failed run is dropped
|
|
4490
|
+
* rather than counted as zero — a model that answered twice at 400 tok/s did not
|
|
4491
|
+
* get slower because the third request timed out — but the sample count records
|
|
4492
|
+
* the shortfall so the card can say "n/3".
|
|
4493
|
+
*
|
|
4494
|
+
* Never throws: this is a diagnostic the user asked for, and a dead model must
|
|
4495
|
+
* produce a result row, not an exception.
|
|
4496
|
+
*/
|
|
4497
|
+
async function measureTps(input) {
|
|
4498
|
+
const runs = input.runs ?? 3;
|
|
4499
|
+
const readings = [];
|
|
4500
|
+
for (let index = 0; index < runs; index++) {
|
|
4501
|
+
if (input.signal?.aborted === true) break;
|
|
4502
|
+
const startedAt = Date.now();
|
|
4503
|
+
try {
|
|
4504
|
+
const answer = await input.client.probeChat(input.credential, tpsProbeRequestBody(input.modelId), input.signal);
|
|
4505
|
+
if (!answer.ok || answer.response === void 0) continue;
|
|
4506
|
+
readings.push(await readTimedStream(answer.response.body, startedAt));
|
|
4507
|
+
} catch {
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
return summarizeTps(input.modelId, readings, runs);
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4209
4514
|
* Check in every account, one at a time.
|
|
4210
4515
|
*
|
|
4211
4516
|
* Idempotent by design (see the module note): an account already checked in
|
|
@@ -5372,6 +5677,17 @@ const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
|
5372
5677
|
*/
|
|
5373
5678
|
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
5374
5679
|
/**
|
|
5680
|
+
* Plugin-owned OUTPUT-SPEED measurement endpoint (issue #92).
|
|
5681
|
+
*
|
|
5682
|
+
* Separate from {@link WORKBUDDY_PROBE_PATH} because the two ask opposite
|
|
5683
|
+
* questions and cost different amounts: the availability probe sends 25,000
|
|
5684
|
+
* input tokens and caps output at one (measured `credit 0.72`), while this one
|
|
5685
|
+
* sends ~35 input tokens and caps output at 256 (measured `credit 0.01`–`0.15`).
|
|
5686
|
+
* Sharing the route would mean sharing its one-model-per-request cap and its
|
|
5687
|
+
* cost wording, and the two would then be wrong for each other.
|
|
5688
|
+
*/
|
|
5689
|
+
const WORKBUDDY_TPS_PATH = "/plugins/dsh-connect-workbuddy/tps";
|
|
5690
|
+
/**
|
|
5375
5691
|
* Plugin-owned account-pool endpoint: one batch action per call.
|
|
5376
5692
|
*
|
|
5377
5693
|
* `?action=checkin` claims the daily reward for every account of a region;
|
|
@@ -6227,6 +6543,28 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
6227
6543
|
}
|
|
6228
6544
|
}
|
|
6229
6545
|
});
|
|
6546
|
+
const disposeTps = ctx.webServer.register({
|
|
6547
|
+
kind: "exact",
|
|
6548
|
+
path: WORKBUDDY_TPS_PATH,
|
|
6549
|
+
handler: async (req, res) => {
|
|
6550
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
6551
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
6552
|
+
if (deps.measureTps === void 0) return json(res, 503, { error: "tps measurement unavailable" });
|
|
6553
|
+
const region = requestRegion(req, res);
|
|
6554
|
+
if (region === void 0) return;
|
|
6555
|
+
try {
|
|
6556
|
+
const body = await readJsonBody(req);
|
|
6557
|
+
const modelIds = Array.isArray(body.modelIds) ? body.modelIds.filter((id) => typeof id === "string" && id !== "") : [];
|
|
6558
|
+
if (modelIds.length === 0) return json(res, 400, { error: "modelIds must be a non-empty array of strings" });
|
|
6559
|
+
if (modelIds.length > 1) return json(res, 400, { error: "a tps measurement accepts exactly one model" });
|
|
6560
|
+
const results = [];
|
|
6561
|
+
for (const modelId of modelIds) results.push(...await deps.measureTps(region, [modelId]));
|
|
6562
|
+
json(res, 200, { results });
|
|
6563
|
+
} catch (error) {
|
|
6564
|
+
json(res, 500, { error: safeMessage(error) });
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
});
|
|
6230
6568
|
const disposePool = ctx.webServer.register({
|
|
6231
6569
|
kind: "exact",
|
|
6232
6570
|
path: WORKBUDDY_POOL_PATH,
|
|
@@ -6502,6 +6840,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
6502
6840
|
disposeUsage();
|
|
6503
6841
|
disposeAccountCredits();
|
|
6504
6842
|
disposeProbe();
|
|
6843
|
+
disposeTps();
|
|
6505
6844
|
disposePool();
|
|
6506
6845
|
disposeOAuth();
|
|
6507
6846
|
disposeTransfer();
|
|
@@ -7152,6 +7491,51 @@ function apply(ctx, config) {
|
|
|
7152
7491
|
}));
|
|
7153
7492
|
return results;
|
|
7154
7493
|
},
|
|
7494
|
+
/**
|
|
7495
|
+
* Output-speed measurement (issue #92), region-scoped like every other
|
|
7496
|
+
* spending path.
|
|
7497
|
+
*
|
|
7498
|
+
* Deliberately NOT routed through the probe's failover machinery: a speed
|
|
7499
|
+
* number describes the MODEL, and having the pool silently serve the run
|
|
7500
|
+
* from a different account would attach one account's rate limit (6004) to
|
|
7501
|
+
* another's measurement. The selected credential runs all three samples, so
|
|
7502
|
+
* "it failed" means this account could not measure it, which is the honest
|
|
7503
|
+
* answer.
|
|
7504
|
+
*/
|
|
7505
|
+
async measureTps(region, modelIds, options) {
|
|
7506
|
+
let credential;
|
|
7507
|
+
try {
|
|
7508
|
+
credential = await stacks[region].store.resolve();
|
|
7509
|
+
} catch (error) {
|
|
7510
|
+
const message = error instanceof Error ? error.message.slice(0, 300) : String(error).slice(0, 300);
|
|
7511
|
+
return modelIds.map((modelId) => ({
|
|
7512
|
+
modelId,
|
|
7513
|
+
outcome: "credential-rejected",
|
|
7514
|
+
samples: 0,
|
|
7515
|
+
message
|
|
7516
|
+
}));
|
|
7517
|
+
}
|
|
7518
|
+
const results = [];
|
|
7519
|
+
for (const modelId of modelIds) {
|
|
7520
|
+
const sample = await measureTps({
|
|
7521
|
+
client,
|
|
7522
|
+
credential,
|
|
7523
|
+
modelId,
|
|
7524
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
7525
|
+
});
|
|
7526
|
+
results.push({
|
|
7527
|
+
modelId: sample.modelId,
|
|
7528
|
+
outcome: sample.tokensPerSecond === void 0 ? "failed" : "ok",
|
|
7529
|
+
...sample.tokensPerSecond === void 0 ? {} : { tokensPerSecond: sample.tokensPerSecond },
|
|
7530
|
+
...sample.timeToFirstTokenMs === void 0 ? {} : { timeToFirstTokenMs: sample.timeToFirstTokenMs },
|
|
7531
|
+
...sample.outputTokens === void 0 ? {} : { outputTokens: sample.outputTokens },
|
|
7532
|
+
samples: sample.samples,
|
|
7533
|
+
...sample.reason === void 0 ? {} : { reason: sample.reason },
|
|
7534
|
+
...sample.credit === void 0 ? {} : { credit: sample.credit }
|
|
7535
|
+
});
|
|
7536
|
+
}
|
|
7537
|
+
return results;
|
|
7538
|
+
},
|
|
7155
7539
|
regionUsable(region, usable) {
|
|
7156
7540
|
if (stacks[region].catalog.setRegionUsable(usable)) invalidateCatalog();
|
|
7157
7541
|
},
|
|
@@ -8017,4 +8401,4 @@ function apply(ctx, config) {
|
|
|
8017
8401
|
});
|
|
8018
8402
|
}
|
|
8019
8403
|
//#endregion
|
|
8020
|
-
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 };
|
|
8404
|
+
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_TPS_PATH, 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": "5.
|
|
5
|
+
"version": "5.2.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|