dsh-router-core 0.3.12 → 0.3.14
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 +2 -2
- package/docs/pool-sticky-block.md +51 -0
- package/lib/client-registry.js +56 -1
- package/lib/client.js +56 -1
- package/lib/index.js +320 -59
- package/lib/types/llm/adapter.d.ts +37 -2
- package/lib/types/router/account-pool.d.ts +29 -19
- package/lib/types/router/prefix-affinity.d.ts +6 -0
- package/package.json +9 -5
- package/src/client/SupplierDetail.tsx +1 -1
- package/src/client/index.tsx +3 -0
- package/src/client/model-hint-copy.test.ts +31 -0
- package/src/client/model-hint-copy.ts +55 -0
- package/src/index.ts +61 -2
- package/src/llm/adapter.test.ts +82 -0
- package/src/llm/adapter.ts +145 -15
- package/src/router/account-pool.test.ts +126 -0
- package/src/router/account-pool.ts +103 -39
- package/src/router/index.ts +16 -1
- package/src/router/prefix-affinity.ts +80 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://www.npmjs.com/package/dsh-router-core"><img src="https://img.shields.io/npm/v/dsh-router-core?style=flat-square&logo=npm&label=npm" alt="npm version"></a>
|
|
7
7
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-10b981?style=flat-square" alt="MIT license"></a>
|
|
8
|
-
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本:0.1.
|
|
8
|
+
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本:0.1.5-rc.1+" src="https://img.shields.io/badge/DSH-0.1.5--rc.1%2B-4d6bfe" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
## 快速安装
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
需要 **DSH `0.1.5-rc.1` 及以上**(支持 `dsh plugin` profile 插件机制)、Node.js `>= 20`,以及 `web` profile。
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
dsh plugin --profile web add dsh-router-core
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
> 状态:设计定稿 + 已实现。目标:保留 round-robin 的**多账号均衡**,
|
|
4
4
|
> 只把轮转的**触发方式**从「每请求」改成「满意才走」,从而大幅降低
|
|
5
5
|
> prompt cache miss。
|
|
6
|
+
>
|
|
7
|
+
> **2026-09-09 后续:round-robin 已升级为前缀亲和**(§8 天花板落地,
|
|
8
|
+
> 见文末 §9)。块轮转降级为**回退路径**——亲和拿不到 `messages`
|
|
9
|
+
> (算不出指纹)时才走;块统计(`BLOCK_MIN/BLOCK_HIT_RATIO/BLOCK_MAX`)
|
|
10
|
+
> 保留,从「驱动轮转」改为「判降权」(§9.3)。
|
|
6
11
|
|
|
7
12
|
## 1. 问题:每请求轮转把缓存打碎
|
|
8
13
|
|
|
@@ -204,3 +209,49 @@ pick():
|
|
|
204
209
|
否则触发失真。当前未启用。
|
|
205
210
|
- **TTL 仍未知**:87.5% 是保证下限,实际可能更高。要精确 N 需反推 TTL
|
|
206
211
|
(同号相邻请求间隔 vs 命中率拟合驱逐拐点)。
|
|
212
|
+
|
|
213
|
+
## 9. 前缀亲和(2026-09-09 落地,round-robin 的新语义)
|
|
214
|
+
|
|
215
|
+
### 9.1 机制
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
pick(round-robin):
|
|
219
|
+
fp = hash(messages 前 2 条: role + content 前 512 字符) ← src/router/prefix-affinity.ts
|
|
220
|
+
绑定表[model][fp] 存在且该号健康 → 返回它 ← 亲和:同会话永远同号
|
|
221
|
+
否则 → 游标分一个号 + 记绑定(fp → uid)
|
|
222
|
+
块统计(样本按 (model, uid) 跨驻留累计)只用来判降权,不再驱动轮转
|
|
223
|
+
fp 算不出(无 messages/解析失败)→ 回退块轮转(§2 的旧行为)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- **会话标识 = 前缀指纹**:取前 **2** 条(system + 首条用户消息)是刻意的
|
|
227
|
+
——这两个槽位在会话生命周期里永不变化(新消息只往后追加),指纹从第 1 个
|
|
228
|
+
请求起就稳定。取 3 条会在首条 assistant 回复落进槽位 2 时变一次指纹 →
|
|
229
|
+
会话开局多付一次全量重算。「system + 首条消息逐字相同」的会话,缓存前缀
|
|
230
|
+
本来就相同,共享一个号是收益不是冲突。
|
|
231
|
+
- **新会话按游标分发** → 不同会话天然散到不同号,per-account 缓存空间互不
|
|
232
|
+
清(§2.1 的打架根治)。
|
|
233
|
+
- **绑定失效即重绑**:绑定的号冷却/降权/被禁 → 会话让位换健康号,并更新绑定。
|
|
234
|
+
|
|
235
|
+
### 9.2 为什么亲和负责选号、块不再负责轮转
|
|
236
|
+
|
|
237
|
+
亲和本身已保证驻留(同会话永远粘同号),块轮转若继续驱动换号,会把会话
|
|
238
|
+
踢出已热的前缀——两者叠加只会互相拆台。块统计保留的真实价值是**号的缓存
|
|
239
|
+
质量判定**:某号累计 `BLOCK_MAX`(48) 次命中率仍 < `M`(0.85) → 判缓存
|
|
240
|
+
无效 → 降权 10 分钟(可恢复),亲和与回退路径都跳过它。
|
|
241
|
+
|
|
242
|
+
样本从「按驻留记」改为「按 (model, uid) 跨驻留累计」:亲和开启后多个会话
|
|
243
|
+
各绑各的号交错服务,「驻留」概念只在回退路径存在,按驻留记会漏掉大部分
|
|
244
|
+
真实反馈。回退路径换驻留时清一次样本(保持旧块语义:新驻留 = 新观察窗口)。
|
|
245
|
+
|
|
246
|
+
### 9.3 状态
|
|
247
|
+
|
|
248
|
+
| 项 | 结果 |
|
|
249
|
+
|---|---|
|
|
250
|
+
| 实现 | `src/router/prefix-affinity.ts`(指纹)、`account-pool.ts`(亲和/LRU/样本)、`index.ts`(透传 messages) |
|
|
251
|
+
| 契约 | **不变**——指纹在核心内闭环,插件与 DSH 均无感知 |
|
|
252
|
+
| 测试 | 191/191(含 9 个新亲和用例) |
|
|
253
|
+
| 天花板 | 绑定表 LRU 封顶 512 条(防指纹无限增长);无状态短请求前 2 条全同 → 退化为同号,行为等同 fallback,可接受 |
|
|
254
|
+
|
|
255
|
+
ponytail:会话标识目前靠前缀指纹推断,真要百分百准得等 DSH 在 `ChatRequest`
|
|
256
|
+
透传会话 id(需改契约,不做);指纹对「首条消息就分叉」的客户端(极少见)
|
|
257
|
+
会散得比会话更细,退化为均衡分发,无害。
|
package/lib/client-registry.js
CHANGED
|
@@ -1173,7 +1173,7 @@ window.__ModuleLoader__.load({
|
|
|
1173
1173
|
}),
|
|
1174
1174
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1175
1175
|
className: "dshr-muted",
|
|
1176
|
-
children: "回退 = 按连接池顺序取第一个健康链接;轮询 =
|
|
1176
|
+
children: "回退 = 按连接池顺序取第一个健康链接;轮询 = 按会话前缀粘住同一链接(前缀亲和,多会话不打架),拿不到会话信息时退回块轮转。"
|
|
1177
1177
|
}),
|
|
1178
1178
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1179
1179
|
className: "dshr-select dshr-input",
|
|
@@ -4115,6 +4115,60 @@ window.__ModuleLoader__.load({
|
|
|
4115
4115
|
};
|
|
4116
4116
|
}
|
|
4117
4117
|
//#endregion
|
|
4118
|
+
//#region src/client/model-hint-copy.ts
|
|
4119
|
+
/**
|
|
4120
|
+
* 设置-模型 → Router 卡片:把宿主那句走不通的提示换成正确说法。
|
|
4121
|
+
*
|
|
4122
|
+
* 为什么不能直接改源头:Router 的卡片走宿主的 "unknown" 布局(宿主只认得
|
|
4123
|
+
* llm-deepseek / llm-pi-ai 两套表单),那句
|
|
4124
|
+
* 「其余字段在 settings.yaml 中,请直接编辑对应段。 (llm-dsh-router)」
|
|
4125
|
+
* 属于宿主 `settings.models` 的 locale 字典,而字典注册对同一个 (ns, locale)
|
|
4126
|
+
* 只允许一个占位者(重复注册直接抛),插件覆写不了,契约也没给可覆写字段。
|
|
4127
|
+
* 所以沿用本插件既有的认领手法(同 settings-nav-icon.ts):按文案指纹认出宿主
|
|
4128
|
+
* 渲染的那一段,就地改写。
|
|
4129
|
+
*
|
|
4130
|
+
* 只写 `nodeValue`、不增删节点(节点归 React,摘掉会让它的引用落空)。React 重渲染
|
|
4131
|
+
* 覆盖回来时观察器会再改一次;已是目标文案就早退,不会自激。
|
|
4132
|
+
*
|
|
4133
|
+
* 升级路径:宿主若给该提示长出可覆写字段(或 Router 有了专属布局),删掉本文件。
|
|
4134
|
+
*/
|
|
4135
|
+
/** 指纹:语言无关(不受中英文切换影响),且只出现在 Router 卡片上。 */
|
|
4136
|
+
var FINGERPRINT = "(llm-dsh-router)";
|
|
4137
|
+
/** 正确说法:对齐 README「组合即模型」。 */
|
|
4138
|
+
var ROUTER_MODEL_HINT_COPY = "组合即模型 · 建好的组合会自动出现在 DSH 模型目录中,选中组合名即可直接使用。";
|
|
4139
|
+
/**
|
|
4140
|
+
* 这段文本要不要改写。
|
|
4141
|
+
* @param text 段落当前文本
|
|
4142
|
+
* @returns 需要改写时给出目标文案,否则 undefined
|
|
4143
|
+
*/
|
|
4144
|
+
function routerHintRewrite(text) {
|
|
4145
|
+
if (!text.includes(FINGERPRINT)) return void 0;
|
|
4146
|
+
if (text === "组合即模型 · 建好的组合会自动出现在 DSH 模型目录中,选中组合名即可直接使用。") return void 0;
|
|
4147
|
+
return ROUTER_MODEL_HINT_COPY;
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* 持续把 Router 卡片里的宿主提示改写成 {@link ROUTER_MODEL_HINT_COPY}。
|
|
4151
|
+
* @returns 清理函数:断开观察
|
|
4152
|
+
*/
|
|
4153
|
+
function rewriteRouterModelHint() {
|
|
4154
|
+
const sync = () => {
|
|
4155
|
+
for (const el of document.querySelectorAll("p[class*=\"advancedHint\"]")) {
|
|
4156
|
+
const next = routerHintRewrite(el.textContent ?? "");
|
|
4157
|
+
const node = el.firstChild;
|
|
4158
|
+
if (next === void 0 || node?.nodeType !== Node.TEXT_NODE) continue;
|
|
4159
|
+
node.nodeValue = next;
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
sync();
|
|
4163
|
+
const observer = new MutationObserver(sync);
|
|
4164
|
+
observer.observe(document.body, {
|
|
4165
|
+
childList: true,
|
|
4166
|
+
subtree: true,
|
|
4167
|
+
characterData: true
|
|
4168
|
+
});
|
|
4169
|
+
return () => observer.disconnect();
|
|
4170
|
+
}
|
|
4171
|
+
//#endregion
|
|
4118
4172
|
//#region src/client/index.tsx
|
|
4119
4173
|
/**
|
|
4120
4174
|
* Client half of dsh-router.
|
|
@@ -4157,6 +4211,7 @@ window.__ModuleLoader__.load({
|
|
|
4157
4211
|
}
|
|
4158
4212
|
ctx.effect(() => disposeSettings, "dsh-router: settings section");
|
|
4159
4213
|
ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
|
|
4214
|
+
ctx.effect(() => rewriteRouterModelHint(), "dsh-router: models provider hint copy");
|
|
4160
4215
|
}
|
|
4161
4216
|
//#endregion
|
|
4162
4217
|
exports.apply = apply;
|
package/lib/client.js
CHANGED
|
@@ -1173,7 +1173,7 @@ window.__ModuleLoader__.load({
|
|
|
1173
1173
|
}),
|
|
1174
1174
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1175
1175
|
className: "dshr-muted",
|
|
1176
|
-
children: "回退 = 按连接池顺序取第一个健康链接;轮询 =
|
|
1176
|
+
children: "回退 = 按连接池顺序取第一个健康链接;轮询 = 按会话前缀粘住同一链接(前缀亲和,多会话不打架),拿不到会话信息时退回块轮转。"
|
|
1177
1177
|
}),
|
|
1178
1178
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1179
1179
|
className: "dshr-select dshr-input",
|
|
@@ -4115,6 +4115,60 @@ window.__ModuleLoader__.load({
|
|
|
4115
4115
|
};
|
|
4116
4116
|
}
|
|
4117
4117
|
//#endregion
|
|
4118
|
+
//#region src/client/model-hint-copy.ts
|
|
4119
|
+
/**
|
|
4120
|
+
* 设置-模型 → Router 卡片:把宿主那句走不通的提示换成正确说法。
|
|
4121
|
+
*
|
|
4122
|
+
* 为什么不能直接改源头:Router 的卡片走宿主的 "unknown" 布局(宿主只认得
|
|
4123
|
+
* llm-deepseek / llm-pi-ai 两套表单),那句
|
|
4124
|
+
* 「其余字段在 settings.yaml 中,请直接编辑对应段。 (llm-dsh-router)」
|
|
4125
|
+
* 属于宿主 `settings.models` 的 locale 字典,而字典注册对同一个 (ns, locale)
|
|
4126
|
+
* 只允许一个占位者(重复注册直接抛),插件覆写不了,契约也没给可覆写字段。
|
|
4127
|
+
* 所以沿用本插件既有的认领手法(同 settings-nav-icon.ts):按文案指纹认出宿主
|
|
4128
|
+
* 渲染的那一段,就地改写。
|
|
4129
|
+
*
|
|
4130
|
+
* 只写 `nodeValue`、不增删节点(节点归 React,摘掉会让它的引用落空)。React 重渲染
|
|
4131
|
+
* 覆盖回来时观察器会再改一次;已是目标文案就早退,不会自激。
|
|
4132
|
+
*
|
|
4133
|
+
* 升级路径:宿主若给该提示长出可覆写字段(或 Router 有了专属布局),删掉本文件。
|
|
4134
|
+
*/
|
|
4135
|
+
/** 指纹:语言无关(不受中英文切换影响),且只出现在 Router 卡片上。 */
|
|
4136
|
+
var FINGERPRINT = "(llm-dsh-router)";
|
|
4137
|
+
/** 正确说法:对齐 README「组合即模型」。 */
|
|
4138
|
+
var ROUTER_MODEL_HINT_COPY = "组合即模型 · 建好的组合会自动出现在 DSH 模型目录中,选中组合名即可直接使用。";
|
|
4139
|
+
/**
|
|
4140
|
+
* 这段文本要不要改写。
|
|
4141
|
+
* @param text 段落当前文本
|
|
4142
|
+
* @returns 需要改写时给出目标文案,否则 undefined
|
|
4143
|
+
*/
|
|
4144
|
+
function routerHintRewrite(text) {
|
|
4145
|
+
if (!text.includes(FINGERPRINT)) return void 0;
|
|
4146
|
+
if (text === "组合即模型 · 建好的组合会自动出现在 DSH 模型目录中,选中组合名即可直接使用。") return void 0;
|
|
4147
|
+
return ROUTER_MODEL_HINT_COPY;
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* 持续把 Router 卡片里的宿主提示改写成 {@link ROUTER_MODEL_HINT_COPY}。
|
|
4151
|
+
* @returns 清理函数:断开观察
|
|
4152
|
+
*/
|
|
4153
|
+
function rewriteRouterModelHint() {
|
|
4154
|
+
const sync = () => {
|
|
4155
|
+
for (const el of document.querySelectorAll("p[class*=\"advancedHint\"]")) {
|
|
4156
|
+
const next = routerHintRewrite(el.textContent ?? "");
|
|
4157
|
+
const node = el.firstChild;
|
|
4158
|
+
if (next === void 0 || node?.nodeType !== Node.TEXT_NODE) continue;
|
|
4159
|
+
node.nodeValue = next;
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
sync();
|
|
4163
|
+
const observer = new MutationObserver(sync);
|
|
4164
|
+
observer.observe(document.body, {
|
|
4165
|
+
childList: true,
|
|
4166
|
+
subtree: true,
|
|
4167
|
+
characterData: true
|
|
4168
|
+
});
|
|
4169
|
+
return () => observer.disconnect();
|
|
4170
|
+
}
|
|
4171
|
+
//#endregion
|
|
4118
4172
|
//#region src/client/index.tsx
|
|
4119
4173
|
/**
|
|
4120
4174
|
* Client half of dsh-router.
|
|
@@ -4157,6 +4211,7 @@ window.__ModuleLoader__.load({
|
|
|
4157
4211
|
}
|
|
4158
4212
|
ctx.effect(() => disposeSettings, "dsh-router: settings section");
|
|
4159
4213
|
ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
|
|
4214
|
+
ctx.effect(() => rewriteRouterModelHint(), "dsh-router: models provider hint copy");
|
|
4160
4215
|
}
|
|
4161
4216
|
//#endregion
|
|
4162
4217
|
exports.apply = apply;
|