dsh-multi-router 0.5.9 → 0.6.1

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 (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +46 -6
  3. package/THIRD_PARTY_NOTICES.md +4 -0
  4. package/cordis.patch.yml +2 -2
  5. package/lib/accounts.js +4 -4
  6. package/lib/api-service.js +11 -0
  7. package/lib/api-service.js.map +7 -0
  8. package/lib/{chunk-U2MU2M5C.js → chunk-2LH6TD2U.js} +25 -115
  9. package/lib/chunk-2LH6TD2U.js.map +7 -0
  10. package/lib/chunk-4V52V5GO.js +105 -0
  11. package/lib/chunk-4V52V5GO.js.map +7 -0
  12. package/lib/chunk-625KMEYS.js +332 -0
  13. package/lib/chunk-625KMEYS.js.map +7 -0
  14. package/lib/{chunk-P43AWJ4Q.js → chunk-BSAI3SW5.js} +3 -3
  15. package/lib/{chunk-J3XO7RTF.js → chunk-DZ4ZUAYH.js} +50 -6
  16. package/lib/chunk-DZ4ZUAYH.js.map +7 -0
  17. package/lib/{chunk-MCHV2R25.js → chunk-NGVWVIBM.js} +22 -6
  18. package/lib/chunk-NGVWVIBM.js.map +7 -0
  19. package/lib/{chunk-FTM7RHQV.js → chunk-OK2T2UAZ.js} +4 -4
  20. package/lib/{chunk-S6ZQ45SS.js → chunk-VZPXRK7Q.js} +3 -3
  21. package/lib/{chunk-I4EW5TI7.js → chunk-ZQZSXCVR.js} +164 -48
  22. package/lib/chunk-ZQZSXCVR.js.map +7 -0
  23. package/lib/cli.js +7 -6
  24. package/lib/cli.js.map +1 -1
  25. package/lib/client.js +462 -44
  26. package/lib/client.js.map +2 -2
  27. package/lib/clients.js +9 -5
  28. package/lib/{host-v0.5.9.js → host-v0.6.1.js} +19 -13
  29. package/lib/host.js +19 -13
  30. package/lib/index.js +19 -13
  31. package/lib/index.js.map +2 -2
  32. package/lib/manager.js +11 -7
  33. package/lib/router.js +2 -2
  34. package/lib/shim.js +3 -3
  35. package/package.json +1 -1
  36. package/lib/chunk-I4EW5TI7.js.map +0 -7
  37. package/lib/chunk-J3XO7RTF.js.map +0 -7
  38. package/lib/chunk-MCHV2R25.js.map +0 -7
  39. package/lib/chunk-U2MU2M5C.js.map +0 -7
  40. /package/lib/{chunk-P43AWJ4Q.js.map → chunk-BSAI3SW5.js.map} +0 -0
  41. /package/lib/{chunk-FTM7RHQV.js.map → chunk-OK2T2UAZ.js.map} +0 -0
  42. /package/lib/{chunk-S6ZQ45SS.js.map → chunk-VZPXRK7Q.js.map} +0 -0
@@ -6,41 +6,45 @@ import {
6
6
  refreshWorkBuddyCheckinCredential,
7
7
  supportsCheckin,
8
8
  workbuddyToken
9
- } from "./chunk-U2MU2M5C.js";
9
+ } from "./chunk-2LH6TD2U.js";
10
10
  import {
11
11
  discoverLocalAccounts,
12
12
  importAccount,
13
13
  localAccountKey,
14
14
  normalizeLocalAccountKey,
15
15
  readPortableCredential
16
- } from "./chunk-S6ZQ45SS.js";
16
+ } from "./chunk-VZPXRK7Q.js";
17
+ import {
18
+ modelWithCurrentPromotion
19
+ } from "./chunk-TEZ2FMEO.js";
20
+ import {
21
+ ApiService
22
+ } from "./chunk-625KMEYS.js";
17
23
  import {
18
24
  hostIsLoopback
19
25
  } from "./chunk-DNQPZMPF.js";
