dsh-multi-router 0.6.0 → 0.6.2
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 +16 -0
- package/README.md +23 -6
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/cordis.patch.yml +2 -2
- package/lib/accounts.js +2 -2
- package/lib/api-service.js +3 -3
- package/lib/{chunk-LLDJVBTQ.js → chunk-3O3L3526.js} +3 -3
- package/lib/{chunk-OCMOMSB4.js → chunk-4EO5V6FW.js} +128 -42
- package/lib/chunk-4EO5V6FW.js.map +7 -0
- package/lib/{chunk-ROZINFTV.js → chunk-5KYJADJ4.js} +49 -6
- package/lib/chunk-5KYJADJ4.js.map +7 -0
- package/lib/{chunk-WVNY4LSL.js → chunk-5R3IOPSF.js} +60 -13
- package/lib/chunk-5R3IOPSF.js.map +7 -0
- package/lib/{chunk-NSKCFVHP.js → chunk-74WQLVJO.js} +15 -7
- package/lib/chunk-74WQLVJO.js.map +7 -0
- package/lib/{chunk-JUOESKAK.js → chunk-KGVTRS5K.js} +2 -2
- package/lib/{chunk-SNEZMYNU.js → chunk-Q34JSMGN.js} +3 -3
- package/lib/cli.js +4 -4
- package/lib/client.js +125 -40
- package/lib/client.js.map +2 -2
- package/lib/clients.js +6 -3
- package/lib/{host-v0.6.0.js → host-v0.6.2.js} +8 -7
- package/lib/host.js +8 -7
- package/lib/index.js +8 -7
- package/lib/index.js.map +2 -2
- package/lib/manager.js +6 -6
- package/lib/router.js +2 -2
- package/lib/shim.js +3 -3
- package/package.json +1 -1
- package/lib/chunk-NSKCFVHP.js.map +0 -7
- package/lib/chunk-OCMOMSB4.js.map +0 -7
- package/lib/chunk-ROZINFTV.js.map +0 -7
- package/lib/chunk-WVNY4LSL.js.map +0 -7
- /package/lib/{chunk-LLDJVBTQ.js.map → chunk-3O3L3526.js.map} +0 -0
- /package/lib/{chunk-JUOESKAK.js.map → chunk-KGVTRS5K.js.map} +0 -0
- /package/lib/{chunk-SNEZMYNU.js.map → chunk-Q34JSMGN.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。版本号从 `0.1` 起以功能迭代为主,接口在 `1.0` 前可能变化。
|
|
4
4
|
|
|
5
|
+
## 0.6.2
|
|
6
|
+
|
|
7
|
+
- 修复旧用户升级后缺少新预制路由:启用或热加载时自动补齐 Qwen3.8-Max、Qwen3.8-Flash 与 Hy4 Preview,更新后直接出现在路由列表与模型选择器,无需手动补齐或重启。
|
|
8
|
+
- 记录已处理的预配置;重复启用不重复写入,用户改名、隐藏、改规则及旧预设的删除选择保留。新增预设处理后再次删除,不会在下一次启动恢复。
|
|
9
|
+
- 预配置处理记录随配置迁移;合并保留两端记录。升级写入使用现有文件锁与原子保存,损坏配置不自动重置。
|
|
10
|
+
|
|
11
|
+
## 0.6.1
|
|
12
|
+
|
|
13
|
+
- 保留三家模型的默认 / 最大上下文和可选档位,原始模型、预制路由及动态免费模型默认最大,不再统一限制 200K;旧隐式默认自动迁移,显式自定义预算保留。
|
|
14
|
+
- 模型设置集中管理名称、上下文预算、图片开关和默认推理档位;免费模型逐项选择预算。原始模型与路由独立设置,随配置导入导出。
|
|
15
|
+
- 路由跳过容量不足的账号,不截断会话或静默降低容量;Qoder 每次请求使用能容纳预算的真实档位,并发选择不污染共享目录。
|
|
16
|
+
- 增加 Qwen3.8-Max、Qwen3.8-Flash 预配置,使用已核对的 Trae / Qoder 模型 ID;补齐预配置不覆盖已有路由。
|
|
17
|
+
- 增加 Hy4 Preview 预配置,精确绑定 WorkBuddy 的 `hy4-preview`;容量与推理模式跟随该渠道目录,支持多个 WorkBuddy 账号切换。
|
|
18
|
+
- 增加手动短请求连通性测试,有超时、间隔与错误流检查,不自动发送容量探测。显示最大输出与完整推理模式,只有声明支持的模型提供图片与关闭思考选项。
|
|
19
|
+
- 对比 dingminhua 的 WorkBuddy / Trae 插件并记录设计取舍,更新模型设置的合成数据截图。
|
|
20
|
+
|
|
5
21
|
## 0.6.0
|
|
6
22
|
|
|
7
23
|
- 新增左侧底部 Token 用量概览,展示今日与累计 Token,点击直达插件详情;默认显示,可在版本号旁关闭并保存到配置。仅读取本机缓存统计,每分钟更新,隐藏窗口暂停,不额外查询供应商。
|
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
一个插件内置 WorkBuddy、Trae、Qoder 的认证、模型发现和聊天传输,提供三家的多账号池及集中路由。运行不依赖原来三个 DSH 插件;三家代码随安装包一起打包。基于 MIT 上游,固定版本见 `upstream-lock.json`,许可见 `THIRD_PARTY_NOTICES.md`。
|
|
8
8
|
|
|
9
|
-
版本:0.6.
|
|
9
|
+
版本:0.6.2。适配 DSH Desktop / Web 0.2.0-rc.2,Node.js ^22.19.0 或 >=24。
|
|
10
10
|
|
|
11
11
|
## 在 DSH 里管理
|
|
12
12
|
|
|
13
|
-
打开 **插件 → dsh-multi-router
|
|
13
|
+
打开 **插件 → dsh-multi-router 的配置页**。默认进入总览,九个页签:
|
|
14
14
|
|
|
15
15
|
- **总览与用量**:今日/累计请求与 Token、近 7 日趋势、最近请求、三家平台余额与刷新时间。平台余额后台每 5 分钟查询,面板每分钟读取本机缓存,隐藏页面暂停轮询,可关闭自动刷新;手动刷新平台用量会立即查询。
|
|
16
16
|
- **Token 明细**:每次上游调用的时间、渠道、账号、路由、实际模型、输入/输出/总 Token 和结果;支持今日、近 3 日、近 7 日、近 30 日及自定义日期范围(北京时间自然日),并按账号、路由、结果筛选,每页 50 条。缓存输入和推理输出在平台返回时展示,不重复计入总量。默认滚动保留最近 30 × 24 小时,可设置 1–365 天,并随配置迁移。同时展示模型用时、工具调用用时、首 Token 平均(TTFT)和输出速度(TPS),可展开单次性能。DSH 步骤采用会话统计相同口径;TPS 按有效输出 Token / 解码秒数加权计算。性能汇总只包含成功调用,随当前筛选变化;旧记录缺少的分项显示「—」。
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
- **积分签到**:按账号设置每日启动签到或定时签到(北京时间),保留每日执行记录、领取意图与设备限制,避免重复请求。
|
|
19
19
|
- **API 服务**:默认关闭,按需提供 OpenAI 兼容的模型列表和聊天接口,支持流式 / 普通 JSON、工具调用与推理参数;复用现有账号池及路由,Key 独立管理,API 用量可在 Token 明细按来源筛选。
|
|
20
20
|
- **插件更新**:仅检查用户安装的 GitHub / npm 插件,包含停用插件;每 24 小时自动检查,在 DSH 任意页面弹窗提醒新版本。同一更新点击关闭或「稍后处理」后不重复提醒,更高版本 / 新提交再次提醒。可在弹窗查看并手动更新;不自动安装。检查失败不冒充发现新版本,内置组件不检查、不展示。
|
|
21
|
-
-
|
|
22
|
-
- **路由规则**:Free
|
|
21
|
+
- **模型管理**:原始模型默认隐藏,只展示统一路由;提供所有渠道「全部展示」「全部隐藏(仅路由)」,作用于现有和以后发现的模型。可搜索、获取真实模型目录,修改显示名与单模型展示开关,每家独立保存。显示实际上下文、最大上下文、输出上限和完整推理档位;「设置」可调整上下文预算、图片输入及默认推理等级,「测试」手动发送短请求检查连通性(正常计费,不代表满上下文测试)。
|
|
22
|
+
- **路由规则**:Free 免费路由排在首位,显示各账号当前免费模型与目录检查时间,支持立即检查。列表显示路由名称、切换策略和目标;新建/编辑打开二级弹窗。「添加模型预配置」可逐个添加或补齐全部预配置(Qwen3.8-Max / Flash 连接已核对的 Trae、Qoder 渠道),不覆盖已有同 ID 路由。模型可从下拉列表选择,上下文默认采用目标渠道已发现的最大容量,支持手动设置较小预算;候选 ID 放在高级选项中。
|
|
23
23
|
|
|
24
24
|
- **配置迁移**:导入 / 导出设置,或使用密码加密迁移账号登录凭证。
|
|
25
25
|
|
|
@@ -31,8 +31,20 @@
|
|
|
31
31
|
|
|
32
32
|
隐藏只影响模型选择器,已有会话及路由仍可使用隐藏模型;停用账号会将其从新请求的候选池中排除。移出账号池保留独立凭证文件,且禁止移除仍被路由明确绑定的账号。
|
|
33
33
|
|
|
34
|
+
### 上下文设置
|
|
35
|
+
|
|
36
|
+
原始模型、预制路由及动态免费模型默认采用各渠道已声明的最大上下文,保留手动调低预算的选择。原始模型在「模型管理 → 设置」调整;普通路由在「编辑路由 → 上下文容量」调整;免费模型在「管理免费模型」逐项调整。旧版隐式 200K 默认会迁移为最大容量,显式自定义预算保留。
|
|
37
|
+
|
|
38
|
+
路由只切换到能够容纳当前预算的渠道,不截断历史、不静默降低容量。Qoder 请求同步选择真实的上下文档位,且不同并发路由互不影响。最大窗口来自渠道目录,账号额度、限流或请求体限制仍可能影响实际可用长度;短请求测试不验证满窗口。
|
|
39
|
+
|
|
40
|
+
原始模型设置和路由预算独立保存。上下文、图片、默认推理及免费模型设置均随配置迁移。
|
|
41
|
+
|
|
42
|
+
两个参考插件的源码对比与本次处理方式见 [参考对比](docs/reference-comparison.md)。
|
|
43
|
+
|
|
34
44
|
### 界面预览
|
|
35
45
|
|
|
46
|
+

|
|
47
|
+
|
|
36
48
|
以下主要页面已更新为 **v0.5.9 实际面板 + 合成演示数据**。账号名称已替换,不包含真实手机号、邮箱、身份 ID、设备标识、登录凭证或会话内容;余额、用量和性能均为演示数值。
|
|
37
49
|
|
|
38
50
|
| 总览与用量 | Token 明细与调用性能 |
|
|
@@ -300,9 +312,9 @@ node lib/cli.js remove --id wb1
|
|
|
300
312
|
|
|
301
313
|
`modelVisibility` 按供应商保存新发现模型的展示默认值,省略时为 false。单模型 `visible` 优先;批量操作同时更新渠道默认值和已有单模型覆盖项,保留模型名称和路由配置。完整配置导入 / 导出也保留该默认值。
|
|
302
314
|
|
|
303
|
-
##
|
|
315
|
+
## 模型预配置
|
|
304
316
|
|
|
305
|
-
|
|
317
|
+
Free 自动免费路由排在首位,以下普通预配置按表中渠道顺序切换,使用该渠道全部启用账号,默认临期优先、目录声明的最大上下文 / 32K 最大输出。Flash 仍采用用户已允许的三家 Flash 兼容切换;其他预配置精确绑定对应版本,不混用 GLM FlashX 等变体。空白渠道不参与该路由。目录或账号权限变化可能使目标暂不可用,预配置弹窗显示当前已发现可用渠道数。
|
|
306
318
|
|
|
307
319
|
| 路由调用 ID | WorkBuddy 目标 ID | Trae 目标 ID | Qoder 目标 ID |
|
|
308
320
|
| --- | --- | --- | --- |
|
|
@@ -313,9 +325,14 @@ node lib/cli.js remove --id wb1
|
|
|
313
325
|
| kimi-k3 | kimi-k3-1 | kimi-k3 | kmodel_latest |
|
|
314
326
|
| kimi-k2.8-preview | kimi-k2.8-preview | kimi-k2.8-preview | kmodel |
|
|
315
327
|
| minimax-m2.7 | minimax-m2.7 | minimax-m2.7 | mmodel |
|
|
328
|
+
| qwen3.8-max | — | qwen3.8-max | qmodel_38max |
|
|
329
|
+
| qwen3.8-flash | — | qwen3.8-flash | qfmodel |
|
|
330
|
+
| hy4-preview | hy4-preview | — | — |
|
|
316
331
|
|
|
317
332
|
路由内部 ID 是 DSH 查找路由所需的标识,由插件维护;用户在面板只配置路由名称。目标模型 ID 决定实际调用哪个模型。上表为预配置的稳定内部标识,新建路由自动生成独立标识。GLM-5.2 已移出预配置,原始渠道模型不受影响。
|
|
318
333
|
|
|
334
|
+
从 v0.6.2 起,升级后的首次启用或热加载自动补齐新引入的预配置,无需重启或手动点击。首次迁移补齐 Qwen3.8-Max / Flash 与 Hy4 Preview,已有同 ID 路由的名称、显隐和规则保持原样,旧预设中已移除的路由不会恢复。已处理的预设会记录在配置中,此后主动删除也不会在启动时恢复;需要恢复时可在「添加预配置」手动添加。记录随配置导入导出迁移。
|
|
335
|
+
|
|
319
336
|
## Free 动态免费模型(0.3.6)
|
|
320
337
|
|
|
321
338
|
预配置内置 free 管理组,名称「Free · 动态免费模型」,排在预配置与路由管理列表首位。按确切的渠道与模型 ID 自动创建各模型的免费路由,DSH 模型列表分别展示「Free · 模型名 · 渠道」,排在普通预配置前。同一渠道同一模型的多个免费账号归为一个路由,不按展示名称模糊合并版本,也不把不同模型混为一个池。原 free 内部标识仅为旧会话保留,默认不在选择器展示。
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
- 使用或再分发本项目时,你必须**同时**遵守本项目 [`LICENSE`](./LICENSE) 与下列三个上游项目的 MIT 许可要求。
|
|
12
12
|
- 上游项目的维护、更新与可用性不由本项目负责。
|
|
13
13
|
|
|
14
|
+
## 设计参考(未包含代码)
|
|
15
|
+
|
|
16
|
+
模型管理交互参考 [dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy) 与 [dingminhua/dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae) 的上下文预算、能力展示和测试反馈设计。本次管理界面与上下文路由逻辑由本项目独立实现,未复制这两个参考项目的新源码;核对的 commit 和差异见 [参考对比](docs/reference-comparison.md)。内置的 Trae 固定版本及其许可仍按下文履行。
|
|
17
|
+
|
|
14
18
|
## 本地集成改动 / Local Integration Changes
|
|
15
19
|
|
|
16
20
|
- WorkBuddy 的共享适配器额外透传集成后 provider 目录中**显式声明**的 Minimal 推理模式;WorkBuddy 原有的模型发现逻辑与 provider 专有的档位词表未作改动。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- insert:
|
|
2
|
-
- id: llm-multi-router-v2-0.6.
|
|
3
|
-
name: ./lib/host-v0.6.
|
|
2
|
+
- id: llm-multi-router-v2-0.6.2
|
|
3
|
+
name: ./lib/host-v0.6.2.js
|
package/lib/accounts.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
localAccountKey,
|
|
5
5
|
normalizeLocalAccountKey,
|
|
6
6
|
readPortableCredential
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KGVTRS5K.js";
|
|
8
8
|
import "./chunk-OK2T2UAZ.js";
|
|
9
9
|
import "./chunk-TEZ2FMEO.js";
|
|
10
10
|
import "./chunk-S5KMFAQM.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-5R3IOPSF.js";
|
|
12
12
|
export {
|
|
13
13
|
discoverLocalAccounts,
|
|
14
14
|
importAccount,
|
package/lib/api-service.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiService
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Q34JSMGN.js";
|
|
4
4
|
import "./chunk-4V52V5GO.js";
|
|
5
5
|
import "./chunk-DNQPZMPF.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-5KYJADJ4.js";
|
|
7
|
+
import "./chunk-5R3IOPSF.js";
|
|
8
8
|
export {
|
|
9
9
|
ApiService
|
|
10
10
|
};
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
} from "./chunk-DNQPZMPF.js";
|
|
5
5
|
import {
|
|
6
6
|
RouteError
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5KYJADJ4.js";
|
|
8
8
|
import {
|
|
9
9
|
REASONING_EFFORTS
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5R3IOPSF.js";
|
|
11
11
|
|
|
12
12
|
// src/shim.mjs
|
|
13
13
|
import { createServer } from "node:http";
|
|
@@ -117,4 +117,4 @@ function createShim(router, { modelPrefix = "" } = {}) {
|
|
|
117
117
|
export {
|
|
118
118
|
createShim
|
|
119
119
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-3O3L3526.js.map
|
|
@@ -6,44 +6,45 @@ import {
|
|
|
6
6
|
refreshWorkBuddyCheckinCredential,
|
|
7
7
|
supportsCheckin,
|
|
8
8
|
workbuddyToken
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-74WQLVJO.js";
|
|
10
10
|
import {
|
|
11
11
|
discoverLocalAccounts,
|
|
12
12
|
importAccount,
|
|
13
13
|
localAccountKey,
|
|
14
14
|
normalizeLocalAccountKey,
|
|
15
15
|
readPortableCredential
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-KGVTRS5K.js";
|
|
17
17
|
import {
|
|
18
18
|
modelWithCurrentPromotion
|
|
19
19
|
} from "./chunk-TEZ2FMEO.js";
|
|
20
20
|
import {
|
|
21
21
|
ApiService
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-Q34JSMGN.js";
|
|
23
23
|
import {
|
|
24
24
|
hostIsLoopback
|
|
25
25
|
} from "./chunk-DNQPZMPF.js";
|
|
26
26
|
import {
|
|
27
27
|
Telemetry,
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
contextInfo,
|
|
29
|
+
matchModel,
|
|
30
|
+
resolveContext
|
|
31
|
+
} from "./chunk-5KYJADJ4.js";
|
|
30
32
|
import {
|
|
31
33
|
PROVIDERS,
|
|
32
34
|
PROVIDER_NAMES,
|
|
33
35
|
ROUTE_PRESETS,
|
|
34
36
|
beijingDay,
|
|
35
37
|
isFreeModel,
|
|
36
|
-
reasoningModes,
|
|
37
38
|
routeReasoning,
|
|
38
39
|
validateConfig,
|
|
39
40
|
writePrivateJson
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-5R3IOPSF.js";
|
|
41
42
|
|
|
42
43
|
// src/manager.mjs
|
|
43
44
|
import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
44
45
|
|
|
45
46
|
// package.json
|
|
46
|
-
var version = "0.6.
|
|
47
|
+
var version = "0.6.2";
|
|
47
48
|
|
|
48
49
|
// src/manager.mjs
|
|
49
50
|
import { readFile as readFile4, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
|
|
@@ -105,14 +106,14 @@ var derive = promisify(scrypt);
|
|
|
105
106
|
var FORMAT = "dsh-multi-router";
|
|
106
107
|
var MAX_BYTES = 512 * 1024;
|
|
107
108
|
var pick = (value, keys) => Object.fromEntries(keys.filter((k) => Object.hasOwn(value ?? {}, k)).map((k) => [k, value[k]]));
|
|
108
|
-
var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays", "checkinSchedule", "sidebarUsage"];
|
|
109
|
+
var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays", "checkinSchedule", "sidebarUsage", "seenRoutePresets"];
|
|
109
110
|
function settings(config) {
|
|
110
111
|
return {
|
|
111
112
|
...pick(config, options),
|
|
112
|
-
models: Object.fromEntries(Object.entries(config.models ?? {}).map(([p, rows]) => [p, Object.fromEntries(Object.entries(rows).map(([id, pref]) => [id, pick(pref, ["name", "visible"])]))])),
|
|
113
|
+
models: Object.fromEntries(Object.entries(config.models ?? {}).map(([p, rows]) => [p, Object.fromEntries(Object.entries(rows).map(([id, pref]) => [id, pick(pref, ["name", "visible", "contextWindow", "supportsImages", "reasoningEffort"])]))])),
|
|
113
114
|
accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled", "autoCheckin"])),
|
|
114
115
|
routes: Object.fromEntries(Object.entries(config.routes).map(([id, r]) => [id, {
|
|
115
|
-
...pick(r, ["name", "visible", "strategy", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
|
|
116
|
+
...pick(r, ["name", "visible", "strategy", "contextMode", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
|
|
116
117
|
targets: r.targets.map((t) => pick(t, ["provider", "account", "model", "models", "weight", "tokenBudget", "creditBudget"]))
|
|
117
118
|
}]))
|
|
118
119
|
};
|
|
@@ -230,6 +231,7 @@ async function prepareMigration(manager, raw, portable, mode) {
|
|
|
230
231
|
const next = mode === "replace" ? { ...portable.config, accounts: imported } : {
|
|
231
232
|
...raw,
|
|
232
233
|
...pick(portable.config, options),
|
|
234
|
+
...raw.seenRoutePresets || portable.config.seenRoutePresets ? { seenRoutePresets: [.../* @__PURE__ */ new Set([...raw.seenRoutePresets ?? [], ...portable.config.seenRoutePresets ?? []])] } : {},
|
|
233
235
|
accounts: [...raw.accounts.map((a) => imported.find((b) => b.id === a.id) ?? a), ...imported.filter((a) => !raw.accounts.some((b) => b.id === a.id))],
|
|
234
236
|
routes: { ...raw.routes, ...portable.config.routes },
|
|
235
237
|
models: Object.fromEntries([.../* @__PURE__ */ new Set([...Object.keys(raw.models ?? {}), ...Object.keys(portable.config.models)])].map((p) => [p, { ...raw.models?.[p], ...portable.config.models[p] }]))
|
|
@@ -1033,8 +1035,8 @@ function modelInfo(row) {
|
|
|
1033
1035
|
return {
|
|
1034
1036
|
id: row.id,
|
|
1035
1037
|
name: row.name || row.id,
|
|
1036
|
-
|
|
1037
|
-
maxTokens: Math.min(finite(row.maxTokens, 32e3),
|
|
1038
|
+
...contextInfo(row),
|
|
1039
|
+
maxTokens: Math.min(finite(row.maxTokens, 32e3), contextInfo(row).maxContextWindow),
|
|
1038
1040
|
supportsImages: row.supportsImages === true || row.multimodal === true || row.input?.includes("image") === true,
|
|
1039
1041
|
...reasoning ? { reasoning } : {},
|
|
1040
1042
|
...billing ? { billing } : {},
|
|
@@ -1085,7 +1087,7 @@ var Manager = class {
|
|
|
1085
1087
|
for (const account of this.config.accounts) {
|
|
1086
1088
|
const item = saved[account.id];
|
|
1087
1089
|
if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
|
|
1088
|
-
this.catalogs.set(account.id, { ...item, models: item.models.filter((m) => typeof m.id === "string" && m.id.length > 0).map(modelInfo) });
|
|
1090
|
+
this.catalogs.set(account.id, { ...item, ...item.contextVersion !== 1 ? { fetchedAt: 0 } : {}, models: item.models.filter((m) => typeof m.id === "string" && m.id.length > 0).map(modelInfo) });
|
|
1089
1091
|
}
|
|
1090
1092
|
}
|
|
1091
1093
|
} catch {
|
|
@@ -1284,16 +1286,25 @@ var Manager = class {
|
|
|
1284
1286
|
const id = `free_${createHash("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
|
|
1285
1287
|
if (!groups.has(id)) groups.set(id, { id, models: [], targets: [], accountNames: [], modelName: model.name, provider: account.provider, modelId: model.id });
|
|
1286
1288
|
const group = groups.get(id);
|
|
1287
|
-
group.models.push({ model, provider: account.provider });
|
|
1289
|
+
group.models.push({ model, provider: account.provider, accountId: account.id });
|
|
1288
1290
|
group.targets.push({ provider: account.provider, account: account.id, model: model.id });
|
|
1289
1291
|
group.accountNames.push(account.name ?? account.id);
|
|
1290
1292
|
}
|
|
1291
1293
|
}
|
|
1292
1294
|
return [...groups.values()].map(({ models, ...group }) => {
|
|
1293
|
-
const preference = row.freeModels?.[group.id] ?? {}
|
|
1295
|
+
const preference = row.freeModels?.[group.id] ?? {};
|
|
1296
|
+
const context = resolveContext(models.map((s) => s.model), preference.contextWindow ?? (row.contextMode === "custom" ? row.contextWindow : null), row.contextWindow);
|
|
1297
|
+
const eligible = models.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
|
|
1298
|
+
const eligibleAccountIds = eligible.map((s) => s.accountId);
|
|
1299
|
+
const reasoning = routeReasoning(eligible);
|
|
1294
1300
|
const selected = preference.reasoningEffort;
|
|
1295
1301
|
return {
|
|
1296
1302
|
...group,
|
|
1303
|
+
...context,
|
|
1304
|
+
eligibleAccountIds,
|
|
1305
|
+
targets: group.targets.filter((t) => eligibleAccountIds.includes(t.account)),
|
|
1306
|
+
accountNames: group.accountNames.filter((_, i) => eligibleAccountIds.includes(models[i].accountId)),
|
|
1307
|
+
enforceContext: true,
|
|
1297
1308
|
parentId,
|
|
1298
1309
|
dynamicFree: true,
|
|
1299
1310
|
freeOnly: true,
|
|
@@ -1306,9 +1317,8 @@ var Manager = class {
|
|
|
1306
1317
|
effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
|
|
1307
1318
|
visible: row.visible !== false && preference.visible !== false,
|
|
1308
1319
|
selected: preference.visible !== false,
|
|
1309
|
-
supportsImages:
|
|
1310
|
-
|
|
1311
|
-
maxTokens: Math.min(row.maxTokens, ...models.map(({ model }) => model.maxTokens), ...models.map(({ model }) => model.contextWindow))
|
|
1320
|
+
supportsImages: eligible.every(({ model }) => model.supportsImages),
|
|
1321
|
+
maxTokens: Math.min(row.maxTokens, context.contextWindow, ...eligible.map(({ model }) => model.maxTokens))
|
|
1312
1322
|
};
|
|
1313
1323
|
});
|
|
1314
1324
|
}
|
|
@@ -1323,7 +1333,7 @@ var Manager = class {
|
|
|
1323
1333
|
publish() {
|
|
1324
1334
|
this.telemetry.configure(this.config.usageRetentionDays);
|
|
1325
1335
|
this.revision = revisionOf(this.raw);
|
|
1326
|
-
const routes =
|
|
1336
|
+
const routes = Object.fromEntries(this.routeModels().map((row) => [row.id, row]));
|
|
1327
1337
|
for (const model of this.adapterModels().filter((m) => m.dynamicFree)) routes[model.id] = model;
|
|
1328
1338
|
for (const provider of PROVIDERS) for (const model of this.providerModels(provider)) {
|
|
1329
1339
|
routes[`${provider}::${model.id}`] = { ...model, targets: [{ provider, model: model.id }] };
|
|
@@ -1335,23 +1345,30 @@ var Manager = class {
|
|
|
1335
1345
|
const rows = /* @__PURE__ */ new Map();
|
|
1336
1346
|
for (const account of this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false)) {
|
|
1337
1347
|
for (const model of this.catalogs.get(account.id)?.models ?? []) {
|
|
1338
|
-
|
|
1339
|
-
rows.
|
|
1340
|
-
...old,
|
|
1341
|
-
contextWindow: Math.min(old.contextWindow, model.contextWindow),
|
|
1342
|
-
maxTokens: Math.min(old.maxTokens, model.maxTokens),
|
|
1343
|
-
supportsImages: old.supportsImages && model.supportsImages,
|
|
1344
|
-
reasoning: routeReasoning([{ model: old, provider }, { model, provider }])
|
|
1345
|
-
} : {
|
|
1346
|
-
...model,
|
|
1347
|
-
...reasoningModes(model, provider).length ? { reasoning: routeReasoning([{ model, provider }]) } : {}
|
|
1348
|
-
});
|
|
1348
|
+
if (!rows.has(model.id)) rows.set(model.id, []);
|
|
1349
|
+
rows.get(model.id).push(model);
|
|
1349
1350
|
}
|
|
1350
1351
|
}
|
|
1351
|
-
return [...rows.values()].map((
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1352
|
+
return [...rows.values()].map((models) => {
|
|
1353
|
+
const model = models[0], preference = this.config.models[provider]?.[model.id] ?? {};
|
|
1354
|
+
const context = resolveContext(models, preference.contextWindow);
|
|
1355
|
+
const eligible = models.filter((m) => contextInfo(m).maxContextWindow >= context.contextWindow);
|
|
1356
|
+
const reasoning = routeReasoning(eligible.map((model2) => ({ model: model2, provider })));
|
|
1357
|
+
const selected = preference.reasoningEffort;
|
|
1358
|
+
return {
|
|
1359
|
+
...model,
|
|
1360
|
+
...context,
|
|
1361
|
+
enforceContext: true,
|
|
1362
|
+
name: preference.name ?? model.name,
|
|
1363
|
+
reasoning,
|
|
1364
|
+
supportsImages: preference.supportsImages !== false && eligible.every((m) => m.supportsImages),
|
|
1365
|
+
imageCapable: eligible.every((m) => m.supportsImages),
|
|
1366
|
+
...selected && reasoning?.supportedEfforts.includes(selected) ? { reasoningEffort: selected } : {},
|
|
1367
|
+
configuredEffort: selected ?? null,
|
|
1368
|
+
effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
|
|
1369
|
+
maxTokens: Math.min(context.contextWindow, ...eligible.map((m) => m.maxTokens))
|
|
1370
|
+
};
|
|
1371
|
+
});
|
|
1355
1372
|
}
|
|
1356
1373
|
hiddenModels(provider) {
|
|
1357
1374
|
return this.providerModels(provider).filter((m) => !this.modelVisible(provider, m.id)).map((m) => m.id);
|
|
@@ -1365,14 +1382,19 @@ var Manager = class {
|
|
|
1365
1382
|
const sources = effectiveTargets.flatMap((target) => this.config.accounts.filter((a) => a.enabled !== false && a.provider === target.provider && (!target.account || target.account === a.id)).flatMap((a) => {
|
|
1366
1383
|
const catalog = this.catalogs.get(a.id)?.models ?? [];
|
|
1367
1384
|
const models2 = target.models ? target.models.map((model) => catalog.find((m) => m.id === model)).filter(Boolean) : [matchModel(id, target, catalog)].filter(Boolean);
|
|
1368
|
-
return models2.map((model) => ({ model, provider: a.provider }));
|
|
1385
|
+
return models2.map((model) => ({ model, provider: a.provider, accountId: a.id }));
|
|
1369
1386
|
}));
|
|
1370
|
-
const
|
|
1387
|
+
const context = resolveContext(sources.map((s) => s.model), row.contextMode === "custom" ? row.contextWindow : null, row.contextWindow);
|
|
1388
|
+
const eligible = sources.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
|
|
1389
|
+
const models = eligible.map((s) => s.model), reasoning = routeReasoning(eligible);
|
|
1371
1390
|
return {
|
|
1372
1391
|
...row,
|
|
1392
|
+
...context,
|
|
1393
|
+
enforceContext: true,
|
|
1373
1394
|
supportsImages: models.length > 0 && models.every((m) => m.supportsImages),
|
|
1374
1395
|
reasoning,
|
|
1375
|
-
|
|
1396
|
+
eligibleAccountIds: [...new Set(eligible.map((s) => s.accountId))],
|
|
1397
|
+
maxTokens: Math.min(row.maxTokens, context.contextWindow, ...models.map((m) => m.maxTokens)),
|
|
1376
1398
|
id,
|
|
1377
1399
|
name: row.name ?? id,
|
|
1378
1400
|
expiringStatus: { ...this.telemetry.data.expiringRounds[id] },
|
|
@@ -1394,7 +1416,7 @@ var Manager = class {
|
|
|
1394
1416
|
const client = await this.router.client(account);
|
|
1395
1417
|
const models = (await client.models(AbortSignal.timeout(6e4), !!force)).map(modelInfo);
|
|
1396
1418
|
if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
|
|
1397
|
-
this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
|
|
1419
|
+
this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, contextVersion: 1, models });
|
|
1398
1420
|
this.catalogErrors.delete(account.id);
|
|
1399
1421
|
await withFileLock3(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
|
|
1400
1422
|
} catch (error) {
|
|
@@ -1451,6 +1473,7 @@ var Manager = class {
|
|
|
1451
1473
|
presets: Object.entries(ROUTE_PRESETS).map(([id, route]) => ({
|
|
1452
1474
|
id,
|
|
1453
1475
|
...structuredClone(route),
|
|
1476
|
+
providerCount: route.targets.length,
|
|
1454
1477
|
added: Object.hasOwn(this.config.routes, id),
|
|
1455
1478
|
availableProviders: [...new Set(route.targets.filter((t) => route.freeOnly ? this.config.accounts.some((a) => a.enabled !== false && a.provider === t.provider && this.freeModels(a).length) : matchModel(id, t, this.providerModels(t.provider))).map((t) => t.provider))]
|
|
1456
1479
|
})),
|
|
@@ -1466,6 +1489,63 @@ var Manager = class {
|
|
|
1466
1489
|
}
|
|
1467
1490
|
async action(input) {
|
|
1468
1491
|
if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
|
|
1492
|
+
if (input.action === "model.test") {
|
|
1493
|
+
const model = this.providerModels(input.provider).find((m) => m.id === input.id);
|
|
1494
|
+
if (!model) bad("\u8BF7\u5148\u5237\u65B0\u5E76\u9009\u62E9\u5DF2\u53D1\u73B0\u7684\u6A21\u578B");
|
|
1495
|
+
if (this.modelTestFlight || Date.now() - (this.modelTestAt ?? 0) < 1e4) bad("\u8BF7\u7B49\u5F85\u5F53\u524D\u6D4B\u8BD5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u95F4\u9694\u81F3\u5C11 10 \u79D2");
|
|
1496
|
+
this.modelTestFlight = true;
|
|
1497
|
+
this.modelTestAt = Date.now();
|
|
1498
|
+
this.modelTestAbort = new AbortController();
|
|
1499
|
+
const started = performance.now();
|
|
1500
|
+
let modelTest;
|
|
1501
|
+
try {
|
|
1502
|
+
const { response } = await this.router.chat(
|
|
1503
|
+
{
|
|
1504
|
+
model: `${input.provider}::${input.id}`,
|
|
1505
|
+
stream: true,
|
|
1506
|
+
messages: [{ role: "system", content: "Answer briefly." }, { role: "user", content: "Reply with OK." }],
|
|
1507
|
+
max_tokens: 16
|
|
1508
|
+
},
|
|
1509
|
+
AbortSignal.any([this.modelTestAbort.signal, AbortSignal.timeout(45e3)])
|
|
1510
|
+
);
|
|
1511
|
+
const reader = response.body.getReader();
|
|
1512
|
+
let size = 0, text = "";
|
|
1513
|
+
const decoder = new TextDecoder();
|
|
1514
|
+
try {
|
|
1515
|
+
for (; ; ) {
|
|
1516
|
+
const chunk = await reader.read();
|
|
1517
|
+
if (chunk.done) break;
|
|
1518
|
+
size += chunk.value.byteLength;
|
|
1519
|
+
if (size > 1024 * 1024) throw Error("probe_response_too_large");
|
|
1520
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
1521
|
+
}
|
|
1522
|
+
text += decoder.decode();
|
|
1523
|
+
let answered = false;
|
|
1524
|
+
for (const line of text.split(/\r?\n/).filter((l) => l.startsWith("data:"))) {
|
|
1525
|
+
if (line.slice(5).trim() === "[DONE]") continue;
|
|
1526
|
+
const payload = JSON.parse(line.slice(5));
|
|
1527
|
+
if (payload.error || payload.choices?.some((c) => c.finish_reason === "error")) throw Error("probe_stream_failed");
|
|
1528
|
+
answered ||= payload.choices?.some((c) => c.delta?.content || c.delta?.reasoning_content || c.delta?.tool_calls?.length) ?? false;
|
|
1529
|
+
}
|
|
1530
|
+
if (!answered || !/^data:\s*\[DONE\]\s*$/m.test(text)) throw Error("probe_incomplete");
|
|
1531
|
+
} finally {
|
|
1532
|
+
await reader.cancel().catch(() => {
|
|
1533
|
+
});
|
|
1534
|
+
reader.releaseLock();
|
|
1535
|
+
}
|
|
1536
|
+
modelTest = { ok: true, elapsedMs: Math.round(performance.now() - started) };
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
modelTest = {
|
|
1539
|
+
ok: false,
|
|
1540
|
+
status: Number(error.status) || 502,
|
|
1541
|
+
hint: error.hint || "\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF0C\u8BF7\u67E5\u770B\u8D26\u53F7\u72B6\u6001\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002"
|
|
1542
|
+
};
|
|
1543
|
+
} finally {
|
|
1544
|
+
this.modelTestFlight = false;
|
|
1545
|
+
this.modelTestAbort = void 0;
|
|
1546
|
+
}
|
|
1547
|
+
return { ...this.state(), modelTest };
|
|
1548
|
+
}
|
|
1469
1549
|
if (input.action?.startsWith("api.")) {
|
|
1470
1550
|
let result;
|
|
1471
1551
|
try {
|
|
@@ -1769,22 +1849,27 @@ var Manager = class {
|
|
|
1769
1849
|
}
|
|
1770
1850
|
} else if (input.action === "model.update") {
|
|
1771
1851
|
if (!PROVIDERS.includes(input.provider) || typeof input.id !== "string" || !input.id || input.id.length > 256) bad("\u6A21\u578B\u65E0\u6548");
|
|
1852
|
+
const model = this.providerModels(input.provider).find((m) => m.id === input.id);
|
|
1853
|
+
if (input.contextWindow != null && (!Number.isSafeInteger(input.contextWindow) || input.contextWindow <= 0 || !model || input.contextWindow > model.maxContextWindow)) bad("\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u80FD\u8D85\u8FC7\u8BE5\u6E20\u9053\u6A21\u578B\u7684\u6700\u5927\u5BB9\u91CF");
|
|
1854
|
+
if (input.supportsImages === true && !model?.imageCapable) bad("\u8BE5\u6A21\u578B\u672A\u58F0\u660E\u56FE\u7247\u8F93\u5165\u80FD\u529B");
|
|
1855
|
+
if (input.reasoningEffort != null && !model?.reasoning?.supportedEfforts.includes(input.reasoningEffort)) bad("\u6B64\u6A21\u578B\u4E0D\u652F\u6301\u6240\u9009\u63A8\u7406\u7B49\u7EA7");
|
|
1772
1856
|
raw.models = { ...raw.models, [input.provider]: {
|
|
1773
1857
|
...raw.models?.[input.provider],
|
|
1774
1858
|
[input.id]: {
|
|
1775
1859
|
...raw.models?.[input.provider]?.[input.id],
|
|
1776
|
-
...
|
|
1777
|
-
...input.visible !== void 0 ? { visible: input.visible } : {}
|
|
1860
|
+
...Object.fromEntries(["name", "visible", "contextWindow", "supportsImages", "reasoningEffort"].filter((k) => input[k] !== void 0).map((k) => [k, input[k]]))
|
|
1778
1861
|
}
|
|
1779
1862
|
} };
|
|
1780
1863
|
} else if (input.action === "free.model.update") {
|
|
1781
1864
|
const parent = raw.routes[input.parentId];
|
|
1782
1865
|
if (!parent?.freeOnly || !this.freeRoutes(input.parentId, this.config.routes[input.parentId]).some((m) => m.id === input.id)) bad("\u514D\u8D39\u6A21\u578B\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u9875\u9762");
|
|
1783
1866
|
const model = this.freeRoutes(input.parentId, this.config.routes[input.parentId]).find((m) => m.id === input.id);
|
|
1867
|
+
if (input.contextWindow != null && (!Number.isSafeInteger(input.contextWindow) || input.contextWindow <= 0 || input.contextWindow > model.maxContextWindow)) bad("\u4E0A\u4E0B\u6587\u9884\u7B97\u4E0D\u80FD\u8D85\u8FC7\u8BE5\u514D\u8D39\u6A21\u578B\u7684\u6700\u5927\u5BB9\u91CF");
|
|
1784
1868
|
if (input.reasoningEffort !== void 0 && input.reasoningEffort !== null && !model.reasoning?.supportedEfforts.includes(input.reasoningEffort)) bad("\u6B64\u514D\u8D39\u6A21\u578B\u4E0D\u652F\u6301\u6240\u9009\u63A8\u7406\u7B49\u7EA7");
|
|
1785
1869
|
parent.freeModels = { ...parent.freeModels, [input.id]: {
|
|
1786
1870
|
...parent.freeModels?.[input.id],
|
|
1787
1871
|
...input.visible !== void 0 ? { visible: input.visible } : {},
|
|
1872
|
+
...input.contextWindow !== void 0 ? { contextWindow: input.contextWindow } : {},
|
|
1788
1873
|
...input.reasoningEffort !== void 0 ? { reasoningEffort: input.reasoningEffort } : {}
|
|
1789
1874
|
} };
|
|
1790
1875
|
} else if (input.action === "route.save") {
|
|
@@ -1804,6 +1889,7 @@ var Manager = class {
|
|
|
1804
1889
|
if (["tokens", "credits"].includes(strategy)) await this.telemetry.advance(input.id, this.router.candidates(input.id), JSON.stringify({ strategy, targets: route.targets }));
|
|
1805
1890
|
} else if (input.action === "route.remove") {
|
|
1806
1891
|
if (!Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531\u4E0D\u5B58\u5728");
|
|
1892
|
+
if (Object.hasOwn(ROUTE_PRESETS, input.id)) raw.seenRoutePresets = [.../* @__PURE__ */ new Set([...raw.seenRoutePresets ?? [], input.id])];
|
|
1807
1893
|
delete raw.routes[input.id];
|
|
1808
1894
|
} else if (input.action === "preset.add") {
|
|
1809
1895
|
const id = input.id ?? "deepseek-v4.1-flash";
|
|
@@ -1941,4 +2027,4 @@ export {
|
|
|
1941
2027
|
Manager,
|
|
1942
2028
|
managementHandler
|
|
1943
2029
|
};
|
|
1944
|
-
//# sourceMappingURL=chunk-
|
|
2030
|
+
//# sourceMappingURL=chunk-4EO5V6FW.js.map
|