dsh-router-core 0.3.2 → 0.3.3
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/README.md +5 -1
- package/docs/suppliers.md +19 -3
- package/lib/client-registry.js +13 -8
- package/lib/client.js +13 -8
- package/lib/index.js +611 -215
- package/lib/types/llm/adapter.d.ts +7 -1
- package/lib/types/router/account-pool.d.ts +54 -19
- package/lib/types/router/index.d.ts +5 -0
- package/lib/types/router/usage-store.d.ts +20 -3
- package/lib/types/shared.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/RouterView.tsx +1 -1
- package/src/client/SupplierDetail.tsx +8 -8
- package/src/llm/adapter.test.ts +404 -1
- package/src/llm/adapter.ts +242 -34
- package/src/router/account-pool.test.ts +163 -61
- package/src/router/account-pool.ts +200 -68
- package/src/router/index.ts +137 -38
- package/src/router/models-cache.test.ts +87 -4
- package/src/router/precise-dispatch.test.ts +110 -7
- package/src/router/usage-store.ts +174 -74
- package/src/router/usage.test.ts +115 -0
- package/src/shared.ts +2 -0
- package/src/suppliers/loader.ts +2 -1
- package/src/suppliers/registry.test.ts +23 -3
- package/src/suppliers/registry.ts +7 -2
package/README.md
CHANGED
|
@@ -71,7 +71,11 @@ dsh plugin --profile web add dsh-router-core
|
|
|
71
71
|
- Top 榜:按供应商 / 按模型(请求数带失败计数);
|
|
72
72
|
- 最近请求:时间 / 模型 / 供应商 / in↑ out↓ / 耗时,显示最近 10 条;
|
|
73
73
|
- **清空** — 清掉全部用量统计(不影响供应商、账号、组合配置);
|
|
74
|
-
- 数据落盘 `data/usage.json`(按天聚合 + 最近 500 条明细 +
|
|
74
|
+
- 数据落盘 `data/usage.json`(按天聚合 + 每天 24 个小时桶 + 最近 500 条明细 +
|
|
75
|
+
累计计数)。今日/7 天/30 天读天桶、24 小时读小时桶,都不受明细环容量限制;
|
|
76
|
+
明细环只服务「最近请求」列表。
|
|
77
|
+
小时桶从新数据开始累积,升级前那几天的天内分布查不到(明细环只剩 500 条
|
|
78
|
+
回溯不回去),那段历史的小时柱状图留空、24 小时口径按整桶计入 —— 不编数据。
|
|
75
79
|
token 口径:上游返回 `usage` 就用真值(分散在多帧时按字段取最大值合并);
|
|
76
80
|
上游不发时按 ~4 字符/token 估算,面板上标 `~`。**失败请求不估算**——
|
|
77
81
|
它没到上游,编造输入 token 只会把总量灌水;
|
package/docs/suppliers.md
CHANGED
|
@@ -76,7 +76,7 @@ js 报告**此刻**观察到的账户状况,而不是把上游原始响应丢给
|
|
|
76
76
|
|
|
77
77
|
**`no_such_model` 是必须的,不能拿 `unavailable` 顶替。** 后者在核心策略里
|
|
78
78
|
会计一次连续错误——组合里每有一个别人家的模型,就会给无关账号攒一次错误,
|
|
79
|
-
攒够 3 次把它冷却
|
|
79
|
+
攒够 3 次把它冷却 5 分钟,表现为「组合越用越没号可用」。所以「这个模型不属于我」
|
|
80
80
|
一定要报 `no_such_model`,核心才知道该换供应商而不是换账号。
|
|
81
81
|
|
|
82
82
|
**js 只说「现在怎么了」,不说「该怎么办」。** 冷却多久、是否禁用、要不要换号
|
|
@@ -108,7 +108,7 @@ type ChatOnceResult =
|
|
|
108
108
|
| `rate_limit` | 冷却 1 分钟 |
|
|
109
109
|
| `quota` | 冷却 10 分钟 |
|
|
110
110
|
| `session_dead` | **永久禁用**(需重新登录才能恢复) |
|
|
111
|
-
| `unavailable` / `transport` / `unknown` | 计一次连续错误,攒够 3 次冷却
|
|
111
|
+
| `unavailable` / `transport` / `unknown` | 计一次连续错误,攒够 3 次冷却 5 分钟 |
|
|
112
112
|
| `no_such_model` | **不惩罚账号**,核心直接跳过整个供应商换下一个 |
|
|
113
113
|
|
|
114
114
|
- 非流式若聚合后发现**空响应**(无 content / reasoning / tool_calls),请返回
|
|
@@ -128,6 +128,22 @@ type ChatOnceResult =
|
|
|
128
128
|
- 要让「上游吐到一半才断」也能回退,必须在写响应头前缓冲判定,代价是
|
|
129
129
|
首字节延迟,目前没做
|
|
130
130
|
|
|
131
|
+
### 终止帧 `[DONE]` 由核心兜底
|
|
132
|
+
|
|
133
|
+
客户端(dsh-llm adapter)严格等 `data: [DONE]` 才认为流正常结束;缺了就抛
|
|
134
|
+
`SSE payload stream ended without [DONE]`,整轮判失败——已经流式吐出去的内容
|
|
135
|
+
**全部作废**。
|
|
136
|
+
|
|
137
|
+
核心是 SSE 的唯一写入方,所以这个兜底由核心做,不要求 js 合成:
|
|
138
|
+
|
|
139
|
+
- 上游流结束时**没发过** `[DONE]` → 核心补发一个(降级为「拿到截断内容并
|
|
140
|
+
正常收尾」,而不是整轮白干)
|
|
141
|
+
- 上游**已经发了** → 不再重复发(重复 `[DONE]` 同样坑客户端)
|
|
142
|
+
- **一个字节都没写出就断** → **不补**,留给换号/换模型回退(补了就把这次
|
|
143
|
+
失败坐实成一个空响应,组合回退就没了)
|
|
144
|
+
|
|
145
|
+
js 因此不必自己合成终止帧(合成也不算错,核心会认)。
|
|
146
|
+
|
|
131
147
|
所以 js 的流转换要**尽早产出第一个字节**,别攒着。攒着不仅延迟高,还会把
|
|
132
148
|
自己的失败伪装成「可回退」,让组合以为没成功而反复重试别的模型。
|
|
133
149
|
|
|
@@ -296,7 +312,7 @@ export function dispose() {}
|
|
|
296
312
|
|
|
297
313
|
存裸 id 时,核心只能遍历所有供应商逐个试。插件用 `unavailable` 回答
|
|
298
314
|
「不是我的」,而这个状态在池策略里**计一次连续错误**——组合里每有一个别人家的
|
|
299
|
-
模型就给无关账号攒一次,攒够 3 次冷却
|
|
315
|
+
模型就给无关账号攒一次,攒够 3 次冷却 5 分钟,表现为「组合越用越没号可用」。
|
|
300
316
|
|
|
301
317
|
存了供应商 id 就不需要问了:直接调那一个。查不到供应商(拼错/供应商没加载)
|
|
302
318
|
直接失败,**不兜底遍历**——兜底会让拼错的 id 静默落到别的供应商上,更难查。
|
package/lib/client-registry.js
CHANGED
|
@@ -243,16 +243,21 @@ window.__ModuleLoader__.load({
|
|
|
243
243
|
cache: "no-store"
|
|
244
244
|
})).json();
|
|
245
245
|
if (data.ok && data.total > 0) {
|
|
246
|
-
const failed = (data.results ?? []).filter((r) => r.status !== "ok" && r.status !== "already");
|
|
247
246
|
let msg = `签到完成:${data.succeeded}/${data.total} 成功`;
|
|
248
247
|
const already = data.already ?? 0;
|
|
249
248
|
if (already > 0) msg += ` · ${already} 今日已签`;
|
|
250
|
-
if (failed.length > 0) {
|
|
251
|
-
const first = failed[0];
|
|
252
|
-
msg += ` · ${first?.message ?? "有失败"}`;
|
|
253
|
-
}
|
|
254
249
|
showToast(msg);
|
|
255
|
-
} else
|
|
250
|
+
} else {
|
|
251
|
+
const firstFail = (data.results ?? []).find((r) => r.status !== "ok" && r.status !== "already");
|
|
252
|
+
const reason = firstFail ? `${firstFail.message ?? "有链接签到失败"}` : "";
|
|
253
|
+
const already = data.already ?? 0;
|
|
254
|
+
const head = data.total > 0 ? `签到未完成:${data.succeeded}/${data.total} 成功` : "没有可签到的链接";
|
|
255
|
+
showToast(data.error ?? [
|
|
256
|
+
head,
|
|
257
|
+
already > 0 ? `${already} 今日已签` : "",
|
|
258
|
+
reason
|
|
259
|
+
].filter(Boolean).join(" · "));
|
|
260
|
+
}
|
|
256
261
|
onRefresh();
|
|
257
262
|
} catch (err) {
|
|
258
263
|
showToast(`签到失败:${err.message}`);
|
|
@@ -3392,9 +3397,9 @@ window.__ModuleLoader__.load({
|
|
|
3392
3397
|
children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3393
3398
|
className: "dshr-muted",
|
|
3394
3399
|
children: [
|
|
3395
|
-
supplierAccounts.length,
|
|
3396
|
-
"/",
|
|
3397
3400
|
healthy,
|
|
3401
|
+
"/",
|
|
3402
|
+
supplierAccounts.length,
|
|
3398
3403
|
" 健康"
|
|
3399
3404
|
]
|
|
3400
3405
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
package/lib/client.js
CHANGED
|
@@ -243,16 +243,21 @@ window.__ModuleLoader__.load({
|
|
|
243
243
|
cache: "no-store"
|
|
244
244
|
})).json();
|
|
245
245
|
if (data.ok && data.total > 0) {
|
|
246
|
-
const failed = (data.results ?? []).filter((r) => r.status !== "ok" && r.status !== "already");
|
|
247
246
|
let msg = `签到完成:${data.succeeded}/${data.total} 成功`;
|
|
248
247
|
const already = data.already ?? 0;
|
|
249
248
|
if (already > 0) msg += ` · ${already} 今日已签`;
|
|
250
|
-
if (failed.length > 0) {
|
|
251
|
-
const first = failed[0];
|
|
252
|
-
msg += ` · ${first?.message ?? "有失败"}`;
|
|
253
|
-
}
|
|
254
249
|
showToast(msg);
|
|
255
|
-
} else
|
|
250
|
+
} else {
|
|
251
|
+
const firstFail = (data.results ?? []).find((r) => r.status !== "ok" && r.status !== "already");
|
|
252
|
+
const reason = firstFail ? `${firstFail.message ?? "有链接签到失败"}` : "";
|
|
253
|
+
const already = data.already ?? 0;
|
|
254
|
+
const head = data.total > 0 ? `签到未完成:${data.succeeded}/${data.total} 成功` : "没有可签到的链接";
|
|
255
|
+
showToast(data.error ?? [
|
|
256
|
+
head,
|
|
257
|
+
already > 0 ? `${already} 今日已签` : "",
|
|
258
|
+
reason
|
|
259
|
+
].filter(Boolean).join(" · "));
|
|
260
|
+
}
|
|
256
261
|
onRefresh();
|
|
257
262
|
} catch (err) {
|
|
258
263
|
showToast(`签到失败:${err.message}`);
|
|
@@ -3392,9 +3397,9 @@ window.__ModuleLoader__.load({
|
|
|
3392
3397
|
children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3393
3398
|
className: "dshr-muted",
|
|
3394
3399
|
children: [
|
|
3395
|
-
supplierAccounts.length,
|
|
3396
|
-
"/",
|
|
3397
3400
|
healthy,
|
|
3401
|
+
"/",
|
|
3402
|
+
supplierAccounts.length,
|
|
3398
3403
|
" 健康"
|
|
3399
3404
|
]
|
|
3400
3405
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|