dsh-multi-router 0.0.0-stage → 0.3.7

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -2
  3. package/THIRD_PARTY_NOTICES.md +135 -0
  4. package/cordis.patch.yml +3 -0
  5. package/lib/accounts.js +13 -0
  6. package/lib/accounts.js.map +7 -0
  7. package/lib/chunk-3VVQXFPV.js +449 -0
  8. package/lib/chunk-3VVQXFPV.js.map +7 -0
  9. package/lib/chunk-BIEDP6NW.js +885 -0
  10. package/lib/chunk-BIEDP6NW.js.map +7 -0
  11. package/lib/chunk-DNQPZMPF.js +33 -0
  12. package/lib/chunk-DNQPZMPF.js.map +7 -0
  13. package/lib/chunk-HZVBEBOD.js +208 -0
  14. package/lib/chunk-HZVBEBOD.js.map +7 -0
  15. package/lib/chunk-IEN3ZQFS.js +95 -0
  16. package/lib/chunk-IEN3ZQFS.js.map +7 -0
  17. package/lib/chunk-PD2DTHG2.js +116 -0
  18. package/lib/chunk-PD2DTHG2.js.map +7 -0
  19. package/lib/chunk-S5KMFAQM.js +50 -0
  20. package/lib/chunk-S5KMFAQM.js.map +7 -0
  21. package/lib/chunk-TEZ2FMEO.js +1091 -0
  22. package/lib/chunk-TEZ2FMEO.js.map +7 -0
  23. package/lib/chunk-VESY2B5O.js +5068 -0
  24. package/lib/chunk-VESY2B5O.js.map +7 -0
  25. package/lib/chunk-WNRHV24A.js +2022 -0
  26. package/lib/chunk-WNRHV24A.js.map +7 -0
  27. package/lib/cli.js +102 -0
  28. package/lib/cli.js.map +7 -0
  29. package/lib/client.js +1371 -0
  30. package/lib/client.js.map +7 -0
  31. package/lib/clients.js +16 -0
  32. package/lib/clients.js.map +7 -0
  33. package/lib/host.js +362 -0
  34. package/lib/index.js +362 -0
  35. package/lib/index.js.map +7 -0
  36. package/lib/manager.js +23 -0
  37. package/lib/manager.js.map +7 -0
  38. package/lib/region-DO3KAV7V.js +15 -0
  39. package/lib/region-DO3KAV7V.js.map +7 -0
  40. package/lib/router.js +14 -0
  41. package/lib/router.js.map +7 -0
  42. package/lib/shim.js +10 -0
  43. package/lib/shim.js.map +7 -0
  44. package/lib/upstream-42RS7F4H.js +25 -0
  45. package/lib/upstream-42RS7F4H.js.map +7 -0
  46. package/package.json +72 -4
  47. package/router.example.json +289 -0
  48. package/upstream-lock.json +17 -0
