dsh-router-core 0.3.20 → 0.4.0
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 +77 -34
- package/docs/pool-sticky-block.md +158 -0
- package/lib/client-registry.js +351 -407
- package/lib/client.js +351 -407
- package/lib/index.js +398 -176
- package/lib/types/ext/contract.d.ts +0 -7
- package/lib/types/host-version.d.ts +15 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/router/account-pool.d.ts +53 -18
- package/lib/types/router/index.d.ts +4 -7
- package/lib/types/router/prefix-affinity.d.ts +18 -1
- package/lib/types/router/types.d.ts +7 -4
- package/lib/types/router/usage-store.d.ts +12 -0
- package/lib/types/shared.d.ts +13 -21
- package/lib/types/supplier-config.d.ts +0 -3
- package/lib/types/suppliers/contract.d.ts +2 -2
- package/lib/types/suppliers/registry.d.ts +0 -1
- package/package.json +12 -13
- package/src/client/CombosTab.tsx +8 -53
- package/src/client/ExtDetail.tsx +15 -178
- package/src/client/LastHitDock.tsx +141 -0
- package/src/client/StatsTab.tsx +8 -6
- package/src/client/SupplierDetail.tsx +2 -34
- package/src/client/index.tsx +45 -0
- package/src/client/last-hit-store.ts +89 -0
- package/src/client/model-hint-copy.test.ts +7 -2
- package/src/client/model-hint-copy.ts +2 -2
- package/src/client/router.css +2106 -1630
- package/src/ext/contract.ts +0 -7
- package/src/host-version.test.ts +61 -0
- package/src/host-version.ts +101 -0
- package/src/index.ts +133 -59
- package/src/llm/adapter.test.ts +128 -1
- package/src/llm/adapter.ts +48 -3
- package/src/router/account-pool.test.ts +189 -83
- package/src/router/account-pool.ts +120 -31
- package/src/router/disconnect-leak.test.ts +50 -0
- package/src/router/index.ts +38 -31
- package/src/router/models-cache.test.ts +9 -9
- package/src/router/precise-dispatch.test.ts +6 -6
- package/src/router/prefix-affinity.ts +22 -1
- package/src/router/types.ts +7 -5
- package/src/router/usage-store.ts +16 -0
- package/src/router/usage.test.ts +1 -1
- package/src/shared.ts +13 -24
- package/src/supplier-config.ts +2 -13
- package/src/suppliers/contract.ts +2 -2
- package/src/suppliers/loader.test.ts +2 -2
- package/src/suppliers/registry.ts +1 -20
- package/src/types.d.ts +2 -1
package/README.md
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
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="
|
|
8
|
+
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="DSH 0.1.5" src="https://img.shields.io/badge/DSH-0.1.5-4d6bfe?style=flat-square" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="DSH 0.1.7-alpha.2 支持" src="https://img.shields.io/badge/DSH-0.1.7--alpha.2-e400dd?style=flat-square" /></a>
|
|
10
|
+
<a href="#agent-team-支持"><img alt="Agent Team" src="https://img.shields.io/badge/Agent%20Team-%E2%9C%93-10b981?style=flat-square" /></a>
|
|
9
11
|
</p>
|
|
10
12
|
|
|
11
13
|
<p align="center">
|
|
12
14
|
<a href="#快速安装">快速安装</a> ·
|
|
13
15
|
<a href="#面板设置--路由">面板</a> ·
|
|
16
|
+
<a href="#agent-team-支持">Agent Team</a> ·
|
|
14
17
|
<a href="#api-端点openai-兼容3080v1">API 端点</a> ·
|
|
15
18
|
<a href="docs/suppliers.md">供应商开发</a> ·
|
|
16
19
|
<a href="docs/ext.md">扩展开发</a>
|
|
@@ -21,6 +24,11 @@
|
|
|
21
24
|
管理界面在**设置 → 路由**(官方设置页座位,不是自己开的页面)。装好即用,
|
|
22
25
|
不用多开一个 9router、不用维护第二个端口、不用在网关和 DSH 之间搬配置。
|
|
23
26
|
|
|
27
|
+
**原生支持 Agent Team / 多会话** —— 0.1.7 起每个 team 成员是独立会话,
|
|
28
|
+
dsh-router 按**会话身份**亲和选号:同会话固定落同一个连接(前缀缓存只写一份),
|
|
29
|
+
不同会话尽量铺开到不同连接;多会话共用同号时按会话隔离统计,**不会互相拖累**。
|
|
30
|
+
详见 [Agent Team 支持](#agent-team-支持)。
|
|
31
|
+
|
|
24
32
|

|
|
25
33
|
|
|
26
34
|
## 快速安装
|
|
@@ -42,16 +50,17 @@ dsh plugin --profile web add dsh-router-core
|
|
|
42
50
|
|
|
43
51
|
## 它解决什么问题
|
|
44
52
|
|
|
45
|
-
| 能力
|
|
46
|
-
|
|
47
|
-
| 零额外进程
|
|
48
|
-
| 扩展即插即拔
|
|
49
|
-
| 供应商即插即拔 | 内置供应商随插件分发;更多供应商 = 装一个 DSH 插件(`dsh-router-*`)或放一个 js 文件到 `~/.dsh/profiles/web/suppliers/`。
|
|
50
|
-
| 模型不内置
|
|
51
|
-
| 策略只写一次
|
|
52
|
-
| 凭证单库
|
|
53
|
-
| 组合即模型
|
|
54
|
-
| 用量可观测
|
|
53
|
+
| 能力 | 说明 |
|
|
54
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
55
|
+
| 零额外进程 | 就是 DSH 插件,随 `dsh web` 启停,天然同源(`/router/api/*` 无 CORS、面板嵌在设置里)。 |
|
|
56
|
+
| 扩展即插即拔 | 扩展插件(如 [`dsh-router-ext-rtk`](https://github.com/CARVIN94/dsh-router-ext-rtk))经 `router.ext` 注册,在 bash 执行前改写命令(如加 `rtk` 前缀压缩输出)。面板「扩展」页一键开关,带自检。 |
|
|
57
|
+
| 供应商即插即拔 | 内置供应商随插件分发;更多供应商 = 装一个 DSH 插件(`dsh-router-*`)或放一个 js 文件到 `~/.dsh/profiles/web/suppliers/`。 |
|
|
58
|
+
| 模型不内置 | 供应商只实现差异化能力,模型拉取与缓存由核心统一管,不写死、不过时。 |
|
|
59
|
+
| 策略只写一次 | 组合回退、账号池(选号/冷却/禁用)、响应写入、凭证存储、积分持久化、模型管理都由核心提供。供应商 js 只对**单个账号**调一次上游并报告成败,不自己遍历账号、不维护冷却表、不落盘积分——否则每个插件都会长出一份互相不一致的实现,而核心也就无从判断「该不该换号」。 |
|
|
60
|
+
| 凭证单库 | `auths/credentials.sqlite`,供应商凭证不透明 blob,核心统一生命周期,干净可备份。 |
|
|
61
|
+
| 组合即模型 | 建好的组合自动带出为 DSH 模型目录里的 `router` provider 选项,设置 → 模型直接选组合名即可。 |
|
|
62
|
+
| 用量可观测 | 面板概览看板:周期切换、汇总卡、趋势折线、Top 榜、最近请求。 |
|
|
63
|
+
| Team 友好 | 按**会话身份**亲和选号:同会话固定同一连接(前缀缓存只写一份),不同会话尽量铺开到不同连接;多会话共用同号时缓存质量**按会话隔离**统计,不会互相拖累。 |
|
|
55
64
|
|
|
56
65
|
> 面板布局、组合 fallback、连接池/账号池、API key 管理都贴近
|
|
57
66
|
> [9router](https://github.com/decolua/9router),但按 DSH「一切皆插件」的方式
|
|
@@ -59,6 +68,35 @@ dsh plugin --profile web add dsh-router-core
|
|
|
59
68
|
>
|
|
60
69
|
> 供应商开发与接入规范见 [`docs/suppliers.md`](docs/suppliers.md)
|
|
61
70
|
> (契约 / 加载顺序 / 模型统一策略 / 内置供应商参考实现)。
|
|
71
|
+
> 选号/亲和/team 的完整设计与实测见
|
|
72
|
+
> [`docs/pool-sticky-block.md`](docs/pool-sticky-block.md)。
|
|
73
|
+
|
|
74
|
+
## Agent Team 支持
|
|
75
|
+
|
|
76
|
+
DSH 0.1.7 的**智能体团队**把任务拆给多个成员,**每个成员是独立会话**,一轮
|
|
77
|
+
team 测试就是多个会话交错发请求。dsh-router 原生适配这个场景:
|
|
78
|
+
|
|
79
|
+
- **真会话身份** —— 从宿主 `GenerateOptions.sessionId` 拿到权威会话 id
|
|
80
|
+
(adapter 转成内部头),不再靠猜。有 id 就按 id 亲和,精确、零撞车;
|
|
81
|
+
外部 OpenAI 客户端拿不到 id 时,回落前缀指纹兜底。
|
|
82
|
+
- **会话亲和选号** —— 同一会话的连续请求**永远落回同一个连接**,前缀在该
|
|
83
|
+
连接缓存里只写一份,缓存命中率稳定在 ~98%;新会话按游标分发,天然均衡。
|
|
84
|
+
- **fresh 会话优先铺开** —— 多个成员交替到达时,优先分配到**还没被别的会话
|
|
85
|
+
占用**的连接,各成员尽量独占一个连接;连接数不够时才回绕复用。
|
|
86
|
+
- **按会话隔离的缓存统计** —— 多会话共用同号时,**各会话的缓存质量分开统计**,
|
|
87
|
+
不会因为别的会话"踩"了同一个号就把这个号误判成"缓存坏了"从而集体降权
|
|
88
|
+
(消除了 team 场景下的「降权串」)。
|
|
89
|
+
- **底部命中指示** —— 输入框旁的「路由」徽章按**当前会话**显示最近一次命中
|
|
90
|
+
的连接/积分;点击展开卡片,点外部或 `Esc` 收起(与原生弹层一致)。
|
|
91
|
+
|
|
92
|
+
> 天花板:**连接数 < 并发成员数**时,多出来的成员必然共用连接 → 前缀缓存互相
|
|
93
|
+
> 驱逐,无法用选号手法消除(上游缓存空间是账号侧物理限制)。要彻底隔离只能
|
|
94
|
+
> **加连接**或**限制 team 并发成员数 ≤ 连接数**。详见
|
|
95
|
+
> [`docs/pool-sticky-block.md`](docs/pool-sticky-block.md) §11。
|
|
96
|
+
>
|
|
97
|
+
> 版本适配:「路由」徽章只在 **DSH ≥ 0.1.7** 出现(0.1.5 的对应座位渲染位置
|
|
98
|
+
> 不同);0.1.5 与 0.1.7 的兼容差异(设置注册、tool 消息形态)统一按宿主版本号
|
|
99
|
+
> 判定。
|
|
62
100
|
|
|
63
101
|
## 面板(设置 → 路由)
|
|
64
102
|
|
|
@@ -93,8 +131,7 @@ dsh plugin --profile web add dsh-router-core
|
|
|
93
131
|
- **链接池** — 账号列表(冷却/禁用/健康数/积分),支持删除;
|
|
94
132
|
- **加链接** — 按供应商能力弹出不同流程:URL 登录(生成链接 → 浏览器登录 → 回调)、
|
|
95
133
|
API key 弹窗(填名字 + key)、轮询登录(登录后自动取凭证);
|
|
96
|
-
- **签到** — 供应商实现了签到的才显示(如 codebuddy:每日 100 积分,连续第 7 天
|
|
97
|
-
1000)。核心遍历所有链接逐个签,汇总「N/M 成功 · X 今日已签」;上游「今日已
|
|
134
|
+
- **签到** — 供应商实现了签到的才显示(如 codebuddy:每日 100 积分,连续第 7 天1000)。核心遍历所有链接逐个签,汇总「N/M 成功 · X 今日已签」;上游「今日已
|
|
98
135
|
签到」按成功处理(幂等),账号额度或凭证失效会单独标出;
|
|
99
136
|
- **刷新** — 刷所有链接的积分,并跑一次最简会话探测该供应商是否还有活着的链接
|
|
100
137
|
(走真实对话路径 + 账号池回退,能分清是账号额度没了还是供应商真挂了);
|
|
@@ -131,24 +168,25 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
131
168
|
|
|
132
169
|
## 面板 API(`/router/api/*`,同源)
|
|
133
170
|
|
|
134
|
-
| 端点
|
|
135
|
-
|
|
136
|
-
| `/health`
|
|
137
|
-
| `/
|
|
138
|
-
| `/
|
|
139
|
-
| `/
|
|
140
|
-
| `/
|
|
141
|
-
| `/keys
|
|
142
|
-
| `/keys/
|
|
143
|
-
| `/
|
|
144
|
-
| `/
|
|
145
|
-
| `/
|
|
146
|
-
| `/stats
|
|
147
|
-
| `/stats/
|
|
148
|
-
| `/
|
|
149
|
-
| `/suppliers/:id/login
|
|
150
|
-
| `/suppliers/:id/
|
|
151
|
-
| `/suppliers/:id/models
|
|
171
|
+
| 端点 | 方法 | 说明 |
|
|
172
|
+
| ------------------------------- | --------- | ---------------------------------------------------------------------- |
|
|
173
|
+
| `/health` | GET | 供应商列表(含来源/能力)+ 宿主版本 `hostVersion` / 徽章支持位 `lastHitDock` |
|
|
174
|
+
| `/last-hit` | GET | 最近一次命中 `?session=<id>`(按会话取,缺省全局);供「路由」徽章 |
|
|
175
|
+
| `/status` | GET | 全部账号(含供应商 id) |
|
|
176
|
+
| `/models` | GET | 合并模型列表(已过滤禁用) |
|
|
177
|
+
| `/combos` | GET | 组合 fallback 链 |
|
|
178
|
+
| `/keys` | GET/POST | 密钥列表(含完整 key)/ 创建 `{name}` → 返回明文一次 |
|
|
179
|
+
| `/keys/toggle` | POST | `{id, isActive}` |
|
|
180
|
+
| `/keys/delete` | POST | `{id}` |
|
|
181
|
+
| `/settings` | GET/PATCH | `{requireApiKey}` |
|
|
182
|
+
| `/ext` | GET/PATCH | 扩展插件列表 + 开关 `{id, enabled}`(见下) |
|
|
183
|
+
| `/stats` | GET | 用量统计 `?period=today\|24h\|7d\|30d`(汇总 + Top 榜 + 最近请求 20 条) |
|
|
184
|
+
| `/stats/chart` | GET | 趋势图数据 `?period=…`(today/24h = 24 小时桶,7d/30d = 天桶) |
|
|
185
|
+
| `/stats/clear` | POST | 清空全部用量统计 |
|
|
186
|
+
| `/suppliers/:id/login` | POST | 生成登录链接 |
|
|
187
|
+
| `/suppliers/:id/login/callback` | POST | `{callbackUrl}` → 加账号 |
|
|
188
|
+
| `/suppliers/:id/models` | GET | 模型 + 启用状态 |
|
|
189
|
+
| `/suppliers/:id/models/toggle` | POST | `{id, enabled}` |
|
|
152
190
|
|
|
153
191
|
## 扩展插件(`router.ext`)
|
|
154
192
|
|
|
@@ -186,11 +224,14 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
186
224
|
├─ StatsTab 概览:用量看板(周期按钮组 + 汇总卡 + 折线趋势 + Top 榜 + 最近请求)
|
|
187
225
|
├─ SupplierDetail 供应商详情:链接池 + 加链接 + 可用模型
|
|
188
226
|
├─ EndpointTab 端点 URL + requireApiKey + 密钥管理
|
|
227
|
+
└─ LastHitDock 「路由」徽章(composer.dock 座位,≥0.1.7;按会话显示最近命中)
|
|
189
228
|
└─ fetch /router/api/* (同源,无 CORS)
|
|
190
229
|
└─ host 半(src/index.ts)
|
|
191
230
|
├─ /v1/models + /v1/chat/completions (OpenAI 兼容, KeysStore 鉴权)
|
|
192
231
|
│ └─ RouterAdapter(src/llm/adapter.ts) OpenAI SSE → DSH StreamChunk
|
|
193
232
|
│ (usage 经 toTokenUsage 转 DSH 契约,见 docs/suppliers.md)
|
|
233
|
+
│ (带 x-dsh-router-session 头:宿主 sessionId → 会话亲和)
|
|
234
|
+
├─ host-version(src/host-version.ts) 按宿主版本号适配 0.1.5 / 0.1.7
|
|
194
235
|
├─ KeysStore(src/keys.ts) 密钥库 + requireApiKey
|
|
195
236
|
└─ Router(路由器) → suppliers[]
|
|
196
237
|
├─ OpenCodeSupplier(lib/suppliers/opencode.js) 无账号直连(Zen 免费档,需 CLI 握手)
|
|
@@ -200,9 +241,9 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
200
241
|
```
|
|
201
242
|
|
|
202
243
|
- **供应商抽象**:可插拔 js 模块只提供**差异化能力**(`status/listModels/getAlias/chatOnce`
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
244
|
+
- 可选登录/签到/加 key);**策略与通用能力**(组合回退、账号池选号/冷却/禁用、
|
|
245
|
+
连接池排序、模型启用/自定义、别名、凭证、响应写入)由核心统一管。
|
|
246
|
+
`chatOnce(uid, req)` 一次只服务一个账号,返回成功/失败 + 语义状态,换号由核心决定。
|
|
206
247
|
- **供应商加载**(三来源,见 [`docs/suppliers.md`](docs/suppliers.md)):
|
|
207
248
|
1. 内置:`lib/suppliers/*.js`(随插件分发,如 opencode)
|
|
208
249
|
2. 用户:`~/.dsh/profiles/web/suppliers/*.js`
|
|
@@ -227,6 +268,8 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
227
268
|
|
|
228
269
|
## 前提
|
|
229
270
|
|
|
271
|
+
- **DSH 版本**:`0.1.5-rc.1` 及以上;插件内部按宿主版本号自动适配 0.1.5 / 0.1.7
|
|
272
|
+
差异(设置注册方式、tool 消息形态、底部徽章);「路由」徽章需 **≥ 0.1.7**;
|
|
230
273
|
- 凭证由 dsh-router 核心统一管(SQLite 库 `<dataDir>/auths/credentials.sqlite`);
|
|
231
274
|
- 供应商接入与开发见 [`docs/suppliers.md`](docs/suppliers.md);
|
|
232
275
|
- 重启 DSH 后 `/v1/*` 即生效;面板管理账号、模型与密钥。
|
|
@@ -255,3 +255,161 @@ pick(round-robin):
|
|
|
255
255
|
ponytail:会话标识目前靠前缀指纹推断,真要百分百准得等 DSH 在 `ChatRequest`
|
|
256
256
|
透传会话 id(需改契约,不做);指纹对「首条消息就分叉」的客户端(极少见)
|
|
257
257
|
会散得比会话更细,退化为均衡分发,无害。
|
|
258
|
+
|
|
259
|
+
## 10. 统一模型:会话亲和选号(2026-09-23 落地,废除策略旋钮)
|
|
260
|
+
|
|
261
|
+
### 10.0 结论先行:没有「策略」
|
|
262
|
+
|
|
263
|
+
`fallback`(永远取健康池第一个)与 `round-robin`(轮转)**都是没有会话身份
|
|
264
|
+
时代的产物**。有了会话亲和后,二者都错:
|
|
265
|
+
|
|
266
|
+
- `fallback` 让**所有**会话挤同一个号 → team/多会话下必互踩(§2.1 实测 3.8%)。
|
|
267
|
+
- `round-robin` 的"轮转"语义与亲和"同会话永不换号"直接冲突。
|
|
268
|
+
|
|
269
|
+
**正确模型只有一条,无需旋钮:**
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
选号 = f(会话身份, 该 model 下的健康号集合)
|
|
273
|
+
1. 会话身份 → 亲和键;该键绑定的号仍健康 → 用它
|
|
274
|
+
2. 无绑定(新会话)→ 从健康池挑一个(避开降权号),记下绑定
|
|
275
|
+
3. 无身份(指纹算不出)→ 块轮询兜底(§2),这不是"策略",是无身份时的替身
|
|
276
|
+
4. 该 model 下全不健康 → 交给组合的降级链,换下一个 model(§10.2)
|
|
277
|
+
|
|
278
|
+
号数 ≥ 并发会话数 → 每个会话独占一个号,缓存空间互不驱逐
|
|
279
|
+
号数 < 并发会话数 → 必然有会话共用号 → 共用者互踩,不可消除(见 10.3 天花板)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 10.1 关键认知边界:会话 id = 选号稳定性,不是缓存隔离
|
|
283
|
+
|
|
284
|
+
**必须分清**,否则会做出错误期望:
|
|
285
|
+
|
|
286
|
+
| 能力 | 会话 id 能带来吗 |
|
|
287
|
+
|---|---|
|
|
288
|
+
| 选号时认出"这是哪个会话",避免误绑 | ✅ 能 |
|
|
289
|
+
| 保证同一会话永远绑同一个号(不漂移) | ✅ 能 |
|
|
290
|
+
| **同一个号上,不同会话的缓存各占一块、互不驱逐** | ❌ **不能** |
|
|
291
|
+
|
|
292
|
+
上游 prompt cache 是 `per-account × per-conversation` 命名空间,**同一个号
|
|
293
|
+
上的缓存空间是共享的、会被互相驱逐的**。session id 只是"货物标签",帮上游
|
|
294
|
+
知道哪批是哪批;**仓库满了照样扔货**,不会因为标签不同就给每批货独立仓位。
|
|
295
|
+
|
|
296
|
+
证据:§2.1 实测那 3 个并发会话本就走不同的对话内容(等价于不同 id 语义),
|
|
297
|
+
照样只命中 3.8% —— **驱逐是账号侧物理限制,不归 id 管**。
|
|
298
|
+
|
|
299
|
+
> 所以「号不够就共用号 + 靠会话 id 分开缓存」**前半成立(选得准)、
|
|
300
|
+
> 后半不成立(缓存仍互踩)**。
|
|
301
|
+
|
|
302
|
+
### 10.2 组合 = 降级链(不是轮转)
|
|
303
|
+
|
|
304
|
+
组合(一组模型)的语义统一为:**从第一个模型起顺序尝试,谁先服务成功就
|
|
305
|
+
停**。原 `round-robin` 组合(换模型轮转)**删除** —— 因为换模型 = 换供应商
|
|
306
|
+
= 整段前缀缓存作废,与亲和的价值直接冲突。
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
组合选模型: start = 0 恒定(删掉 rrCursors 游标与 strategy 字段)
|
|
310
|
+
对 combo.models[i] 依次:
|
|
311
|
+
chatWithModel(该模型的降级链) → 成功即停
|
|
312
|
+
瞬时故障 → 等 TRANSIENT_SETTLE_MS 再试下一个(§已有)
|
|
313
|
+
全失败 → 503 combo: all models unavailable
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
选号(§10.0)与选模型(本节)是**正交的两条轴**:前者在"单个 model 的账号池"
|
|
317
|
+
内做亲和,后者在"多个 model"间做降级。二者的共同点:**都只按"身份 + 健康性"
|
|
318
|
+
决策,不按"轮转"**。
|
|
319
|
+
|
|
320
|
+
### 10.3 天花板(ponytail)
|
|
321
|
+
|
|
322
|
+
1. **号数 < 并发会话数时必然互踩** —— 物理限制(缓存空间有限),只能
|
|
323
|
+
**加号**或**限制 team 并发成员数 ≤ 号数**,任何选号手法都消除不了。
|
|
324
|
+
2. ~~**会话身份目前靠前缀指纹推断**~~ —— **2026-09-23 已落地真会话 id**(见 §11):
|
|
325
|
+
宿主 `GenerateOptions.sessionId` → adapter 转成 `x-dsh-router-session` 头 →
|
|
326
|
+
核心按会话亲和。指纹降为**无会话身份时的兜底**(外部 OpenAI 客户端)。
|
|
327
|
+
3. **上游 TTL 仍未知**(§8):长会话若间隔超过 TTL,miss 是上游行为,
|
|
328
|
+
不误判成选号问题。
|
|
329
|
+
|
|
330
|
+
### 10.4 废除的旧配置
|
|
331
|
+
|
|
332
|
+
| 旧字段 | 处置 |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `supplier-config.json` 的 `poolStrategy` | **静默忽略**:读盘不解析,写盘不产出;旧文件不报错,下次保存自动瘦身 |
|
|
335
|
+
| `combos.json` 的 `strategy` | 同上 |
|
|
336
|
+
| `GET/POST /suppliers/:id/pool/strategy` 路由 | 删除 |
|
|
337
|
+
| 面板「连接池策略」「组合策略」下拉 | 删除(选项无意义则不该占位) |
|
|
338
|
+
|
|
339
|
+
### 10.5 为什么这不是"回退/轮询"的合并,而是概念删除
|
|
340
|
+
|
|
341
|
+
旧的两策略试图用**一个旋钮**回答两个不同的问题("要不要均衡" vs "要不要
|
|
342
|
+
粘缓存"),而这两个问题在亲和下**同时被回答了**:亲和天然均衡(新会话按
|
|
343
|
+
游标分发)+ 天然粘缓存(同会话固定号)。旋钮消失不是"合并了两个选项",
|
|
344
|
+
是**承认这个问题本身提错了** —— 不存在"要不要均衡"的选择,只有"按身份
|
|
345
|
+
选号"这一个正确答案。
|
|
346
|
+
|
|
347
|
+
## 11. Agent Team 支持(2026-09-23 落地)
|
|
348
|
+
|
|
349
|
+
DSH 0.1.7 的**智能体团队**(agent team)把一个任务拆给多个成员,**每个成员
|
|
350
|
+
是一个独立会话**(TeamId = Lead 的 SessionId;成员各自独立 `sessionId`)。
|
|
351
|
+
这正好把 §10 的"多并发会话"从理论变成日常 —— 一轮 team 测试就是 3 个会话
|
|
352
|
+
交错发请求。本节记录为此做的三件事。
|
|
353
|
+
|
|
354
|
+
### 11.1 真会话 id 通道(替代前缀指纹)
|
|
355
|
+
|
|
356
|
+
宿主 `dsh-llm` 的 `GenerateOptions.sessionId` 是**权威会话身份**。链路:
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
dsh-llm GenerateOptions.sessionId
|
|
360
|
+
→ RouterAdapter 发请求时带 `x-dsh-router-session: <sessionId>`
|
|
361
|
+
→ 核心 /v1 读进 ChatRequest.session
|
|
362
|
+
→ 选号: sessionFingerprint(session, messages) = session!=空 ? `sid:<session>` : 前缀指纹
|
|
363
|
+
→ 落账: UsageRecord.session
|
|
364
|
+
→ 面板: /router/api/last-hit?session=<id> 按当前会话取「最近命中」
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
要点:
|
|
368
|
+
- **有会话 id 时以它为主键**("sid:xxx"),精确、零撞车;指纹只在**拿不到
|
|
369
|
+
会话 id 时**(外部 OpenAI 客户端)兜底。`sessionFingerprint` 就这一行分叉。
|
|
370
|
+
- 会话 id 是**选号稳定性**凭证,**不是缓存隔离**承诺 —— 见 §10.1,别混淆。
|
|
371
|
+
|
|
372
|
+
### 11.2 fresh 会话优先「铺开」到空闲号
|
|
373
|
+
|
|
374
|
+
三个 fresh 成员交替到达时,若只按全局游标分发,会**回绕撞号**(实测:
|
|
375
|
+
Lead 与 T1 撞同一个号,T2 独占)。改为**占用感知分配**(`allocSession`):
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
新会话分配:
|
|
379
|
+
1. 先算「本 model 下没有被其它会话绑定的空闲号」(freeUids)
|
|
380
|
+
2. 空闲号非空 → 从空闲号里按游标挑 → 各会话尽量独占一个号
|
|
381
|
+
3. 空闲号用尽 → 才回绕到已占用的号复用(复用无害,见 §11.3)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
"占用"= 该 model 的亲和绑定表里,**其它**指纹当前指向的 uid;同一会话自己
|
|
385
|
+
的旧绑定不算占用(它改绑不冲突)。号数 ≥ 会话数 → 每个会话独占;号数 <
|
|
386
|
+
会话数 → 必然复用(物理限制,§10.3)。
|
|
387
|
+
|
|
388
|
+
### 11.3 缓存质量样本按会话隔离(消除「降权串」)
|
|
389
|
+
|
|
390
|
+
**症状**(实测):team 3 个会话全落到同一个号,且该号看起来"缓存坏了"。
|
|
391
|
+
|
|
392
|
+
**根因**:`samples`(缓存质量样本)与 `demoted`(降权)的键原为
|
|
393
|
+
`(supplier, model, uid)`,**没有会话维度**。多会话共用同号时,各自前缀在
|
|
394
|
+
该号缓存里互相驱逐 → 命中率被**混在一起算** → 跌破阈值 → 误判"这个号缓存
|
|
395
|
+
坏了" → 降权 → 所有会话被挤到剩余号 → 那个号又被同样机制打低 → **降权串**。
|
|
396
|
+
|
|
397
|
+
**修法**:键改为 `(supplier, model, uid[, session])`(`sampleKey`)。有会话
|
|
398
|
+
身份时**按会话隔离统计**,降权只反映「这个会话在这个号上真的反复不命中」,
|
|
399
|
+
不因别的会话共用同号而被牵连。无会话身份(外部客户端走无指纹路径)保持
|
|
400
|
+
`(model, uid)` 原语义。
|
|
401
|
+
|
|
402
|
+
回归测试:`account-pool.test.ts` 的「多会话共用同号互踩 → 不误伤降权」——
|
|
403
|
+
两会话各在该号服务 24 次、命中率 50%,旧逻辑立刻误降权,新逻辑不降;
|
|
404
|
+
同时锁死"单会话自己真攒满 48 次全 miss 仍会被降权"(隔离 ≠ 免疫)。
|
|
405
|
+
|
|
406
|
+
### 11.4 天花板(ponytail)
|
|
407
|
+
|
|
408
|
+
- **隔离让降权更迟钝**:会话越分散,单个会话攒满样本越慢 → 降权触发越晚。
|
|
409
|
+
这是**想要的**(宁可慢也不要误伤)。要更快感知真坏号,调小 `BLOCK_MAX`
|
|
410
|
+
(当前 48)。
|
|
411
|
+
- **号数 < 成员数仍互踩**:隔离只是**不误判**,不改变"缓存空间共享"这个
|
|
412
|
+
物理事实(§10.1/§10.3)。真要不互踩只能加号或减少并发成员。
|
|
413
|
+
- **每会话一份样本 → 内存随会话数增长**:`samples` 按会话分叉,长跑多会话
|
|
414
|
+
场景条目变多。当前无上限(随进程存亡),量大时再加 LRU(对齐 `AFFINITY_MAX`
|
|
415
|
+
的做法)。
|