20
26
  import {
21
27
  Telemetry,
22
- matchModel
23
- } from "./chunk-J3XO7RTF.js";
28
+ contextInfo,
29
+ matchModel,
30
+ resolveContext
31
+ } from "./chunk-DZ4ZUAYH.js";
24
32
  import {
25
33
  PROVIDERS,
26
34
  PROVIDER_NAMES,
27
35
  ROUTE_PRESETS,
28
36
  beijingDay,
29
37
  isFreeModel,
30
- reasoningModes,
31
38
  routeReasoning,
32
39
  validateConfig,
33
40
  writePrivateJson
34
- } from "./chunk-MCHV2R25.js";
35
- import {
36
- modelWithCurrentPromotion
37
- } from "./chunk-TEZ2FMEO.js";
41
+ } from "./chunk-NGVWVIBM.js";
38
42
 
39
43
  // src/manager.mjs
40
44
  import { createHash, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
41
45
 
42
46
  // package.json
43
- var version = "0.5.9";
47
+ var version = "0.6.1";
44
48
 
45
49
  // src/manager.mjs
46
50
  import { readFile as readFile4, mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
@@ -102,14 +106,14 @@ var derive = promisify(scrypt);
102
106
  var FORMAT = "dsh-multi-router";
103
107
  var MAX_BYTES = 512 * 1024;
104
108
  var pick = (value, keys) => Object.fromEntries(keys.filter((k) => Object.hasOwn(value ?? {}, k)).map((k) => [k, value[k]]));
105
- var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays", "checkinSchedule"];
109
+ var options = ["cooldownMs", "requestTimeoutMs", "catalogTtlMs", "retryServerErrors", "modelVisibility", "usageRetentionDays", "checkinSchedule", "sidebarUsage"];
106
110
  function settings(config) {
107
111
  return {
108
112
  ...pick(config, options),
109
- 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"])]))])),
110
114
  accounts: config.accounts.map((a) => pick(a, ["id", "provider", "name", "enabled", "autoCheckin"])),
111
115
  routes: Object.fromEntries(Object.entries(config.routes).map(([id, r]) => [id, {
112
- ...pick(r, ["name", "visible", "strategy", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
116
+ ...pick(r, ["name", "visible", "strategy", "contextMode", "contextWindow", "maxTokens", "preferExpiringCredits", "expiringWithinDays", "freeOnly", "freeModels"]),
113
117
  targets: r.targets.map((t) => pick(t, ["provider", "account", "model", "models", "weight", "tokenBudget", "creditBudget"]))
114
118
  }]))
115
119
  };
@@ -1003,6 +1007,7 @@ var WorkBuddyLogin = class {
1003
1007
 
1004
1008
  // src/manager.mjs
1005
1009
  var MANAGEMENT_PATH = "/.well-known/dsh-multi-router";
1010
+ var SUMMARY_PATH = `${MANAGEMENT_PATH}/summary`;
1006
1011
  var revisionOf = (raw) => createHash("sha256").update(JSON.stringify(raw)).digest("hex");
1007
1012
  var ManagementError = class extends Error {
1008
1013
  constructor(status, message) {
@@ -1029,8 +1034,8 @@ function modelInfo(row) {
1029
1034
  return {
1030
1035
  id: row.id,
1031
1036
  name: row.name || row.id,
1032
- contextWindow: finite(row.contextWindow, 2e5),
1033
- maxTokens: Math.min(finite(row.maxTokens, 32e3), finite(row.contextWindow, 2e5)),
1037
+ ...contextInfo(row),
1038
+ maxTokens: Math.min(finite(row.maxTokens, 32e3), contextInfo(row).maxContextWindow),
1034
1039
  supportsImages: row.supportsImages === true || row.multimodal === true || row.input?.includes("image") === true,
1035
1040
  ...reasoning ? { reasoning } : {},
1036
1041
  ...billing ? { billing } : {},
@@ -1060,6 +1065,7 @@ var Manager = class {
1060
1065
  this.checkins = new Checkins(join5(dirname3(file), "checkins.json"), this);
1061
1066
  this.workbuddyLogin = new WorkBuddyLogin(join5(dirname3(file), "workbuddy-login.json"), { fetchImpl: pluginAccess.fetchImpl });
1062
1067
  this.setupFile = join5(dirname3(file), "initial-setup.json");
1068
+ this.api = new ApiService(join5(dirname3(file), "api-service.json"), this);
1063
1069
  router.telemetry = this.telemetry;
1064
1070
  router.monitor = (a) => this.monitorAccount(a, { maxAgeMs: 6e4 });
1065
1071
  router.prepareFree = () => this.checkFreeModels(true);
@@ -1080,13 +1086,14 @@ var Manager = class {
1080
1086
  for (const account of this.config.accounts) {
1081
1087
  const item = saved[account.id];
1082
1088
  if (item?.credentialFile === account.credentialFile && Array.isArray(item.models)) {
1083
- this.catalogs.set(account.id, { ...item, models: item.models.filter((m) => typeof m.id === "string" && m.id.length > 0).map(modelInfo) });
1089
+ 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) });
1084
1090
  }
1085
1091
  }
1086
1092
  } catch {
1087
1093
  }
1088
1094
  this.telemetry.configure(this.config.usageRetentionDays);
1089
1095
  await this.telemetry.load();
1096
+ await this.api.load();
1090
1097
  await Promise.all(this.config.accounts.map((a) => this.readLogin(a)));
1091
1098
  this.publish();
1092
1099
  }
@@ -1278,16 +1285,25 @@ var Manager = class {
1278
1285
  const id = `free_${createHash("sha256").update(JSON.stringify([parentId, account.provider, model.id])).digest("hex").slice(0, 24)}`;
1279
1286
  if (!groups.has(id)) groups.set(id, { id, models: [], targets: [], accountNames: [], modelName: model.name, provider: account.provider, modelId: model.id });
1280
1287
  const group = groups.get(id);
1281
- group.models.push({ model, provider: account.provider });
1288
+ group.models.push({ model, provider: account.provider, accountId: account.id });
1282
1289
  group.targets.push({ provider: account.provider, account: account.id, model: model.id });
1283
1290
  group.accountNames.push(account.name ?? account.id);
1284
1291
  }
1285
1292
  }
1286
1293
  return [...groups.values()].map(({ models, ...group }) => {
1287
- const preference = row.freeModels?.[group.id] ?? {}, reasoning = routeReasoning(models);
1294
+ const preference = row.freeModels?.[group.id] ?? {};
1295
+ const context = resolveContext(models.map((s) => s.model), preference.contextWindow ?? (row.contextMode === "custom" ? row.contextWindow : null), row.contextWindow);
1296
+ const eligible = models.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
1297
+ const eligibleAccountIds = eligible.map((s) => s.accountId);
1298
+ const reasoning = routeReasoning(eligible);
1288
1299
  const selected = preference.reasoningEffort;
1289
1300
  return {
1290
1301
  ...group,
1302
+ ...context,
1303
+ eligibleAccountIds,
1304
+ targets: group.targets.filter((t) => eligibleAccountIds.includes(t.account)),
1305
+ accountNames: group.accountNames.filter((_, i) => eligibleAccountIds.includes(models[i].accountId)),
1306
+ enforceContext: true,
1291
1307
  parentId,
1292
1308
  dynamicFree: true,
1293
1309
  freeOnly: true,
@@ -1300,9 +1316,8 @@ var Manager = class {
1300
1316
  effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
1301
1317
  visible: row.visible !== false && preference.visible !== false,
1302
1318
  selected: preference.visible !== false,
1303
- supportsImages: models.every(({ model }) => model.supportsImages),
1304
- contextWindow: Math.min(row.contextWindow, ...models.map(({ model }) => model.contextWindow)),
1305
- maxTokens: Math.min(row.maxTokens, ...models.map(({ model }) => model.maxTokens), ...models.map(({ model }) => model.contextWindow))
1319
+ supportsImages: eligible.every(({ model }) => model.supportsImages),
1320
+ maxTokens: Math.min(row.maxTokens, context.contextWindow, ...eligible.map(({ model }) => model.maxTokens))
1306
1321
  };
1307
1322
  });
1308
1323
  }
@@ -1317,7 +1332,7 @@ var Manager = class {
1317
1332
  publish() {
1318
1333
  this.telemetry.configure(this.config.usageRetentionDays);
1319
1334
  this.revision = revisionOf(this.raw);
1320
- const routes = { ...this.config.routes };
1335
+ const routes = Object.fromEntries(this.routeModels().map((row) => [row.id, row]));
1321
1336
  for (const model of this.adapterModels().filter((m) => m.dynamicFree)) routes[model.id] = model;
1322
1337
  for (const provider of PROVIDERS) for (const model of this.providerModels(provider)) {
1323
1338
  routes[`${provider}::${model.id}`] = { ...model, targets: [{ provider, model: model.id }] };
@@ -1329,23 +1344,30 @@ var Manager = class {
1329
1344
  const rows = /* @__PURE__ */ new Map();
1330
1345
  for (const account of this.config.accounts.filter((a) => a.provider === provider && a.enabled !== false)) {
1331
1346
  for (const model of this.catalogs.get(account.id)?.models ?? []) {
1332
- const old = rows.get(model.id);
1333
- rows.set(model.id, old ? {
1334
- ...old,
1335
- contextWindow: Math.min(old.contextWindow, model.contextWindow),
1336
- maxTokens: Math.min(old.maxTokens, model.maxTokens),
1337
- supportsImages: old.supportsImages && model.supportsImages,
1338
- reasoning: routeReasoning([{ model: old, provider }, { model, provider }])
1339
- } : {
1340
- ...model,
1341
- ...reasoningModes(model, provider).length ? { reasoning: routeReasoning([{ model, provider }]) } : {}
1342
- });
1347
+ if (!rows.has(model.id)) rows.set(model.id, []);
1348
+ rows.get(model.id).push(model);
1343
1349
  }
1344
1350
  }
1345
- return [...rows.values()].map((model) => ({
1346
- ...model,
1347
- name: this.config.models[provider]?.[model.id]?.name ?? model.name
1348
- }));
1351
+ return [...rows.values()].map((models) => {
1352
+ const model = models[0], preference = this.config.models[provider]?.[model.id] ?? {};
1353
+ const context = resolveContext(models, preference.contextWindow);
1354
+ const eligible = models.filter((m) => contextInfo(m).maxContextWindow >= context.contextWindow);
1355
+ const reasoning = routeReasoning(eligible.map((model2) => ({ model: model2, provider })));
1356
+ const selected = preference.reasoningEffort;
1357
+ return {
1358
+ ...model,
1359
+ ...context,
1360
+ enforceContext: true,
1361
+ name: preference.name ?? model.name,
1362
+ reasoning,
1363
+ supportsImages: preference.supportsImages !== false && eligible.every((m) => m.supportsImages),
1364
+ imageCapable: eligible.every((m) => m.supportsImages),
1365
+ ...selected && reasoning?.supportedEfforts.includes(selected) ? { reasoningEffort: selected } : {},
1366
+ configuredEffort: selected ?? null,
1367
+ effortUnavailable: !!selected && !reasoning?.supportedEfforts.includes(selected),
1368
+ maxTokens: Math.min(context.contextWindow, ...eligible.map((m) => m.maxTokens))
1369
+ };
1370
+ });
1349
1371
  }
1350
1372
  hiddenModels(provider) {
1351
1373
  return this.providerModels(provider).filter((m) => !this.modelVisible(provider, m.id)).map((m) => m.id);
@@ -1359,14 +1381,19 @@ var Manager = class {
1359
1381
  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) => {
1360
1382
  const catalog = this.catalogs.get(a.id)?.models ?? [];
1361
1383
  const models2 = target.models ? target.models.map((model) => catalog.find((m) => m.id === model)).filter(Boolean) : [matchModel(id, target, catalog)].filter(Boolean);
1362
- return models2.map((model) => ({ model, provider: a.provider }));
1384
+ return models2.map((model) => ({ model, provider: a.provider, accountId: a.id }));
1363
1385
  }));
1364
- const models = sources.map((s) => s.model), reasoning = routeReasoning(sources);
1386
+ const context = resolveContext(sources.map((s) => s.model), row.contextMode === "custom" ? row.contextWindow : null, row.contextWindow);
1387
+ const eligible = sources.filter((s) => contextInfo(s.model).maxContextWindow >= context.contextWindow);
1388
+ const models = eligible.map((s) => s.model), reasoning = routeReasoning(eligible);
1365
1389
  return {
1366
1390
  ...row,
1391
+ ...context,
1392
+ enforceContext: true,
1367
1393
  supportsImages: models.length > 0 && models.every((m) => m.supportsImages),
1368
1394
  reasoning,
1369
- ...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)) } : {},
1395
+ eligibleAccountIds: [...new Set(eligible.map((s) => s.accountId))],
1396
+ maxTokens: Math.min(row.maxTokens, context.contextWindow, ...models.map((m) => m.maxTokens)),
1370
1397
  id,
1371
1398
  name: row.name ?? id,
1372
1399
  expiringStatus: { ...this.telemetry.data.expiringRounds[id] },
@@ -1388,7 +1415,7 @@ var Manager = class {
1388
1415
  const client = await this.router.client(account);
1389
1416
  const models = (await client.models(AbortSignal.timeout(6e4), !!force)).map(modelInfo);
1390
1417
  if (this.closed || !this.config.accounts.some((a) => a.id === account.id && a.credentialFile === account.credentialFile)) return;
1391
- this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, models });
1418
+ this.catalogs.set(account.id, { credentialFile: account.credentialFile, fetchedAt: Date.now(), pricingVersion: 1, contextVersion: 1, models });
1392
1419
  this.catalogErrors.delete(account.id);
1393
1420
  await withFileLock3(this.catalogFile, () => writePrivateJson(this.catalogFile, Object.fromEntries(this.catalogs)));
1394
1421
  } catch (error) {
@@ -1400,6 +1427,19 @@ var Manager = class {
1400
1427
  this.refreshes.set(account.id, promise);
1401
1428
  return promise;
1402
1429
  }
1430
+ summaryState() {
1431
+ const today = new Date(this.telemetry.now()).toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" });
1432
+ const daily = this.telemetry.data.days[today] ?? {};
1433
+ return {
1434
+ pluginVersion: version,
1435
+ enabled: this.config.sidebarUsage,
1436
+ today,
1437
+ tokens: daily.tokens ?? 0,
1438
+ requests: daily.requests ?? 0,
1439
+ unmetered: daily.unmetered ?? 0,
1440
+ totalTokens: Object.values(this.telemetry.data.accounts).reduce((sum, row) => sum + (row.tokens ?? 0), 0)
1441
+ };
1442
+ }
1403
1443
  state() {
1404
1444
  const status = this.router.status();
1405
1445
  return {
@@ -1412,6 +1452,7 @@ var Manager = class {
1412
1452
  pluginUpdates: this.plugins.state(),
1413
1453
  checkins: this.checkins.state(),
1414
1454
  telemetry: this.telemetry.state(),
1455
+ apiService: this.api.state(),
1415
1456
  accounts: this.config.accounts.map((a) => ({
1416
1457
  id: a.id,
1417
1458
  provider: a.provider,
@@ -1431,6 +1472,7 @@ var Manager = class {
1431
1472
  presets: Object.entries(ROUTE_PRESETS).map(([id, route]) => ({
1432
1473
  id,
1433
1474
  ...structuredClone(route),
1475
+ providerCount: route.targets.length,
1434
1476
  added: Object.hasOwn(this.config.routes, id),
1435
1477
  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))]
1436
1478
  })),
@@ -1439,12 +1481,79 @@ var Manager = class {
1439
1481
  cooldownMs: this.config.cooldownMs,
1440
1482
  requestTimeoutMs: this.config.requestTimeoutMs,
1441
1483
  retryServerErrors: this.config.retryServerErrors,
1442
- usageRetentionDays: this.config.usageRetentionDays
1484
+ usageRetentionDays: this.config.usageRetentionDays,
1485
+ sidebarUsage: this.config.sidebarUsage
1443
1486
  }
1444
1487
  };
1445
1488
  }
1446
1489
  async action(input) {
1447
1490
  if (!input || typeof input !== "object" || Array.isArray(input)) bad("\u8BF7\u6C42\u683C\u5F0F\u65E0\u6548");
1491
+ if (input.action === "model.test") {
1492
+ const model = this.providerModels(input.provider).find((m) => m.id === input.id);
1493
+ if (!model) bad("\u8BF7\u5148\u5237\u65B0\u5E76\u9009\u62E9\u5DF2\u53D1\u73B0\u7684\u6A21\u578B");
1494
+ 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");
1495
+ this.modelTestFlight = true;
1496
+ this.modelTestAt = Date.now();
1497
+ this.modelTestAbort = new AbortController();
1498
+ const started = performance.now();
1499
+ let modelTest;
1500
+ try {
1501
+ const { response } = await this.router.chat(
1502
+ {
1503
+ model: `${input.provider}::${input.id}`,
1504
+ stream: true,
1505
+ messages: [{ role: "system", content: "Answer briefly." }, { role: "user", content: "Reply with OK." }],
1506
+ max_tokens: 16
1507
+ },
1508
+ AbortSignal.any([this.modelTestAbort.signal, AbortSignal.timeout(45e3)])
1509
+ );
1510
+ const reader = response.body.getReader();
1511
+ let size = 0, text = "";
1512
+ const decoder = new TextDecoder();
1513
+ try {
1514
+ for (; ; ) {
1515
+ const chunk = await reader.read();
1516
+ if (chunk.done) break;
1517
+ size += chunk.value.byteLength;
1518
+ if (size > 1024 * 1024) throw Error("probe_response_too_large");
1519
+ text += decoder.decode(chunk.value, { stream: true });
1520
+ }
1521
+ text += decoder.decode();
1522
+ let answered = false;
1523
+ for (const line of text.split(/\r?\n/).filter((l) => l.startsWith("data:"))) {
1524
+ if (line.slice(5).trim() === "[DONE]") continue;
1525
+ const payload = JSON.parse(line.slice(5));
1526
+ if (payload.error || payload.choices?.some((c) => c.finish_reason === "error")) throw Error("probe_stream_failed");
1527
+ answered ||= payload.choices?.some((c) => c.delta?.content || c.delta?.reasoning_content || c.delta?.tool_calls?.length) ?? false;
1528
+ }
1529
+ if (!answered || !/^data:\s*\[DONE\]\s*$/m.test(text)) throw Error("probe_incomplete");
1530
+ } finally {
1531
+ await reader.cancel().catch(() => {
1532
+ });
1533
+ reader.releaseLock();
1534
+ }
1535
+ modelTest = { ok: true, elapsedMs: Math.round(performance.now() - started) };
1536
+ } catch (error) {
1537
+ modelTest = {
1538
+ ok: false,
1539
+ status: Number(error.status) || 502,
1540
+ hint: error.hint || "\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF0C\u8BF7\u67E5\u770B\u8D26\u53F7\u72B6\u6001\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u3002"
1541
+ };
1542
+ } finally {
1543
+ this.modelTestFlight = false;
1544
+ this.modelTestAbort = void 0;
1545
+ }
1546
+ return { ...this.state(), modelTest };
1547
+ }
1548
+ if (input.action?.startsWith("api.")) {
1549
+ let result;
1550
+ try {
1551
+ result = await this.api.action(input);
1552
+ } catch (e) {
1553
+ bad(e.message);
1554
+ }
1555
+ return { ...this.state(), ...result };
1556
+ }
1448
1557
  if (input.action === "plugin.reload") {
1449
1558
  if (!this.runtime) bad("\u5F53\u524D\u540E\u53F0\u4E0D\u652F\u6301\u91CD\u65B0\u52A0\u8F7D");
1450
1559
  this.runtime.retryAt = 0;
@@ -1739,22 +1848,27 @@ var Manager = class {
1739
1848
  }
1740
1849
  } else if (input.action === "model.update") {
1741
1850
  if (!PROVIDERS.includes(input.provider) || typeof input.id !== "string" || !input.id || input.id.length > 256) bad("\u6A21\u578B\u65E0\u6548");
1851
+ const model = this.providerModels(input.provider).find((m) => m.id === input.id);
1852
+ 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");
1853
+ if (input.supportsImages === true && !model?.imageCapable) bad("\u8BE5\u6A21\u578B\u672A\u58F0\u660E\u56FE\u7247\u8F93\u5165\u80FD\u529B");
1854
+ if (input.reasoningEffort != null && !model?.reasoning?.supportedEfforts.includes(input.reasoningEffort)) bad("\u6B64\u6A21\u578B\u4E0D\u652F\u6301\u6240\u9009\u63A8\u7406\u7B49\u7EA7");
1742
1855
  raw.models = { ...raw.models, [input.provider]: {
1743
1856
  ...raw.models?.[input.provider],
1744
1857
  [input.id]: {
1745
1858
  ...raw.models?.[input.provider]?.[input.id],
1746
- ...input.name !== void 0 ? { name: input.name } : {},
1747
- ...input.visible !== void 0 ? { visible: input.visible } : {}
1859
+ ...Object.fromEntries(["name", "visible", "contextWindow", "supportsImages", "reasoningEffort"].filter((k) => input[k] !== void 0).map((k) => [k, input[k]]))
1748
1860
  }
1749
1861
  } };
1750
1862
  } else if (input.action === "free.model.update") {
1751
1863
  const parent = raw.routes[input.parentId];
1752
1864
  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");
1753
1865
  const model = this.freeRoutes(input.parentId, this.config.routes[input.parentId]).find((m) => m.id === input.id);
1866
+ 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");
1754
1867
  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");
1755
1868
  parent.freeModels = { ...parent.freeModels, [input.id]: {
1756
1869
  ...parent.freeModels?.[input.id],
1757
1870
  ...input.visible !== void 0 ? { visible: input.visible } : {},
1871
+ ...input.contextWindow !== void 0 ? { contextWindow: input.contextWindow } : {},
1758
1872
  ...input.reasoningEffort !== void 0 ? { reasoningEffort: input.reasoningEffort } : {}
1759
1873
  } };
1760
1874
  } else if (input.action === "route.save") {
@@ -1787,7 +1901,7 @@ var Manager = class {
1787
1901
  if (!input.schedule) bad("\u8BF7\u586B\u5199\u7B7E\u5230\u8BA1\u5212");
1788
1902
  raw.checkinSchedule = input.schedule;
1789
1903
  } else if (input.action === "options.save") {
1790
- for (const key of ["cooldownMs", "requestTimeoutMs", "retryServerErrors", "usageRetentionDays"]) if (input[key] !== void 0) raw[key] = input[key];
1904
+ for (const key of ["cooldownMs", "requestTimeoutMs", "retryServerErrors", "usageRetentionDays", "sidebarUsage"]) if (input[key] !== void 0) raw[key] = input[key];
1791
1905
  } else bad("\u672A\u77E5\u64CD\u4F5C");
1792
1906
  let config;
1793
1907
  try {
@@ -1854,7 +1968,7 @@ var Manager = class {
1854
1968
  };
1855
1969
  }
1856
1970
  };
1857
- function managementHandler(manager) {
1971
+ function managementHandler(manager, { summaryOnly = false } = {}) {
1858
1972
  const key = randomBytes3(32).toString("base64url");
1859
1973
  const json = (res, status, value) => {
1860
1974
  res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
@@ -1868,9 +1982,10 @@ function managementHandler(manager) {
1868
1982
  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");
1869
1983
  if (req.headers["sec-fetch-site"] === "cross-site") throw new ManagementError(403, "\u6765\u6E90\u65E0\u6548");
1870
1984
  if (req.method === "GET") {
1871
- json(res, 200, { ...manager.state(), key });
1985
+ json(res, 200, summaryOnly ? manager.summaryState() : { ...manager.state(), key });
1872
1986
  return;
1873
1987
  }
1988
+ if (summaryOnly) throw new ManagementError(405, "\u65B9\u6CD5\u65E0\u6548");
1874
1989
  if (req.method !== "POST") throw new ManagementError(405, "\u65B9\u6CD5\u65E0\u6548");
1875
1990
  const actual = Buffer.from(req.headers["x-multi-router-key"] ?? ""), expected = Buffer.from(key);
1876
1991
  if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new ManagementError(401, "\u8BF7\u5237\u65B0\u914D\u7F6E\u9875\u9762");
@@ -1904,9 +2019,10 @@ export {
1904
2019
  version,
1905
2020
  PluginReload,
1906
2021
  MANAGEMENT_PATH,
2022
+ SUMMARY_PATH,
1907
2023
  ManagementError,
1908
2024
  modelInfo,
1909
2025
  Manager,
1910
2026
  managementHandler
1911
2027
  };
1912
- //# sourceMappingURL=chunk-I4EW5TI7.js.map
2028
+ //# sourceMappingURL=chunk-ZQZSXCVR.js.map