@@ -0,0 +1,885 @@
1
+ import {
2
+ jsonCredential,
3
+ qoderPluginDataDir
4
+ } from "./chunk-VESY2B5O.js";
5
+ import {
6
+ importAccount,
7
+ readPortableCredential
8
+ } from "./chunk-IEN3ZQFS.js";
9
+ import {
10
+ WORKBUDDY_VARIANTS,
11
+ defaultDesktopAuthPath,
12
+ traeStorageCandidates
13
+ } from "./chunk-WNRHV24A.js";
14
+ import {
15
+ hostIsLoopback
16
+ } from "./chunk-DNQPZMPF.js";
17
+ import {
18
+ Telemetry,
19
+ matchModel
20
+ } from "./chunk-3VVQXFPV.js";
21
+ import {
22
+ PROVIDERS,
23
+ PROVIDER_NAMES,
24
+ ROUTE_PRESETS,
25
+ beijingDay,
26
+ isFreeModel,
27
+ reasoningModes,
28
+ routeReasoning,
29
+ validateConfig,
30
+ writePrivateJson
31
+ } from "./chunk-HZVBEBOD.js";
32
+ import {
33
+ modelWithCurrentPromotion
34
+ } from "./chunk-TEZ2FMEO.js";
35
+
36
+ // src/manager.mjs
37
+ import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
38
+ import { readFile, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2, access } from "node:fs/promises";
39
+ import { watch } from "node:fs";
40
+ import { dirname as dirname3, join as join3, relative as relative2, resolve } from "node:path";
41
+ import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
42
+
43
+ // upstream/qoder/src/status-paths.ts
44
+ var QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
45
+ var QODER_PROBE_PATH = "/plugins/dsh-qoder-connect/probe";
46
+ var QODER_AUTH_PATH = "/plugins/dsh-qoder-connect/auth";
47
+ var QODER_GLOBAL_STATUS_PATH = "/plugins/dsh-qoder-connect/global/status";
48
+ var QODER_GLOBAL_PROBE_PATH = "/plugins/dsh-qoder-connect/global/probe";
49
+ var QODER_GLOBAL_AUTH_PATH = "/plugins/dsh-qoder-connect/global/auth";
50
+
51
+ // upstream/qoder/src/variants.ts
52
+ var QODER_VARIANTS = [
53
+ {
54
+ id: "qoder",
55
+ displayName: "Qoder",
56
+ appName: "Qoder",
57
+ region: "china",
58
+ ownFilename: ".qoder-auth.json",
59
+ probeFilename: ".qoder-probe.json",
60
+ catalogFilename: ".qoder-catalog.json",
61
+ statusPath: QODER_STATUS_PATH,
62
+ probePath: QODER_PROBE_PATH,
63
+ authPath: QODER_AUTH_PATH
64
+ },
65
+ {
66
+ id: "qoder-global",
67
+ displayName: "Qoder Global",
68
+ appName: "Qoder Global",
69
+ region: "global",
70
+ ownFilename: ".qoder-global-auth.json",
71
+ probeFilename: ".qoder-global-probe.json",
72
+ catalogFilename: ".qoder-global-catalog.json",
73
+ statusPath: QODER_GLOBAL_STATUS_PATH,
74
+ probePath: QODER_GLOBAL_PROBE_PATH,
75
+ authPath: QODER_GLOBAL_AUTH_PATH
76
+ }
77
+ ];
78
+ var CHINA_VARIANT = QODER_VARIANTS[0];
79
+ var GLOBAL_VARIANT = QODER_VARIANTS[1];
80
+
81
+ // upstream/qoder/src/auth.ts
82
+ import { dirname, join } from "node:path";
83
+ import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
84
+ function qoderOwnAuthPath(variant) {
85
+ return join(qoderPluginDataDir(), variant.ownFilename);
86
+ }
87
+
88
+ // src/migration.mjs
89
+ import { randomBytes, scrypt, createCipheriv, createDecipheriv } from "node:crypto";
90
+ import { promisify } from "node:util";
91
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
92
+ import { dirname as dirname2, join as join2, relative } from "node:path";
93
+ var derive = promisify(scrypt);
94
+ var FORMAT = "dsh-multi-router";
95
+ var MAX_BYTES = 512 * 1024;
96
+ var pick = (value, keys) => Object.fromEntries(keys.filter((k) => Object.hasOwn(value ?? {}, k)).map((k) => [k, value[k]]));
97
+ var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays"];
98
+ function settings(config) {
99
+ return {
100
+ ...pick(config, options),
101
+ models: Object.fromEntries(Object.entries(config.models ?? {}).map(([p, rows]) => [p, Object.fromEntries(Object.entries(rows).map(([id, pref]) => [id, pick(pref, ["name", "visible"])]))])),
102
+ accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled"])),
103
+ routes: Object.fromEntries(Object.entries(config.routes).map(([id, r]) => [id, {
104
+ ...pick(r, ["name", "visible", "strategy", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
105
+ targets: r.targets.map((t) => pick(t, ["provider", "account", "model", "models", "weight", "tokenBudget", "creditBudget"]))
106
+ }]))
107
+ };
108
+ }
109
+ var problem = (message) => {
110
+ throw Error(message);
111
+ };
112
+ function password(value, exporting = false) {
113
+ if (typeof value !== "string" || value.length > 1024 || value.length < (exporting ? 10 : 1)) problem(exporting ? "\u8FC1\u79FB\u5BC6\u7801\u81F3\u5C11 10 \u4E2A\u5B57\u7B26" : "\u8BF7\u8F93\u5165\u8FC1\u79FB\u5BC6\u7801");
114
+ }
115
+ async function exportConfiguration(manager, input) {
116
+ const payload = { format: FORMAT, version: 1, encrypted: false, createdAt: (/* @__PURE__ */ new Date()).toISOString(), config: settings(manager.config) };
117
+ if (!input.includeCredentials) return payload;
118
+ password(input.password, true);
119
+ payload.credentials = {};
120
+ for (const account of manager.config.accounts) {
121
+ try {
122
+ const credential = await readPortableCredential(account);
123
+ if (credential) Object.defineProperty(payload.credentials, account.id, { value: credential, enumerable: true });
124
+ } catch {
125
+ problem(`\u8D26\u53F7 ${account.id} \u7684\u51ED\u8BC1\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u4FEE\u590D\u540E\u5BFC\u51FA\u6216\u9009\u62E9\u4EC5\u914D\u7F6E`);
126
+ }
127
+ }
128
+ const salt = randomBytes(16), iv = randomBytes(12), key = await derive(input.password, salt, 32);
129
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
130
+ const bytes = Buffer.from(JSON.stringify(payload));
131
+ if (bytes.length > MAX_BYTES) problem("\u914D\u7F6E\u8FC7\u5927\uFF0C\u65E0\u6CD5\u5BFC\u51FA");
132
+ const data = Buffer.concat([cipher.update(bytes), cipher.final()]);
133
+ return {
134
+ format: FORMAT,
135
+ version: 1,
136
+ encrypted: true,
137
+ cipher: "aes-256-gcm",
138
+ kdf: "scrypt",
139
+ salt: salt.toString("base64"),
140
+ iv: iv.toString("base64"),
141
+ tag: cipher.getAuthTag().toString("base64"),
142
+ data: data.toString("base64")
143
+ };
144
+ }
145
+ async function decodeConfiguration(input) {
146
+ let payload;
147
+ try {
148
+ if (typeof input.document !== "string" || Buffer.byteLength(input.document) > 900 * 1024) problem("\u8FC1\u79FB\u6587\u4EF6\u8FC7\u5927");
149
+ payload = JSON.parse(input.document);
150
+ } catch {
151
+ problem("\u8BF7\u9009\u62E9\u6709\u6548\u7684\u63D2\u4EF6\u8FC1\u79FB JSON \u6587\u4EF6\uFF08\u5C0F\u4E8E 900 KB\uFF09");
152
+ }
153
+ if (payload?.format !== FORMAT || payload.version !== 1) problem("\u4E0D\u662F\u53D7\u652F\u6301\u7684\u63D2\u4EF6\u8FC1\u79FB\u6587\u4EF6");
154
+ const encrypted = payload.encrypted === true;
155
+ if (encrypted) {
156
+ password(input.password);
157
+ try {
158
+ if (payload.cipher !== "aes-256-gcm" || payload.kdf !== "scrypt") throw Error();
159
+ const salt = Buffer.from(payload.salt, "base64"), iv = Buffer.from(payload.iv, "base64"), tag = Buffer.from(payload.tag, "base64"), data = Buffer.from(payload.data, "base64");
160
+ if (salt.length !== 16 || iv.length !== 12 || tag.length !== 16 || data.length > MAX_BYTES) throw Error();
161
+ const decipher = createDecipheriv("aes-256-gcm", await derive(input.password, salt, 32), iv);
162
+ decipher.setAuthTag(tag);
163
+ payload = JSON.parse(Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8"));
164
+ } catch {
165
+ problem("\u5BC6\u7801\u9519\u8BEF\u6216\u8FC1\u79FB\u6587\u4EF6\u5DF2\u635F\u574F");
166
+ }
167
+ }
168
+ if (payload?.format !== FORMAT || payload.version !== 1 || !Array.isArray(payload.config?.accounts) || payload.config.accounts.length > 200 || !payload.config.routes || typeof payload.config.routes !== "object" || Object.keys(payload.config.routes).length > 500) problem("\u8FC1\u79FB\u914D\u7F6E\u683C\u5F0F\u65E0\u6548");
169
+ if (payload.credentials && !encrypted) problem("\u542B\u51ED\u8BC1\u7684\u8FC1\u79FB\u6587\u4EF6\u5FC5\u987B\u52A0\u5BC6");
170
+ if (payload.credentials && (typeof payload.credentials !== "object" || Array.isArray(payload.credentials))) problem("\u8FC1\u79FB\u51ED\u8BC1\u683C\u5F0F\u65E0\u6548");
171
+ const config = settings(payload.config);
172
+ validateConfig({ ...config, accounts: config.accounts.map((a) => ({ ...a, credentialFile: "placeholder.json" })) });
173
+ if (payload.credentials && Object.keys(payload.credentials).some((id) => !config.accounts.some((a) => a.id === id))) problem("\u8FC1\u79FB\u51ED\u8BC1\u5B58\u5728\u672A\u77E5\u8D26\u53F7");
174
+ return { config, credentials: payload.credentials ?? {}, encrypted };
175
+ }
176
+ function migrationSummary(manager, portable, mode) {
177
+ if (!["merge", "replace"].includes(mode)) problem("\u8BF7\u9009\u62E9\u5408\u5E76\u6216\u66FF\u6362");
178
+ for (const a of portable.config.accounts) {
179
+ const old = manager.config.accounts.find((b) => b.id === a.id);
180
+ if (mode === "merge" && old && old.provider !== a.provider) problem(`\u8D26\u53F7 ${a.id} \u7684\u4F9B\u5E94\u5546\u51B2\u7A81\uFF0C\u8BF7\u6539\u7528\u66FF\u6362\u6216\u4FEE\u6539\u5BFC\u5165\u6587\u4EF6`);
181
+ }
182
+ const existing = (id) => mode === "merge" && manager.config.accounts.some((a) => a.id === id);
183
+ return {
184
+ mode,
185
+ accounts: portable.config.accounts.length,
186
+ newAccounts: portable.config.accounts.filter((a) => !existing(a.id)).length,
187
+ credentials: Object.keys(portable.credentials).length,
188
+ needsLogin: portable.config.accounts.filter((a) => !Object.hasOwn(portable.credentials, a.id) && !existing(a.id)).length,
189
+ routes: Object.keys(portable.config.routes).length,
190
+ overwrittenRoutes: Object.keys(portable.config.routes).filter((id) => Object.hasOwn(manager.config.routes, id)).length
191
+ };
192
+ }
193
+ async function prepareMigration(manager, raw, portable, mode) {
194
+ migrationSummary(manager, portable, mode);
195
+ const base = join2(dirname2(manager.file), "accounts");
196
+ await mkdir(base, { recursive: true, mode: 448 });
197
+ const staging = await mkdtemp(join2(base, "migration-"));
198
+ try {
199
+ const imported = [];
200
+ for (const a of portable.config.accounts) {
201
+ const old = mode === "merge" && raw.accounts.find((b) => b.id === a.id), secret = Object.hasOwn(portable.credentials, a.id) ? portable.credentials[a.id] : void 0;
202
+ if (!secret && old) {
203
+ imported.push({ ...old, ...a });
204
+ continue;
205
+ }
206
+ const credentialFile = join2(staging, a.id, "credential.json"), identityFile = join2(staging, a.id, "identity.json");
207
+ if (secret) await importAccount({ provider: a.provider, credentialFile, identityFile, document: secret.document, identityDocument: secret.identity });
208
+ else {
209
+ await writePrivateJson(credentialFile, {});
210
+ if (a.provider.startsWith("trae")) await writePrivateJson(identityFile, {});
211
+ }
212
+ imported.push({
213
+ ...a,
214
+ ...!secret ? { enabled: false } : {},
215
+ credentialFile: relative(dirname2(manager.file), credentialFile),
216
+ ...a.provider.startsWith("trae") ? { identityFile: relative(dirname2(manager.file), identityFile) } : {}
217
+ });
218
+ }
219
+ const next = mode === "replace" ? { ...portable.config, accounts: imported } : {
220
+ ...raw,
221
+ ...pick(portable.config, options),
222
+ accounts: [...raw.accounts.map((a) => imported.find((b) => b.id === a.id) ?? a), ...imported.filter((a) => !raw.accounts.some((b) => b.id === a.id))],
223
+ routes: { ...raw.routes, ...portable.config.routes },
224
+ 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] }]))
225
+ };
226
+ validateConfig(next, manager.file);
227
+ return { raw: next, staging };
228
+ } catch (error) {
229
+ await rm(staging, { recursive: true, force: true });
230
+ throw error;
231
+ }
232
+ }
233
+
234
+ // src/manager.mjs
235
+ var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
236
+ var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
237
+ var ManagementError = class extends Error {
238
+ constructor(status, message) {
239
+ super(message);
240
+ this.status = status;
241
+ }
242
+ };
243
+ var bad = (message) => {
244
+ throw new ManagementError(400, message);
245
+ };
246
+ var finite = (value, fallback) => Number.isSafeInteger(value) && value > 0 ? value : fallback;
247
+ function modelInfo(row) {
248
+ const billing = row.billing ? {
249
+ free: row.billing.free === true,
250
+ rateUnknown: row.billing.rateUnknown === true,
251
+ ...typeof row.billing.credits === "string" ? { credits: row.billing.credits } : {}
252
+ } : void 0;
253
+ const reasoning = row.reasoning?.supported ? {
254
+ supports: true,
255
+ supportedEfforts: row.reasoning.supported,
256
+ defaultEffort: row.reasoning.defaultEffort,
257
+ canDisableThinking: row.reasoning.supported.includes("off")
258
+ } : row.reasoning;
259
+ return {
260
+ id: row.id,
261
+ name: row.name || row.id,
262
+ contextWindow: finite(row.contextWindow, 2e5),
263
+ maxTokens: Math.min(finite(row.maxTokens, 32e3), finite(row.contextWindow, 2e5)),
264
+ supportsImages: row.supportsImages === true || row.multimodal === true || row.input?.includes("image") === true,
265
+ ...reasoning ? { reasoning } : {},
266
+ ...billing ? { billing } : {},
267
+ ...typeof row.creditMultiplier === "number" && Number.isFinite(row.creditMultiplier) && row.creditMultiplier >= 0 ? { creditMultiplier: row.creditMultiplier } : {},
268
+ ...Array.isArray(row.promotions) ? { promotions: row.promotions.map((p) => ({ start: p.start, end: p.end, factor: p.factor, priority: p.priority, label: typeof p.label === "string" ? p.label : "" })) } : {}
269
+ };
270
+ }
271
+ var Manager = class {
272
+ constructor(file, router, onChange = () => {
273
+ }) {
274
+ this.file = file;
275
+ this.router = router;
276
+ this.onChange = onChange;
277
+ this.catalogFile = join3(dirname3(file), "catalogs.json");
278
+ this.catalogs = /* @__PURE__ */ new Map();
279
+ this.catalogErrors = /* @__PURE__ */ new Map();
280
+ this.refreshes = /* @__PURE__ */ new Map();
281
+ this.closed = false;
282
+ this.monitorFlights = /* @__PURE__ */ new Map();
283
+ this.loginInfo = /* @__PURE__ */ new Map();
284
+ this.telemetry = new Telemetry(join3(dirname3(file), "usage.json"));
285
+ router.telemetry = this.telemetry;
286
+ router.monitor = (a) => this.monitorAccount(a);
287
+ router.prepareFree = () => this.checkFreeModels(true);
288
+ router.freeModels = (account) => this.freeModels(account);
289
+ }
290
+ async load() {
291
+ this.raw = JSON.parse(await readFile(this.file, "utf8"));
292
+ this.config = validateConfig(this.raw, this.file);
293
+ try {
294
+ const saved = JSON.parse(await readFile(this.catalogFile, "utf8"));
295
+ for (const account of this.config.accounts) {
296
+ const item = saved[account.id];
297
+ if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
298
+ this.catalogs.set(account.id, { ...item, models: item.models.filter((m) => typeof m.id === "string" && m.id.length > 0).map(modelInfo) });
299
+ }
300
+ }
301
+ } catch {
302
+ }
303
+ this.telemetry.configure(this.config.usageRetentionDays);
304
+ await this.telemetry.load();
305
+ await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
306
+ this.publish();
307
+ }
308
+ async readLogin(account) {
309
+ try {
310
+ const raw = await jsonCredential(account.credentialFile), c = raw.credential;
311
+ this.loginInfo.set(account.id, account.provider.startsWith("qoder") ? { state: raw.pat ? "configured" : "missing", kind: "PAT", refreshable: false } : {
312
+ state: !c?.accessToken ? "missing" : c?.expiresAtMs > Date.now() ? "valid" : c?.refreshToken ? "refresh-needed" : "expired",
313
+ name: c?.nickname || c?.accountName || c?.uid || c?.userId,
314
+ identity: c?.uid || c?.userId,
315
+ expiresAt: c?.expiresAtMs,
316
+ refreshExpiresAt: c?.refreshExpiresAtMs,
317
+ refreshable: !!c?.refreshToken,
318
+ kind: "\u5BA2\u6237\u7AEF\u767B\u5F55\u6001",
319
+ edition: c?.edition
320
+ });
321
+ } catch {
322
+ this.loginInfo.set(account.id, { state: "missing" });
323
+ }
324
+ }
325
+ async monitorAccount(account) {
326
+ if (this.monitorFlights.has(account.id)) return this.monitorFlights.get(account.id);
327
+ const promise = (async () => {
328
+ try {
329
+ const client = await this.router.client(account);
330
+ if (!client.accountInfo) return;
331
+ const snapshot = await client.accountInfo(AbortSignal.timeout(3e4));
332
+ if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
333
+ this.loginInfo.set(account.id, snapshot.login);
334
+ await this.telemetry.observe(account.id, snapshot);
335
+ } catch (e) {
336
+ if (this.closed) return;
337
+ const old = this.telemetry.row("accounts", account.id).balance;
338
+ await this.telemetry.observe(account.id, { ...old, error: { code: "usage_fetch_failed", status: Number(e.status ?? e.failure?.status) || 502 }, attemptedAt: Date.now() });
339
+ await this.readLogin(account);
340
+ }
341
+ })().finally(() => this.monitorFlights.delete(account.id));
342
+ this.monitorFlights.set(account.id, promise);
343
+ return promise;
344
+ }
345
+ async monitorAll(provider) {
346
+ await Promise.all(this.config.accounts.filter((a) => !provider || a.provider === provider).map((a) => this.monitorAccount(a)));
347
+ }
348
+ freeAccounts() {
349
+ const targets = Object.values(this.config.routes).filter((r) => r.freeOnly).flatMap((r) => r.targets);
350
+ return this.config.accounts.filter((a) => a.enabled !== false && targets.some((t) => t.provider === a.provider && (!t.account || t.account === a.id)));
351
+ }
352
+ freeModels(account) {
353
+ const saved = this.catalogs.get(account.id);
354
+ if (this.catalogErrors.has(account.id) || !saved || beijingDay(saved.fetchedAt) !== beijingDay(Date.now())) return [];
355
+ return saved.models.map((m) => modelWithCurrentPromotion(m)).filter((m) => isFreeModel(m));
356
+ }
357
+ async checkFreeModels(force = false) {
358
+ await Promise.all(this.freeAccounts().filter((a) => {
359
+ const saved = this.catalogs.get(a.id);
360
+ if (force) return true;
361
+ if (Date.now() - (this.catalogErrors.get(a.id)?.at ?? 0) < 3e5) return false;
362
+ return !saved || beijingDay(saved.fetchedAt) !== beijingDay(Date.now()) || saved.pricingVersion !== 1;
363
+ }).map((a) => this.refreshAccount(a, "pricing")));
364
+ }
365
+ freeStatus(row) {
366
+ const accounts = this.config.accounts.filter((a) => a.enabled !== false && row.targets.some((t) => t.provider === a.provider && (!t.account || t.account === a.id)));
367
+ return {
368
+ checking: accounts.some((a) => this.refreshes.has(a.id)),
369
+ accounts: accounts.map((a) => ({
370
+ id: a.id,
371
+ provider: a.provider,
372
+ name: a.name ?? a.id,
373
+ checkedAt: this.catalogs.get(a.id)?.fetchedAt,
374
+ error: this.catalogErrors.get(a.id),
375
+ models: this.freeModels(a).map((m) => ({ id: m.id, name: m.name }))
376
+ }))
377
+ };
378
+ }
379
+ freeRoutes(parentId, row) {
380
+ const groups = /* @__PURE__ */ new Map();
381
+ for (const account of this.config.accounts.filter((a) => a.enabled !== false)) {
382
+ for (const model of this.freeModels(account)) {
383
+ if (!row.targets.some((t) => t.provider === account.provider && (!t.account || t.account === account.id) && (!t.model || t.model === model.id) && (!t.models || t.models.includes(model.id)))) continue;
384
+ const id = `free_${createHash("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
385
+ if (!groups.has(id)) groups.set(id, { id, models: [], targets: [], accountNames: [], modelName: model.name, provider: account.provider, modelId: model.id });
386
+ const group = groups.get(id);
387
+ group.models.push({ model, provider: account.provider });
388
+ group.targets.push({ provider: account.provider, account: account.id, model: model.id });
389
+ group.accountNames.push(account.name ?? account.id);
390
+ }
391
+ }
392
+ return [...groups.values()].map(({ models, ...group }) => {
393
+ const preference = row.freeModels?.[group.id] ?? {}, reasoning = routeReasoning(models);
394
+ const selected = preference.reasoningEffort;
395
+ return {
396
+ ...group,
397
+ parentId,
398
+ dynamicFree: true,
399
+ freeOnly: true,
400
+ strategy: "failover",
401
+ preferExpiringCredits: false,
402
+ name: `Free \xB7 ${group.modelName} \xB7 ${PROVIDER_NAMES[group.provider]}`,
403
+ reasoning,
404
+ ...selected && reasoning?.supportedEfforts.includes(selected) ? { reasoningEffort: selected } : {},
405
+ configuredEffort: selected ?? null,
406
+ effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
407
+ visible: row.visible !== false && preference.visible !== false,
408
+ selected: preference.visible !== false,
409
+ supportsImages: models.every(({ model }) => model.supportsImages),
410
+ contextWindow: Math.min(row.contextWindow, ...models.map(({ model }) => model.contextWindow)),
411
+ maxTokens: Math.min(row.maxTokens, ...models.map(({ model }) => model.maxTokens), ...models.map(({ model }) => model.contextWindow))
412
+ };
413
+ });
414
+ }
415
+ adapterModels() {
416
+ const rows = this.routeModels();
417
+ return [
418
+ ...rows.filter((r) => r.freeOnly).flatMap((r) => this.freeRoutes(r.id, r)),
419
+ ...rows.filter((r) => !r.freeOnly),
420
+ ...rows.filter((r) => r.freeOnly).map((r) => ({ ...r, visible: false }))
421
+ ];
422
+ }
423
+ publish() {
424
+ this.telemetry.configure(this.config.usageRetentionDays);
425
+ this.revision = revisionOf(this.raw);
426
+ const routes = { ...this.config.routes };
427
+ for (const model of this.adapterModels().filter((m) => m.dynamicFree)) routes[model.id] = model;
428
+ for (const provider of PROVIDERS) for (const model of this.providerModels(provider)) {
429
+ routes[`${provider}::${model.id}`] = { ...model, targets: [{ provider, model: model.id }] };
430
+ }
431
+ this.router.updateConfig({ ...this.config, routes });
432
+ this.onChange();
433
+ }
434
+ providerModels(provider) {
435
+ const rows = /* @__PURE__ */ new Map();
436
+ for (const account of this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false)) {
437
+ for (const model of this.catalogs.get(account.id)?.models ?? []) {
438
+ const old = rows.get(model.id);
439
+ rows.set(model.id, old ? {
440
+ ...old,
441
+ contextWindow: Math.min(old.contextWindow, model.contextWindow),
442
+ maxTokens: Math.min(old.maxTokens, model.maxTokens),
443
+ supportsImages: old.supportsImages && model.supportsImages,
444
+ reasoning: routeReasoning([{ model: old, provider }, { model, provider }])
445
+ } : {
446
+ ...model,
447
+ ...reasoningModes(model, provider).length ? { reasoning: routeReasoning([{ model, provider }]) } : {}
448
+ });
449
+ }
450
+ }
451
+ return [...rows.values()].map((model) => ({
452
+ ...model,
453
+ name: this.config.models[provider]?.[model.id]?.name ?? model.name
454
+ }));
455
+ }
456
+ hiddenModels(provider) {
457
+ return this.providerModels(provider).filter((m) => !this.modelVisible(provider, m.id)).map((m) => m.id);
458
+ }
459
+ modelVisible(provider, id) {
460
+ return this.config.models[provider]?.[id]?.visible ?? this.config.modelVisibility[provider] ?? false;
461
+ }
462
+ routeModels() {
463
+ return Object.entries(this.config.routes).sort(([, a], [, b]) => Number(!!b.freeOnly) - Number(!!a.freeOnly)).map(([id, row]) => {
464
+ const effectiveTargets = row.freeOnly ? this.freeRoutes(id, row).flatMap((r) => r.targets) : row.targets;
465
+ 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) => {
466
+ const catalog = this.catalogs.get(a.id)?.models ?? [];
467
+ const models2 = target.models ? target.models.map((model) => catalog.find((m) => m.id === model)).filter(Boolean) : [matchModel(id, target, catalog)].filter(Boolean);
468
+ return models2.map((model) => ({ model, provider: a.provider }));
469
+ }));
470
+ const models = sources.map((s) => s.model), reasoning = routeReasoning(sources);
471
+ return {
472
+ ...row,
473
+ supportsImages: models.length > 0 && models.every((m) => m.supportsImages),
474
+ reasoning,
475
+ ...row.freeOnly && models.length ? { contextWindow: Math.min(row.contextWindow, ...models.map((m) => m.contextWindow)), maxTokens: Math.min(row.maxTokens, ...models.map((m) => m.maxTokens), ...models.map((m) => m.contextWindow)) } : {},
476
+ id,
477
+ name: row.name ?? id,
478
+ expiringStatus: { ...this.telemetry.data.expiringRounds[id] },
479
+ ...row.freeOnly ? { freeStatus: { ...this.freeStatus(row), modelRoutes: this.freeRoutes(id, row) } } : {}
480
+ };
481
+ });
482
+ }
483
+ async ensureCatalog(provider, force = false) {
484
+ await Promise.all(this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false).map((a) => this.refreshAccount(a, force)));
485
+ }
486
+ async refreshAccount(account, force) {
487
+ const saved = this.catalogs.get(account.id);
488
+ if (!force && saved && Date.now() - saved.fetchedAt < this.config.catalogTtlMs) return;
489
+ if (!force && Date.now() - (this.catalogErrors.get(account.id)?.at ?? 0) < 1e4) return;
490
+ if (this.refreshes.has(account.id)) return this.refreshes.get(account.id);
491
+ const promise = (async () => {
492
+ try {
493
+ if (force === true) this.router.clients.delete(account.id);
494
+ const client = await this.router.client(account);
495
+ const models = (await client.models(AbortSignal.timeout(6e4), !!force)).map(modelInfo);
496
+ if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
497
+ this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
498
+ this.catalogErrors.delete(account.id);
499
+ await withFileLock(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
500
+ } catch (error) {
501
+ if (this.closed || !this.config.accounts.some((a) => a.id === account.id)) return;
502
+ this.catalogErrors.set(account.id, { status: Number(error.status ?? error.failure?.status) || 502, code: "catalog_fetch_failed", at: Date.now() });
503
+ }
504
+ if (!this.closed) this.publish();
505
+ })().finally(() => this.refreshes.delete(account.id));
506
+ this.refreshes.set(account.id, promise);
507
+ return promise;
508
+ }
509
+ state() {
510
+ const status = this.router.status();
511
+ return {
512
+ revision: this.revision,
513
+ telemetry: this.telemetry.state(),
514
+ accounts: this.config.accounts.map((a) => ({
515
+ id: a.id,
516
+ provider: a.provider,
517
+ name: a.name ?? a.id,
518
+ enabled: a.enabled !== false,
519
+ status: status.find((s) => s.id === a.id),
520
+ login: this.loginInfo.get(a.id),
521
+ usage: this.telemetry.row("accounts", a.id),
522
+ modelCount: this.catalogs.get(a.id)?.models.length ?? 0,
523
+ fetchedAt: this.catalogs.get(a.id)?.fetchedAt,
524
+ error: this.catalogErrors.get(a.id)
525
+ })),
526
+ providers: PROVIDERS.map((id) => ({ id, name: PROVIDER_NAMES[id], models: this.providerModels(id).map((m) => ({
527
+ ...m,
528
+ visible: this.modelVisible(id, m.id)
529
+ })) })),
530
+ presets: Object.entries(ROUTE_PRESETS).map(([id, route]) => ({
531
+ id,
532
+ ...structuredClone(route),
533
+ added: Object.hasOwn(this.config.routes, id),
534
+ 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))]
535
+ })),
536
+ routes: this.routeModels(),
537
+ options: {
538
+ cooldownMs: this.config.cooldownMs,
539
+ requestTimeoutMs: this.config.requestTimeoutMs,
540
+ retryServerErrors: this.config.retryServerErrors,
541
+ usageRetentionDays: this.config.usageRetentionDays
542
+ }
543
+ };
544
+ }
545
+ async action(input) {
546
+ if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
547
+ if (input.action === "account.export") {
548
+ if (input.revision !== revisionOf(JSON.parse(await readFile(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u63D0\u53D6\u51ED\u8BC1");
549
+ const account = this.config.accounts.find((a) => a.id === input.id);
550
+ if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
551
+ let credential;
552
+ try {
553
+ credential = await readPortableCredential(account);
554
+ } catch {
555
+ bad("\u8D26\u53F7\u51ED\u8BC1\u4E0D\u53EF\u8BFB\uFF0C\u8BF7\u8865\u5145\u6216\u66F4\u65B0\u540E\u518D\u63D0\u53D6");
556
+ }
557
+ if (!credential) bad("\u8D26\u53F7\u5C1A\u672A\u4FDD\u5B58\u51ED\u8BC1\uFF0C\u8BF7\u5148\u8865\u5145\u767B\u5F55\u4FE1\u606F");
558
+ return { ...this.state(), exported: {
559
+ filename: `dsh-account-${account.provider}-${account.id}.json`,
560
+ document: {
561
+ ...credential.document,
562
+ ...credential.identity ? { identity: credential.identity } : {},
563
+ format: "dsh-multi-router-account",
564
+ provider: account.provider,
565
+ name: account.name || account.id
566
+ }
567
+ } };
568
+ }
569
+ if (input.action === "config.export") {
570
+ if (input.revision !== revisionOf(JSON.parse(await readFile(this.file, "utf8")))) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u5BFC\u51FA");
571
+ try {
572
+ return { ...this.state(), exported: { filename: `dsh-multi-router-${input.includeCredentials ? "encrypted" : "settings"}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`, document: await exportConfiguration(this, input) } };
573
+ } catch (e) {
574
+ bad(e.message);
575
+ }
576
+ }
577
+ if (input.action === "config.preview") {
578
+ try {
579
+ const portable = await decodeConfiguration(input);
580
+ return { ...this.state(), importPreview: migrationSummary(this, portable, input.mode) };
581
+ } catch (e) {
582
+ bad(e.message);
583
+ }
584
+ }
585
+ if (input.action === "monitor.refresh" || input.action === "account.refresh") {
586
+ if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
587
+ if (input.action === "account.refresh") {
588
+ const account = this.config.accounts.find((a) => a.id === input.id);
589
+ if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
590
+ this.router.clients.delete(account.id);
591
+ await this.monitorAccount(account);
592
+ } else await this.monitorAll(input.provider);
593
+ return this.state();
594
+ }
595
+ if (input.action === "account.capture") {
596
+ if (!PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
597
+ input = { ...input, action: "account.add", id: `${input.provider.replace("-", "_")}_${randomBytes2(4).toString("hex")}`, capture: true };
598
+ }
599
+ if (input.action === "refresh") {
600
+ if (input.provider && !PROVIDERS.includes(input.provider)) bad("\u672A\u77E5\u4F9B\u5E94\u5546");
601
+ await Promise.all((input.provider ? [input.provider] : PROVIDERS).map((p) => this.ensureCatalog(p, true)));
602
+ return this.state();
603
+ }
604
+ if (input.action === "free.refresh") {
605
+ await this.checkFreeModels(true);
606
+ return this.state();
607
+ }
608
+ return withFileLock(this.file, async () => {
609
+ let raw = JSON.parse(await readFile(this.file, "utf8"));
610
+ if (input.revision !== revisionOf(raw)) throw new ManagementError(409, "\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u540E\u518D\u4FDD\u5B58");
611
+ raw.routes ??= structuredClone(this.config.routes);
612
+ let staging;
613
+ try {
614
+ const account = raw.accounts.find((a) => a.id === input.id);
615
+ if (input.action === "config.import") {
616
+ try {
617
+ const prepared = await prepareMigration(this, raw, await decodeConfiguration(input), input.mode);
618
+ staging = prepared.staging;
619
+ await writePrivateJson(`${this.file}.before-import-${Date.now()}-${randomBytes2(3).toString("hex")}.bak`, raw);
620
+ raw = prepared.raw;
621
+ } catch (e) {
622
+ bad(e.message);
623
+ }
624
+ } else if (input.action === "account.add" || input.action === "account.credentials") {
625
+ const replacing = input.action === "account.credentials";
626
+ if (replacing) {
627
+ if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
628
+ input = { ...input, provider: account.provider };
629
+ }
630
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(input.id ?? "") || !PROVIDERS.includes(input.provider)) bad("\u8D26\u53F7 ID \u6216\u4F9B\u5E94\u5546\u65E0\u6548");
631
+ if (account && !replacing) bad("\u8D26\u53F7 ID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C ID");
632
+ const base = join3(dirname3(this.file), "accounts");
633
+ await mkdir2(base, { recursive: true, mode: 448 });
634
+ staging = await mkdtemp2(join3(base, `${input.id}-`));
635
+ const credentialFile = join3(staging, "credential.json"), identityFile = join3(staging, "identity.json");
636
+ const added = {
637
+ id: input.id,
638
+ name: input.name || account?.name || input.id,
639
+ provider: input.provider,
640
+ credentialFile: relative2(dirname3(this.file), credentialFile),
641
+ ...input.provider.startsWith("trae") ? { identityFile: relative2(dirname3(this.file), identityFile) } : {}
642
+ };
643
+ if (replacing) raw.accounts[raw.accounts.indexOf(account)] = { ...account, ...added, enabled: true };
644
+ else raw.accounts.push(added);
645
+ validateConfig(raw, this.file);
646
+ if (input.document !== void 0) {
647
+ try {
648
+ await importAccount({ provider: input.provider, credentialFile, identityFile, document: input.document, identityDocument: input.identityDocument });
649
+ } catch {
650
+ bad("\u51ED\u8BC1\u683C\u5F0F\u65E0\u6548\uFF1A\u8BF7\u68C0\u67E5\u5FC5\u586B\u5B57\u6BB5\u3001\u533A\u57DF\u548C Trae \u8BBE\u5907\u8EAB\u4EFD");
651
+ }
652
+ } else if (input.provider.startsWith("qoder") && input.pat) {
653
+ if (typeof input.pat !== "string" || input.pat.length > 8192 || /\s/.test(input.pat.trim())) bad("PAT \u683C\u5F0F\u65E0\u6548");
654
+ await writePrivateJson(credentialFile, {
655
+ version: 2,
656
+ pat: input.pat.trim(),
657
+ region: input.provider === "qoder" ? "china" : "global",
658
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
659
+ });
660
+ } else {
661
+ let source = input.source;
662
+ if (!source && input.provider.startsWith("qoder")) source = qoderOwnAuthPath(QODER_VARIANTS.find((v) => v.id === input.provider));
663
+ if (!source && input.provider.startsWith("workbuddy")) source = defaultDesktopAuthPath(WORKBUDDY_VARIANTS.find((v) => v.id === input.provider));
664
+ if (!source && input.provider.startsWith("trae")) {
665
+ for (const candidate of traeStorageCandidates().filter((c) => c.source === "desktop" && c.edition === input.edition)) {
666
+ try {
667
+ await access(candidate.path);
668
+ source = candidate.path;
669
+ break;
670
+ } catch {
671
+ }
672
+ }
673
+ }
674
+ if (typeof source !== "string" || !source.trim()) bad("\u672A\u627E\u5230\u767B\u5F55\u6587\u4EF6\uFF0C\u8BF7\u586B\u5199\u6765\u6E90\u8DEF\u5F84\u6216 Qoder PAT");
675
+ try {
676
+ await importAccount({
677
+ provider: input.provider,
678
+ source: resolve(source),
679
+ credentialFile,
680
+ identityFile,
681
+ edition: input.edition,
682
+ identitySource: input.identity ? resolve(input.identity) : void 0
683
+ });
684
+ } catch {
685
+ bad("\u8D26\u53F7\u5BFC\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u767B\u5F55\u6587\u4EF6\u3001\u533A\u57DF\u3001Trae edition \u548C\u8BBE\u5907\u8EAB\u4EFD");
686
+ }
687
+ }
688
+ const imported = await jsonCredential(credentialFile);
689
+ const fingerprint = (value) => input.provider.startsWith("qoder") ? value.pat : value.credential?.uid || value.credential?.userId;
690
+ for (const existing of input.capture || input.document !== void 0 ? this.config.accounts.filter((a) => a.provider === input.provider && a.id !== input.id) : []) {
691
+ let value;
692
+ try {
693
+ value = await jsonCredential(existing.credentialFile);
694
+ } catch {
695
+ continue;
696
+ }
697
+ if (fingerprint(value) && fingerprint(value) === fingerprint(imported)) bad(`\u6B64\u767B\u5F55\u8D26\u53F7\u5DF2\u5728\u8D26\u53F7\u6C60\u4E2D\uFF1A${existing.name || existing.id}\uFF0C\u8BF7\u5207\u6362\u5BA2\u6237\u7AEF\u8D26\u53F7\u540E\u518D\u83B7\u53D6`);
698
+ }
699
+ if (!input.name && !replacing) added.name = imported.credential?.nickname || imported.credential?.accountName || input.id;
700
+ } else if (input.action === "account.update") {
701
+ if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
702
+ if (input.name !== void 0) account.name = input.name;
703
+ if (input.enabled !== void 0) account.enabled = input.enabled;
704
+ } else if (input.action === "account.move") {
705
+ if (!account || ![-1, 1].includes(input.direction)) bad("\u8D26\u53F7\u6216\u987A\u5E8F\u65E0\u6548");
706
+ const group = raw.accounts.filter((a) => a.provider === account.provider), position = group.indexOf(account) + input.direction;
707
+ if (position >= 0 && position < group.length) {
708
+ const index = raw.accounts.indexOf(account), next = raw.accounts.indexOf(group[position]);
709
+ [raw.accounts[index], raw.accounts[next]] = [raw.accounts[next], raw.accounts[index]];
710
+ }
711
+ } else if (input.action === "account.remove") {
712
+ if (!account) bad("\u8D26\u53F7\u4E0D\u5B58\u5728");
713
+ if (Object.values(raw.routes).some((r) => r.targets.some((t) => t.account === input.id))) bad("\u8BF7\u5148\u79FB\u9664\u8DEF\u7531\u4E2D\u7ED1\u5B9A\u6B64\u8D26\u53F7\u7684\u76EE\u6807");
714
+ raw.accounts = raw.accounts.filter((a) => a.id !== input.id);
715
+ } else if (input.action === "models.visibility") {
716
+ if (input.provider !== void 0 && !PROVIDERS.includes(input.provider) || typeof input.visible !== "boolean") bad("\u4F9B\u5E94\u5546\u6216\u5C55\u793A\u5F00\u5173\u65E0\u6548");
717
+ raw.modelVisibility = { ...raw.modelVisibility };
718
+ raw.models = { ...raw.models };
719
+ for (const provider of input.provider ? [input.provider] : PROVIDERS) {
720
+ raw.modelVisibility[provider] = input.visible;
721
+ raw.models[provider] = Object.fromEntries(Object.entries(raw.models[provider] ?? {}).map(([id, pref]) => [id, { ...pref, visible: input.visible }]));
722
+ }
723
+ } else if (input.action === "model.update") {
724
+ if (!PROVIDERS.includes(input.provider) || typeof input.id !== "string" || !input.id || input.id.length > 256) bad("\u6A21\u578B\u65E0\u6548");
725
+ raw.models = { ...raw.models, [input.provider]: {
726
+ ...raw.models?.[input.provider],
727
+ [input.id]: {
728
+ ...raw.models?.[input.provider]?.[input.id],
729
+ ...input.name !== void 0 ? { name: input.name } : {},
730
+ ...input.visible !== void 0 ? { visible: input.visible } : {}
731
+ }
732
+ } };
733
+ } else if (input.action === "free.model.update") {
734
+ const parent = raw.routes[input.parentId];
735
+ 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");
736
+ const model = this.freeRoutes(input.parentId, this.config.routes[input.parentId]).find((m) => m.id === input.id);
737
+ 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");
738
+ parent.freeModels = { ...parent.freeModels, [input.id]: {
739
+ ...parent.freeModels?.[input.id],
740
+ ...input.visible !== void 0 ? { visible: input.visible } : {},
741
+ ...input.reasoningEffort !== void 0 ? { reasoningEffort: input.reasoningEffort } : {}
742
+ } };
743
+ } else if (input.action === "route.save") {
744
+ if (input.id === void 0 || input.id === "") {
745
+ input.id = input.previousId || `route_${randomBytes2(8).toString("hex")}`;
746
+ }
747
+ if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(input.id ?? "")) bad("\u8DEF\u7531 ID \u4EC5\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF");
748
+ if (input.previousId && !Object.hasOwn(raw.routes, input.previousId)) bad("\u539F\u8DEF\u7531\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u9875\u9762");
749
+ if (input.previousId !== input.id && Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531 ID \u5DF2\u5B58\u5728");
750
+ raw.routes = { ...raw.routes, [input.id]: input.route };
751
+ if (input.previousId && input.previousId !== input.id) delete raw.routes[input.previousId];
752
+ } else if (input.action === "route.force-expiring") {
753
+ if (!Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531\u4E0D\u5B58\u5728");
754
+ raw.routes[input.id].preferExpiringCredits = true;
755
+ await this.telemetry.restartExpiring(input.id);
756
+ const route = this.router.config.routes[input.id], strategy = route.strategy ?? "failover";
757
+ if (["tokens", "credits"].includes(strategy)) await this.telemetry.advance(input.id, this.router.candidates(input.id), JSON.stringify({ strategy, targets: route.targets }));
758
+ } else if (input.action === "route.remove") {
759
+ if (!Object.hasOwn(raw.routes, input.id)) bad("\u8DEF\u7531\u4E0D\u5B58\u5728");
760
+ delete raw.routes[input.id];
761
+ } else if (input.action === "preset.add") {
762
+ const id = input.id ?? "deepseek-v4.1-flash";
763
+ if (!Object.hasOwn(ROUTE_PRESETS, id)) bad("\u672A\u77E5\u9884\u914D\u7F6E\u8DEF\u7531");
764
+ if (Object.hasOwn(raw.routes, id)) bad("\u9884\u914D\u7F6E\u8DEF\u7531\u5DF2\u5B58\u5728\uFF0C\u4E0D\u8986\u76D6\u5DF2\u6709\u8BBE\u7F6E");
765
+ raw.routes[id] = structuredClone(ROUTE_PRESETS[id]);
766
+ } else if (input.action === "preset.add-all") {
767
+ for (const [id, route] of Object.entries(ROUTE_PRESETS)) if (!Object.hasOwn(raw.routes, id)) raw.routes[id] = structuredClone(route);
768
+ } else if (input.action === "options.save") {
769
+ for (const key of ["cooldownMs", "requestTimeoutMs", "retryServerErrors", "usageRetentionDays"]) if (input[key] !== void 0) raw[key] = input[key];
770
+ } else bad("\u672A\u77E5\u64CD\u4F5C");
771
+ let config;
772
+ try {
773
+ config = validateConfig(raw, this.file);
774
+ } catch (error) {
775
+ bad(error.message);
776
+ }
777
+ await writePrivateJson(this.file, raw);
778
+ staging = void 0;
779
+ this.raw = raw;
780
+ this.config = config;
781
+ for (const id of this.catalogs.keys()) if (!config.accounts.some((a) => a.id === id && a.credentialFile === this.catalogs.get(id).credentialFile)) {
782
+ this.catalogs.delete(id);
783
+ this.catalogErrors.delete(id);
784
+ }
785
+ await Promise.all(config.accounts.map((a) => this.readLogin(a)));
786
+ this.publish();
787
+ await this.telemetry.persist();
788
+ } finally {
789
+ if (staging) await rm2(staging, { recursive: true, force: true });
790
+ }
791
+ return this.state();
792
+ }, { waitMs: 3e4 }).then(async (state) => {
793
+ if (input.action === "config.import") {
794
+ void Promise.all([this.monitorAll(), ...PROVIDERS.map((p) => this.ensureCatalog(p, true))]).catch(() => {
795
+ });
796
+ }
797
+ if (input.action === "preset.add" || input.action === "preset.add-all") {
798
+ await this.checkFreeModels();
799
+ return this.state();
800
+ }
801
+ if (input.action === "account.add" || input.action === "account.credentials") {
802
+ const account = this.config.accounts.find((a) => a.id === input.id);
803
+ if (account) {
804
+ await Promise.all([this.refreshAccount(account, true), this.monitorAccount(account)]);
805
+ return this.state();
806
+ }
807
+ }
808
+ return state;
809
+ });
810
+ }
811
+ watch(onError) {
812
+ let timer;
813
+ const watcher = watch(dirname3(this.file), (_, filename) => {
814
+ if (filename && String(filename) !== this.file.split(/[\\/]/).pop()) return;
815
+ clearTimeout(timer);
816
+ timer = setTimeout(() => {
817
+ void withFileLock(this.file, async () => {
818
+ const raw = JSON.parse(await readFile(this.file, "utf8"));
819
+ if (this.closed || revisionOf(raw) === this.revision) return;
820
+ const config = validateConfig(raw, this.file);
821
+ this.raw = raw;
822
+ this.config = config;
823
+ this.publish();
824
+ await this.telemetry.persist();
825
+ }).catch(() => onError?.());
826
+ }, 150);
827
+ });
828
+ return () => {
829
+ this.closed = true;
830
+ clearTimeout(timer);
831
+ watcher.close();
832
+ };
833
+ }
834
+ };
835
+ function managementHandler(manager) {
836
+ const key = randomBytes2(32).toString("base64url");
837
+ const json = (res, status, value) => {
838
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
839
+ res.end(JSON.stringify(value));
840
+ };
841
+ return async (req, res) => {
842
+ try {
843
+ const remote = req.socket?.remoteAddress;
844
+ if (!hostIsLoopback(req.headers.host) || remote && !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) throw new ManagementError(403, "\u4EC5\u5141\u8BB8\u672C\u673A\u8BF7\u6C42");
845
+ if (req.headers.origin && req.headers.origin !== `http://${req.headers.host}` && req.headers.origin !== `https://${req.headers.host}`) throw new ManagementError(403, "\u6765\u6E90\u65E0\u6548");
846
+ if (req.headers["sec-fetch-site"] === "cross-site") throw new ManagementError(403, "\u6765\u6E90\u65E0\u6548");
847
+ if (req.method === "GET") {
848
+ json(res, 200, { ...manager.state(), key });
849
+ return;
850
+ }
851
+ if (req.method !== "POST") throw new ManagementError(405, "\u65B9\u6CD5\u65E0\u6548");
852
+ const actual = Buffer.from(req.headers["x-multi-router-key"] ?? ""), expected = Buffer.from(key);
853
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new ManagementError(401, "\u8BF7\u5237\u65B0\u914D\u7F6E\u9875\u9762");
854
+ if (!/^application\/json(?:\s*;|$)/i.test(req.headers["content-type"] ?? "")) throw new ManagementError(415, "\u9700\u8981 JSON");
855
+ let size = 0, chunks = [];
856
+ for await (const chunk of req) {
857
+ size += chunk.length;
858
+ if (size > 1024 * 1024) throw new ManagementError(413, "\u8BF7\u6C42\u8FC7\u5927");
859
+ chunks.push(chunk);
860
+ }
861
+ let input;
862
+ try {
863
+ input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
864
+ } catch {
865
+ throw new ManagementError(400, "JSON \u65E0\u6548");
866
+ }
867
+ json(res, 200, { ...await manager.action(input), key });
868
+ } catch (error) {
869
+ json(
870
+ res,
871
+ error instanceof ManagementError ? error.status : 500,
872
+ { error: error instanceof ManagementError ? error.message : "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u548C\u6765\u6E90\u6587\u4EF6" }
873
+ );
874
+ }
875
+ };
876
+ }
877
+
878
+ export {
879
+ MANAGEMENT_PATH,
880
+ ManagementError,
881
+ modelInfo,
882
+ Manager,
883
+ managementHandler
884
+ };
885
+ //# sourceMappingURL=chunk-BIEDP6NW.js.map