mslxdff 0.1.233 → 0.1.235
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/bin/mslxdff.js +3 -3
- package/docs/cli_help_mini.md +149 -0
- package/package.json +1 -1
- package/src/auto.js +264 -264
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +155 -155
- package/src/chat-pipeline/policy.js +84 -84
- package/src/chat-pipeline/serial-trial.js +16 -3
- package/src/chat-pipeline/upstream-5xx-retry.js +72 -0
- package/src/cli/commands/group.js +266 -266
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +293 -293
- package/src/peers.js +345 -345
- package/src/providers/AGENTS.md +49 -0
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +292 -292
- package/src/providers/qoder/index.js +311 -311
- package/src/providers/qoder/sse.js +233 -233
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/const.js +219 -219
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/raccoon/index.js +36 -4
- package/src/providers/raccoon/sticky.js +46 -0
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/chat.js +298 -298
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/peer-handler.js +14 -4
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +156 -156
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peer-serial.js +5 -2
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -364
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +355 -355
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { compatFetch } from "../../compat.js";
|
|
4
4
|
import { envInt } from "../base.js";
|
|
5
5
|
import { createKeyRing } from "../keyring.js";
|
|
6
|
+
import { createStickyPicker } from "./sticky.js";
|
|
6
7
|
import { loadProviderKeys } from "../../state.js";
|
|
7
8
|
import { forwardRaccoonChat } from "./chat.js";
|
|
8
9
|
import { buildRaccoonModelList, listRaccoonModels, mergeRaccoonCatalogIntoAllowlist } from "./models.js";
|
|
@@ -14,6 +15,14 @@ import { RACCOON_DEFAULT_COOLDOWN_MS, RACCOON_DEFAULT_QUOTA_COOLDOWN_MS } from "
|
|
|
14
15
|
/** kind → 冷却档位。auth 走短冷(重登指引已由响应体给出);quota 走长冷(按天回血)。 */
|
|
15
16
|
const COOLDOWN_BY_KIND = { auth: "short", rate_limit: "short", server: "short", quota: "long" };
|
|
16
17
|
|
|
18
|
+
/** 会话粘号 TTL:允 0(=关,退回每次调用轮转),故不能用只收正数的 envInt。 */
|
|
19
|
+
function stickyMsFromEnv(env = process.env) {
|
|
20
|
+
const raw = env?.MSLXDFF_RACCOON_STICKY_MS;
|
|
21
|
+
if (raw === undefined || String(raw).trim() === "") return 1_800_000;
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
return Number.isFinite(n) && n >= 0 ? n : 1_800_000;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function notLoggedInResponse() {
|
|
18
27
|
return new Response(
|
|
19
28
|
JSON.stringify({ error: { message: "raccoon: 无可用账号 —— 先运行 mslxdff -provider raccoon login", type: "auth_error" } }),
|
|
@@ -63,6 +72,17 @@ export function createRaccoonProvider({
|
|
|
63
72
|
}
|
|
64
73
|
const ring = createKeyRing(keys, { cooldownMs });
|
|
65
74
|
|
|
75
|
+
// 会话粘号:同一段对话(opts.sessionId)固定用同一个号,换会话才轮转到下一个号。
|
|
76
|
+
// 为什么不是每次调用轮转:积分按号均摊靠「会话之间」轮换就够了,而同一会话逐轮换号会丢掉上游
|
|
77
|
+
// 的 prompt 缓存亲和,也让排障时分不清「这个号到底好不好」。无会话标识时退回 reqId(同请求重试不换号)
|
|
78
|
+
// 与纯轮转,行为与旧版一致。TTL 内每次命中续期;`MSLXDFF_RACCOON_STICKY_MS=0` 关掉退回逐调用轮转。
|
|
79
|
+
const pickKey = createStickyPicker({
|
|
80
|
+
pick: () => ring.next(),
|
|
81
|
+
// 被 replace() 换掉的旧 token 已不在 ring 里 → 同样视为不可用,会话会前进到新号而不是粘住废 token
|
|
82
|
+
isCooling: (k) => !ring.keys.includes(k) || ring.isCooling(k),
|
|
83
|
+
ttlMs: stickyMsFromEnv(),
|
|
84
|
+
});
|
|
85
|
+
|
|
66
86
|
// access_token → 账号文档(office_identity / device_id / refresh_token 跟号走,取自 auths/raccoon-<uid>.json)
|
|
67
87
|
// 缓存**可失效**:重登与续期都会换盘上的 token,只建一次会让本进程永远停在旧号(见 healKeyFromDocs)
|
|
68
88
|
let docCache = null;
|
|
@@ -103,7 +123,7 @@ export function createRaccoonProvider({
|
|
|
103
123
|
const inflightRefresh = new Map();
|
|
104
124
|
|
|
105
125
|
/** 临期即续期;续期成功把新 token 回写账号文档并替换 keyring 里的旧键。失败不阻断(让上游如实报)。 */
|
|
106
|
-
async function ensureFresh(token) {
|
|
126
|
+
async function ensureFresh(token, scope) {
|
|
107
127
|
const credential = credentialFor(token);
|
|
108
128
|
if (!credential.refresh_token || !isRaccoonExpiringSoon(credential)) return credential;
|
|
109
129
|
const pending = inflightRefresh.get(token);
|
|
@@ -114,6 +134,8 @@ export function createRaccoonProvider({
|
|
|
114
134
|
if (next.access_token !== token) {
|
|
115
135
|
ring.replace(token, next.access_token);
|
|
116
136
|
docCache?.delete(token);
|
|
137
|
+
// 粘号表跟着换 token:否则该会话下一轮会认为旧 token 不可用而漂到别的号(同一账号被拆成两半)
|
|
138
|
+
pickKey.remember(scope, next.access_token);
|
|
117
139
|
}
|
|
118
140
|
// ⚠ 同 token 也必须刷新到期时间:否则 docCache 永远停在旧 expires_at,每个请求都会重复调一次 refresh
|
|
119
141
|
docCache?.set(next.access_token, { ...credential, accessToken: next.access_token, refreshToken: next.refresh_token, expiresAt: next.expires_at || "" });
|
|
@@ -153,12 +175,18 @@ export function createRaccoonProvider({
|
|
|
153
175
|
async function chat(body, opts = {}) {
|
|
154
176
|
const total = ring.keys.length;
|
|
155
177
|
if (!total) return notLoggedInResponse();
|
|
178
|
+
// 会话优先:一段对话固定一个号(积分按会话轮换均摊);没有会话标识时才退到「同请求粘号」
|
|
179
|
+
const scope = opts?.sessionId ? `sess:${opts.sessionId}` : opts?.reqId ? `req:${opts.reqId}` : null;
|
|
156
180
|
let lastRes = null;
|
|
157
181
|
let healed = false; // 一次请求内只自愈一次:防重试风暴,也防「文档里那份同样不可用」时打旋
|
|
158
182
|
for (let hop = 0; hop <= total + 1; hop++) {
|
|
159
|
-
|
|
183
|
+
let pick = "rr";
|
|
184
|
+
const key = pickKey(scope, (d) => { pick = d; });
|
|
160
185
|
if (!key) break;
|
|
161
|
-
|
|
186
|
+
if (pick !== "sticky" && pick !== "rr") {
|
|
187
|
+
console.error(`[raccoon] 会话选号 pick=${pick} account=${raccoonTokenFingerprint(key)} ${scope || ""}`);
|
|
188
|
+
}
|
|
189
|
+
const credential = await ensureFresh(key, scope);
|
|
162
190
|
const res = await forwardRaccoonChat({ body, credential, fetchImpl, timeoutMs, effort: opts?.effort ?? effort });
|
|
163
191
|
const kind = res.headers.get("x-mslxdff-raccoon-kind");
|
|
164
192
|
if (!kind) return res; // 成功或未分类:直接交出去
|
|
@@ -166,7 +194,11 @@ export function createRaccoonProvider({
|
|
|
166
194
|
if (kind === "auth" && !healed) {
|
|
167
195
|
healed = true;
|
|
168
196
|
// 自愈成功就换号重试;不给失效号记冷却(ring.replace 也不把冷却带给新号)
|
|
169
|
-
|
|
197
|
+
const healedKey = await healKeyFromDocs(key);
|
|
198
|
+
if (healedKey) {
|
|
199
|
+
pickKey.remember(scope, healedKey); // 本会话钉到救回来的号,别在下一轮又漂走
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
170
202
|
}
|
|
171
203
|
const mode = COOLDOWN_BY_KIND[kind] || "none";
|
|
172
204
|
if (mode === "long") ring.onError(key, quotaCooldownMs);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// raccoon 会话粘号:一段对话(sessionId)固定用一个号,换会话才轮转下一个号 —— 积分按会话均摊。
|
|
2
|
+
// 语义与 qoder 的同请求粘号一致(只有该号进冷却 401/403/429/5xx 才换),差别在多两个能力:
|
|
3
|
+
// · scope 由会话(opts.sessionId)优先,没有会话标识时退到 reqId,两者都无则退回纯 round-robin;
|
|
4
|
+
// · remember():续期换 token / 换号自愈后把本会话重新钉到那个号,避免同一账号被拆成两半。
|
|
5
|
+
// 本文件是独立实现(qoder 只用同请求粘号且无需 remember),不改 qoder 的粘号逻辑。
|
|
6
|
+
export function createStickyPicker({ pick, isCooling = () => false, now = Date.now, ttlMs = 600_000, maxEntries = 200 } = {}) {
|
|
7
|
+
const seen = new Map(); // scope → { key, at }
|
|
8
|
+
|
|
9
|
+
function evict() {
|
|
10
|
+
const t = now();
|
|
11
|
+
for (const [scope, hit] of seen) if (t - hit.at >= ttlMs) seen.delete(scope);
|
|
12
|
+
while (seen.size > maxEntries) seen.delete(seen.keys().next().value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// onDecision(可选):把"这次为什么选它"回报给调用方 —— 决定必须可观测。
|
|
16
|
+
// sticky=同会话复用 / new=新会话轮转选中 / switch=上次的号在冷却(或 TTL 过期)故换 / rr=无 scope 退回轮转
|
|
17
|
+
function pickSticky(scope, onDecision) {
|
|
18
|
+
if (!scope) {
|
|
19
|
+
onDecision?.("rr");
|
|
20
|
+
return pick();
|
|
21
|
+
}
|
|
22
|
+
const hit = seen.get(scope);
|
|
23
|
+
// 粘住:同一会话内复用上次的号,除非它已被冷却(那时必须换)
|
|
24
|
+
if (hit && now() - hit.at < ttlMs && !isCooling(hit.key)) {
|
|
25
|
+
hit.at = now();
|
|
26
|
+
onDecision?.("sticky");
|
|
27
|
+
return hit.key;
|
|
28
|
+
}
|
|
29
|
+
const key = pick();
|
|
30
|
+
onDecision?.(hit ? "switch" : "new");
|
|
31
|
+
if (key) {
|
|
32
|
+
seen.set(scope, { key, at: now() });
|
|
33
|
+
if (seen.size > maxEntries) evict();
|
|
34
|
+
}
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
// forget:丢弃 scope 的粘号记录(令牌刷新换了 key 后必须丢弃,否则同会话还粘旧 key)
|
|
38
|
+
pickSticky.forget = (scope) => { seen.delete(scope); };
|
|
39
|
+
// remember:把 scope 的粘号改钉到指定 key(换号自愈 / 续期换 token 后,让同一会话继续留在同一个号上)
|
|
40
|
+
pickSticky.remember = (scope, key) => {
|
|
41
|
+
if (!scope || !key) return;
|
|
42
|
+
seen.set(scope, { key, at: now() });
|
|
43
|
+
if (seen.size > maxEntries) evict();
|
|
44
|
+
};
|
|
45
|
+
return pickSticky;
|
|
46
|
+
}
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 可扩展供应商注册表:新增特殊供应商时,仅在此文件注册 + 新增对应 provider 文件即可
|
|
3
|
-
* 每个条目:{ id, match(id, baseUrl) => bool, load() => Promise<factory> }
|
|
4
|
-
* 匹配优先级:按数组顺序,首个命中即用;未命中走通用 generic
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export const customProviders = [
|
|
8
|
-
{
|
|
9
|
-
id: "workbuddy",
|
|
10
|
-
match: (id, baseUrl) => id === "workbuddy" || String(baseUrl).includes("copilot.tencent"),
|
|
11
|
-
load: () => import("./workbuddy.js").then((m) => m.createWorkbuddyProvider),
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
id: "traework",
|
|
15
|
-
match: (id, baseUrl) => id === "traework" || String(baseUrl).includes("trae"),
|
|
16
|
-
load: () => import("./traework.js").then((m) => m.createTraeworkProvider),
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: "cline",
|
|
20
|
-
match: (id, baseUrl) => id === "cline" || String(baseUrl).includes("cline.bot"),
|
|
21
|
-
load: () => import("./cline.js").then((m) => m.createClineProvider),
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: "codearts",
|
|
25
|
-
match: (id, baseUrl) => id === "codearts" || String(baseUrl).includes("myhuaweicloud.com"),
|
|
26
|
-
load: () => import("./codearts.js").then((m) => m.createCodeartsProvider),
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
id: "qoder",
|
|
30
|
-
match: (id, baseUrl) => id === "qoder" || String(baseUrl).includes("qoder"),
|
|
31
|
-
load: () => import("./qoder/index.js").then((m) => m.createQoderProvider),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
// ⚠ 必须排在 qwenwork 之前:qwenwork 的 match 用 `includes("qwenwork")`,
|
|
35
|
-
// 而 "globalqwenwork" 与 "gateway.qwenwork.ai"/"globalqwenwork://native" 都含该子串,
|
|
36
|
-
// 若后置会被 cn 条目抢先命中(first-match-wins),把国际站 token 打到 cn 网关。
|
|
37
|
-
id: "globalqwenwork",
|
|
38
|
-
match: (id, baseUrl) => id === "globalqwenwork" || String(baseUrl).includes("qwenwork.ai"),
|
|
39
|
-
load: () => import("./globalqwenwork/index.js").then((m) => m.createGlobalQwenworkProvider),
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "qwenwork",
|
|
43
|
-
match: (id, baseUrl) => id === "qwenwork" || String(baseUrl).includes("qwenwork"),
|
|
44
|
-
load: () => import("./qwenwork/index.js").then((m) => m.createQwenworkProvider),
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "zcode",
|
|
48
|
-
match: (id, baseUrl) => id === "zcode" || String(baseUrl).includes("zcode.z.ai"),
|
|
49
|
-
load: () => import("./zcode/index.js").then((m) => m.createZcodeProvider),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "railway",
|
|
53
|
-
// opencode zen free 层的另一个 IP 出口(Railway 反代),只暴露 free 模型。
|
|
54
|
-
// 匹配必须排在 generic 之前,且不能与其它条目撞(railway.app 域名足够独特)。
|
|
55
|
-
match: (id, baseUrl) => id === "railway" || String(baseUrl).includes("railway.app"),
|
|
56
|
-
load: () => import("./railway.js").then((m) => m.createRailwayProvider),
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: "raccoon",
|
|
60
|
-
match: (id, baseUrl) => id === "raccoon" || String(baseUrl).includes("xiaohuanxiong.com"),
|
|
61
|
-
load: () => import("./raccoon/index.js").then((m) => m.createRaccoonProvider),
|
|
62
|
-
},
|
|
63
|
-
// agentrouter 定制工厂已于 2026-10-21 摘除:现网成功率仅 ~48%(502/400/422/500 高发),
|
|
64
|
-
// 从注册表 + 预热名单移除后网关不再解析 agentrouter/* 模型。工厂文件 src/providers/agentrouter.js
|
|
65
|
-
// 与其测试保留,日后要重新启用只需在此加回一条 { id:"agentrouter", ... } 并同步 dispatcher 预热名单。
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// 供 bench/probe 等需要定制化解析模型列表的场景
|
|
69
|
-
export const customNormalizers = [
|
|
70
|
-
{
|
|
71
|
-
id: "cline",
|
|
72
|
-
match: (baseUrl) => String(baseUrl).includes("cline.bot"),
|
|
73
|
-
normalize: (json) => Array.isArray(json?.free) ? json.free : null,
|
|
74
|
-
},
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
export async function getCustomProviderFactory(id, baseUrl) {
|
|
78
|
-
for (const entry of customProviders) {
|
|
79
|
-
try { if (entry.match(id, baseUrl)) return await entry.load(); } catch {}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getCustomNormalizer(baseUrl) {
|
|
85
|
-
for (const entry of customNormalizers) {
|
|
86
|
-
try { if (entry.match(baseUrl)) return entry.normalize; } catch {}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 可扩展供应商注册表:新增特殊供应商时,仅在此文件注册 + 新增对应 provider 文件即可
|
|
3
|
+
* 每个条目:{ id, match(id, baseUrl) => bool, load() => Promise<factory> }
|
|
4
|
+
* 匹配优先级:按数组顺序,首个命中即用;未命中走通用 generic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const customProviders = [
|
|
8
|
+
{
|
|
9
|
+
id: "workbuddy",
|
|
10
|
+
match: (id, baseUrl) => id === "workbuddy" || String(baseUrl).includes("copilot.tencent"),
|
|
11
|
+
load: () => import("./workbuddy.js").then((m) => m.createWorkbuddyProvider),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "traework",
|
|
15
|
+
match: (id, baseUrl) => id === "traework" || String(baseUrl).includes("trae"),
|
|
16
|
+
load: () => import("./traework.js").then((m) => m.createTraeworkProvider),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "cline",
|
|
20
|
+
match: (id, baseUrl) => id === "cline" || String(baseUrl).includes("cline.bot"),
|
|
21
|
+
load: () => import("./cline.js").then((m) => m.createClineProvider),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "codearts",
|
|
25
|
+
match: (id, baseUrl) => id === "codearts" || String(baseUrl).includes("myhuaweicloud.com"),
|
|
26
|
+
load: () => import("./codearts.js").then((m) => m.createCodeartsProvider),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "qoder",
|
|
30
|
+
match: (id, baseUrl) => id === "qoder" || String(baseUrl).includes("qoder"),
|
|
31
|
+
load: () => import("./qoder/index.js").then((m) => m.createQoderProvider),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// ⚠ 必须排在 qwenwork 之前:qwenwork 的 match 用 `includes("qwenwork")`,
|
|
35
|
+
// 而 "globalqwenwork" 与 "gateway.qwenwork.ai"/"globalqwenwork://native" 都含该子串,
|
|
36
|
+
// 若后置会被 cn 条目抢先命中(first-match-wins),把国际站 token 打到 cn 网关。
|
|
37
|
+
id: "globalqwenwork",
|
|
38
|
+
match: (id, baseUrl) => id === "globalqwenwork" || String(baseUrl).includes("qwenwork.ai"),
|
|
39
|
+
load: () => import("./globalqwenwork/index.js").then((m) => m.createGlobalQwenworkProvider),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "qwenwork",
|
|
43
|
+
match: (id, baseUrl) => id === "qwenwork" || String(baseUrl).includes("qwenwork"),
|
|
44
|
+
load: () => import("./qwenwork/index.js").then((m) => m.createQwenworkProvider),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "zcode",
|
|
48
|
+
match: (id, baseUrl) => id === "zcode" || String(baseUrl).includes("zcode.z.ai"),
|
|
49
|
+
load: () => import("./zcode/index.js").then((m) => m.createZcodeProvider),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "railway",
|
|
53
|
+
// opencode zen free 层的另一个 IP 出口(Railway 反代),只暴露 free 模型。
|
|
54
|
+
// 匹配必须排在 generic 之前,且不能与其它条目撞(railway.app 域名足够独特)。
|
|
55
|
+
match: (id, baseUrl) => id === "railway" || String(baseUrl).includes("railway.app"),
|
|
56
|
+
load: () => import("./railway.js").then((m) => m.createRailwayProvider),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "raccoon",
|
|
60
|
+
match: (id, baseUrl) => id === "raccoon" || String(baseUrl).includes("xiaohuanxiong.com"),
|
|
61
|
+
load: () => import("./raccoon/index.js").then((m) => m.createRaccoonProvider),
|
|
62
|
+
},
|
|
63
|
+
// agentrouter 定制工厂已于 2026-10-21 摘除:现网成功率仅 ~48%(502/400/422/500 高发),
|
|
64
|
+
// 从注册表 + 预热名单移除后网关不再解析 agentrouter/* 模型。工厂文件 src/providers/agentrouter.js
|
|
65
|
+
// 与其测试保留,日后要重新启用只需在此加回一条 { id:"agentrouter", ... } 并同步 dispatcher 预热名单。
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// 供 bench/probe 等需要定制化解析模型列表的场景
|
|
69
|
+
export const customNormalizers = [
|
|
70
|
+
{
|
|
71
|
+
id: "cline",
|
|
72
|
+
match: (baseUrl) => String(baseUrl).includes("cline.bot"),
|
|
73
|
+
normalize: (json) => Array.isArray(json?.free) ? json.free : null,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export async function getCustomProviderFactory(id, baseUrl) {
|
|
78
|
+
for (const entry of customProviders) {
|
|
79
|
+
try { if (entry.match(id, baseUrl)) return await entry.load(); } catch {}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getCustomNormalizer(baseUrl) {
|
|
85
|
+
for (const entry of customNormalizers) {
|
|
86
|
+
try { if (entry.match(baseUrl)) return entry.normalize; } catch {}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|