dsh-multi-router 0.6.12 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +2 -430
  2. package/package.json +5 -81
  3. package/CHANGELOG.md +0 -243
  4. package/LICENSE +0 -21
  5. package/THIRD_PARTY_NOTICES.md +0 -171
  6. package/cordis.patch.yml +0 -3
  7. package/lib/accounts.js +0 -19
  8. package/lib/accounts.js.map +0 -7
  9. package/lib/api-service.js +0 -11
  10. package/lib/api-service.js.map +0 -7
  11. package/lib/chunk-2UM45OXY.js +0 -181
  12. package/lib/chunk-2UM45OXY.js.map +0 -7
  13. package/lib/chunk-4L2CBGXN.js +0 -674
  14. package/lib/chunk-4L2CBGXN.js.map +0 -7
  15. package/lib/chunk-4V52V5GO.js +0 -105
  16. package/lib/chunk-4V52V5GO.js.map +0 -7
  17. package/lib/chunk-DNQPZMPF.js +0 -33
  18. package/lib/chunk-DNQPZMPF.js.map +0 -7
  19. package/lib/chunk-FLL3DUVP.js +0 -2017
  20. package/lib/chunk-FLL3DUVP.js.map +0 -7
  21. package/lib/chunk-GEBZMIMA.js +0 -287
  22. package/lib/chunk-GEBZMIMA.js.map +0 -7
  23. package/lib/chunk-KWXLQJS5.js +0 -1102
  24. package/lib/chunk-KWXLQJS5.js.map +0 -7
  25. package/lib/chunk-NZF36O5I.js +0 -5421
  26. package/lib/chunk-NZF36O5I.js.map +0 -7
  27. package/lib/chunk-S5KMFAQM.js +0 -50
  28. package/lib/chunk-S5KMFAQM.js.map +0 -7
  29. package/lib/chunk-TM5XDMGY.js +0 -332
  30. package/lib/chunk-TM5XDMGY.js.map +0 -7
  31. package/lib/chunk-V37VG7MN.js +0 -2101
  32. package/lib/chunk-V37VG7MN.js.map +0 -7
  33. package/lib/chunk-WDIT4E2W.js +0 -120
  34. package/lib/chunk-WDIT4E2W.js.map +0 -7
  35. package/lib/cli.js +0 -103
  36. package/lib/cli.js.map +0 -7
  37. package/lib/client.js +0 -2805
  38. package/lib/client.js.map +0 -7
  39. package/lib/clients.js +0 -30
  40. package/lib/clients.js.map +0 -7
  41. package/lib/host-v0.6.12.js +0 -414
  42. package/lib/host.js +0 -414
  43. package/lib/index.js +0 -414
  44. package/lib/index.js.map +0 -7
  45. package/lib/manager.js +0 -27
  46. package/lib/manager.js.map +0 -7
  47. package/lib/region-DO3KAV7V.js +0 -15
  48. package/lib/region-DO3KAV7V.js.map +0 -7
  49. package/lib/router.js +0 -14
  50. package/lib/router.js.map +0 -7
  51. package/lib/shim.js +0 -10
  52. package/lib/shim.js.map +0 -7
  53. package/lib/upstream-VP6J7LU2.js +0 -25
  54. package/lib/upstream-VP6J7LU2.js.map +0 -7
  55. package/router.example.json +0 -289
  56. package/upstream-lock.json +0 -17
@@ -1,674 +0,0 @@
1
- import {
2
- isFreeModel,
3
- reasoningModes,
4
- writePrivateJson
5
- } from "./chunk-GEBZMIMA.js";
6
-
7
- // src/telemetry.mjs
8
- import { createHash, randomUUID } from "node:crypto";
9
- import { assistantStreamFirstTokenTime } from "@deepseek-ai/dsh-llm";
10
- import { readFile } from "node:fs/promises";
11
- var empty = () => ({ requests: 0, failures: 0, input: 0, output: 0, tokens: 0, unmetered: 0 });
12
- var day = (at) => new Date(at).toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" });
13
- var safe = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
14
- var measured = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : null;
15
- var Telemetry = class {
16
- constructor(file, now = Date.now) {
17
- this.file = file;
18
- this.now = now;
19
- this.retentionDays = 30;
20
- this.data = { accounts: {}, routes: {}, days: {}, rounds: {}, expiringRounds: {}, events: [] };
21
- this.queue = Promise.resolve();
22
- }
23
- async load() {
24
- try {
25
- const saved = JSON.parse(await readFile(this.file, "utf8"));
26
- if (saved.version !== 1) throw Error("Unsupported telemetry");
27
- this.data = { ...saved, expiringRounds: saved.expiringRounds ?? {} };
28
- } catch (e) {
29
- if (e.code !== "ENOENT") throw e;
30
- }
31
- if (this.prune()) await this.persist();
32
- }
33
- configure(retentionDays) {
34
- this.retentionDays = retentionDays;
35
- }
36
- prune() {
37
- const cutoff = this.now() - this.retentionDays * 864e5, before = this.data.events.length;
38
- this.data.events = this.data.events.filter((e) => Number.isFinite(e.at) && e.at >= cutoff);
39
- return before !== this.data.events.length;
40
- }
41
- row(group, key) {
42
- if (!Object.hasOwn(this.data[group], key)) Object.defineProperty(this.data[group], key, { value: empty(), enumerable: true, writable: true });
43
- return this.data[group][key];
44
- }
45
- count(alias, account) {
46
- return this.row("routes", JSON.stringify([alias, account]));
47
- }
48
- expiring(alias) {
49
- if (!Object.hasOwn(this.data.expiringRounds, alias)) Object.defineProperty(this.data.expiringRounds, alias, { value: { accounts: [], completed: [], cooldownUntil: 0, number: 1 }, enumerable: true, writable: true });
50
- return this.data.expiringRounds[alias];
51
- }
52
- restartExpiring(alias) {
53
- const row = this.expiring(alias);
54
- row.accounts = [];
55
- row.completed = [];
56
- row.cooldownUntil = 0;
57
- row.number++;
58
- return this.persist();
59
- }
60
- round(alias, signature) {
61
- if (!Object.hasOwn(this.data.rounds, alias) || this.data.rounds[alias].signature !== signature) Object.defineProperty(this.data.rounds, alias, { value: { signature, number: 1, bases: {} }, enumerable: true, writable: true });
62
- return this.data.rounds[alias];
63
- }
64
- advance(alias, candidates, signature) {
65
- const round = this.round(alias, signature);
66
- round.number++;
67
- for (const c of candidates) Object.defineProperty(round.bases, c.account.id, { value: { tokens: this.count(alias, c.account.id).tokens, credits: this.row("accounts", c.account.id).creditsObserved ?? 0 }, enumerable: true, writable: true, configurable: true });
68
- return this.persist();
69
- }
70
- observe(account, snapshot) {
71
- const row = this.row("accounts", account);
72
- if (Number.isFinite(snapshot.remaining) && snapshot.unit === "credits") {
73
- const sameCycle = row.balance?.cycle === snapshot.cycle;
74
- const change = sameCycle && Number.isFinite(row.balance?.remaining) ? Math.max(0, row.balance.remaining - snapshot.remaining) : 0;
75
- row.creditsObserved = (row.creditsObserved ?? 0) + change;
76
- }
77
- row.balance = snapshot;
78
- return this.persist();
79
- }
80
- record(event) {
81
- const at = this.now();
82
- const rows = [this.row("accounts", event.account), this.count(event.alias, event.account), this.row("days", day(at))];
83
- for (const row of rows) {
84
- row.requests++;
85
- if (!event.ok) row.failures++;
86
- if (event.usage) {
87
- const input2 = safe(event.usage.prompt_tokens), output2 = safe(event.usage.completion_tokens);
88
- row.input += input2;
89
- row.output += output2;
90
- row.tokens += safe(event.usage.total_tokens) || input2 + output2;
91
- } else if (event.status === 200 || event.accepted) row.unmetered++;
92
- }
93
- const input = measured(event.usage?.prompt_tokens), output = measured(event.usage?.completion_tokens);
94
- this.data.events.unshift({
95
- at,
96
- account: event.account,
97
- provider: event.provider,
98
- alias: event.alias,
99
- model: event.model ?? null,
100
- status: event.status,
101
- outcome: event.ok ? "completed" : event.cancelled ? "cancelled" : "failed",
102
- input,
103
- output,
104
- tokens: measured(event.usage?.total_tokens) ?? (input !== null && output !== null ? input + output : null),
105
- cachedInput: measured(event.usage?.prompt_tokens_details?.cached_tokens),
106
- reasoningOutput: measured(event.usage?.completion_tokens_details?.reasoning_tokens),
107
- callId: event.callId ?? null,
108
- session: event.session ?? null,
109
- source: event.source === "api" ? "api" : "dsh",
110
- timing: event.timing ? Object.fromEntries(["modelMs", "ttftMs", "decodeMs", "decodeTokens"].map((k) => [k, measured(event.timing[k])])) : null,
111
- timingSource: event.timing ? "upstream" : null
112
- });
113
- const dates = Object.keys(this.data.days).sort();
114
- for (const key of dates.slice(0, -31)) delete this.data.days[key];
115
- return this.persist();
116
- }
117
- persist() {
118
- this.prune();
119
- const saved = structuredClone({ ...this.data, version: 1 });
120
- this.queue = this.queue.catch(() => {
121
- }).then(() => writePrivateJson(this.file, saved));
122
- return this.queue;
123
- }
124
- state() {
125
- this.prune();
126
- return { ...structuredClone(this.data), retentionDays: this.retentionDays, today: day(this.now()), updatedAt: this.now() };
127
- }
128
- };
129
- function meterResponse(response, complete, signal, { now = () => performance.now(), startedAt = now() } = {}) {
130
- if (!response.body) return response;
131
- const reader = response.body.getReader(), decoder = new TextDecoder();
132
- let pending = "", usage, ended = false, failed = false, finished = false, firstToken = null, terminalAt = null;
133
- async function finish(cancelled = false) {
134
- if (finished) return;
135
- finished = true;
136
- const end = terminalAt ?? now();
137
- await complete({ usage, ok: ended && !failed && !cancelled, cancelled, timing: { modelMs: Math.max(0, end - startedAt), ttftMs: firstToken === null ? null : Math.max(0, firstToken - startedAt), decodeMs: firstToken === null ? null : Math.max(0, end - firstToken), decodeTokens: firstToken === null ? null : measured(usage?.completion_tokens) } });
138
- }
139
- function inspect(bytes) {
140
- pending += decoder.decode(bytes, { stream: true });
141
- let split;
142
- while (split = /\r?\n\r?\n/.exec(pending)) {
143
- const frame = pending.slice(0, split.index);
144
- pending = pending.slice(split.index + split[0].length);
145
- const data = frame.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart()).join("\n");
146
- if (data === "[DONE]") {
147
- ended = true;
148
- terminalAt ??= now();
149
- continue;
150
- }
151
- try {
152
- const value = JSON.parse(data);
153
- if (value.usage && ["prompt_tokens", "completion_tokens", "total_tokens"].some((k) => Number.isFinite(value.usage[k]) && value.usage[k] >= 0)) usage = value.usage;
154
- if (value.error) failed = true;
155
- if (firstToken === null && value.choices?.some((c) => {
156
- const d = c.delta;
157
- return [d?.content, d?.reasoning_content, d?.reasoning].some((v) => typeof v === "string" && v.trim()) || d?.tool_calls?.some((t) => t.function?.name || t.function?.arguments);
158
- })) firstToken = now();
159
- if (value.choices?.some((c) => c.finish_reason)) {
160
- ended = true;
161
- terminalAt ??= now();
162
- }
163
- } catch {
164
- }
165
- }
166
- if (pending.length > 1024 * 1024) {
167
- pending = "";
168
- failed = true;
169
- }
170
- }
171
- const body = new ReadableStream({
172
- async pull(controller) {
173
- try {
174
- signal?.throwIfAborted();
175
- const chunk = await reader.read();
176
- if (chunk.done) {
177
- await finish();
178
- controller.close();
179
- } else {
180
- inspect(chunk.value);
181
- controller.enqueue(chunk.value);
182
- }
183
- } catch (e) {
184
- await finish(signal?.aborted);
185
- controller.error(e);
186
- await reader.cancel().catch(() => {
187
- });
188
- }
189
- },
190
- async cancel(reason) {
191
- await reader.cancel(reason).catch(() => {
192
- });
193
- await finish(!ended);
194
- }
195
- });
196
- return new Response(body, { status: response.status, headers: response.headers });
197
- }
198
- var SessionMetrics = class {
199
- constructor(telemetry) {
200
- this.telemetry = telemetry;
201
- this.steps = /* @__PURE__ */ new Map();
202
- }
203
- key(id) {
204
- return createHash("sha256").update(String(id)).digest("hex").slice(0, 24);
205
- }
206
- beginCall(sessionId, purpose) {
207
- const callId = randomUUID(), session = sessionId == null ? null : this.key(sessionId);
208
- const step = session && this.steps.get(session);
209
- if (step && !purpose) step.callId = callId;
210
- return { callId, session };
211
- }
212
- event(session, event) {
213
- const key = this.key(session.id), d = event.data;
214
- if (event.type === "step/start") {
215
- this.steps.set(key, { turn: d.turn, step: d.step, start: event.time, first: null, calls: /* @__PURE__ */ new Map(), toolMs: 0, toolCount: 0, toolsCompleted: 0 });
216
- return;
217
- }
218
- const step = this.steps.get(key);
219
- if (!step) return;
220
- if (d.turn !== void 0 && (d.turn !== step.turn || d.step !== void 0 && d.step !== step.step)) return;
221
- if (event.type === "assistant/attempt") {
222
- step.first ??= assistantStreamFirstTokenTime(d.stream) ?? null;
223
- return;
224
- }
225
- const row = this.telemetry.data.events.find((e) => e.callId === step.callId && e.session === key);
226
- if (event.type === "assistant/message") {
227
- if (!row || d.interrupted) return;
228
- const first = step.first ?? assistantStreamFirstTokenTime(d.stream) ?? null;
229
- const output = measured(d.usage?.outputTokens);
230
- row.timing = {
231
- modelMs: Math.max(0, event.time - step.start),
232
- ttftMs: first === null ? null : Math.max(0, first - step.start),
233
- decodeMs: first === null || output === null ? null : Math.max(0, event.time - first),
234
- decodeTokens: first === null ? null : output
235
- };
236
- row.timingSource = "dsh-step";
237
- } else if (event.type === "tool/call" && row?.timingSource === "dsh-step") {
238
- if (!step.calls.has(d.callId)) {
239
- step.calls.set(d.callId, event.time);
240
- step.toolCount++;
241
- }
242
- } else if (event.type === "tool/result" && step.calls.has(d.message?.source?.callId)) {
243
- const id = d.message.source.callId;
244
- step.toolMs += Math.max(0, event.time - step.calls.get(id));
245
- step.toolsCompleted++;
246
- step.calls.delete(id);
247
- } else if (event.type === "step/end" || event.type === "turn/end") {
248
- this.steps.delete(key);
249
- } else return;
250
- if (row?.timingSource !== "dsh-step") return;
251
- Object.assign(row.timing, { toolMs: step.toolMs, toolCount: step.toolCount, toolsCompleted: step.toolsCompleted });
252
- return this.telemetry.persist();
253
- }
254
- dispose(session) {
255
- this.steps.delete(this.key(session.id));
256
- }
257
- };
258
-
259
- // src/model-context.mjs
260
- var positive = (n) => Number.isSafeInteger(n) && n > 0;
261
- function contextInfo(model) {
262
- const defaultContextWindow = positive(model.defaultContextWindow) ? model.defaultContextWindow : positive(model.contextWindow) ? model.contextWindow : 2e5;
263
- const supportedContextWindows = [...new Set([
264
- defaultContextWindow,
265
- model.contextWindow,
266
- model.maxContextWindow,
267
- ...Array.isArray(model.supportedContextWindows) ? model.supportedContextWindows : []
268
- ].filter(positive))].sort((a, b) => a - b);
269
- const maxContextWindow = supportedContextWindows.at(-1);
270
- return { defaultContextWindow, supportedContextWindows, maxContextWindow, contextWindow: maxContextWindow };
271
- }
272
- function resolveContext(models, selected, fallback = 2e5) {
273
- const infos = models.map(contextInfo);
274
- const maxContextWindow = infos.length ? Math.max(...infos.map((m) => m.maxContextWindow)) : fallback;
275
- const contextWindow = positive(selected) ? Math.min(selected, maxContextWindow) : maxContextWindow;
276
- return {
277
- maxContextWindow,
278
- contextWindow,
279
- contextKnown: infos.length > 0,
280
- configuredContextWindow: positive(selected) ? selected : null,
281
- supportedContextWindows: [.../* @__PURE__ */ new Set([contextWindow, ...infos.flatMap((m) => m.supportedContextWindows)])].sort((a, b) => a - b)
282
- };
283
- }
284
- function qoderContextModel(model, budget) {
285
- if (!model?.contextOptions) return model;
286
- const entries = Object.entries(model.contextOptions).filter(([, v]) => positive(v.tokenCount)).sort((a, b) => a[1].tokenCount - b[1].tokenCount);
287
- if (!entries.length) return model;
288
- const selected = budget == null ? entries.at(-1) : entries.find(([, v]) => v.tokenCount >= budget);
289
- if (!selected) throw Object.assign(Error("context_window_unavailable"), { status: 400, code: "context_window_unavailable" });
290
- return {
291
- ...model,
292
- contextWindow: selected[1].tokenCount,
293
- contextOptions: Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, { ...value, isDefault: key === selected[0] }]))
294
- };
295
- }
296
-
297
- // src/router.mjs
298
- var RouteError = class extends Error {
299
- constructor(status, code, attempts = [], hint) {
300
- super(`${code}${attempts.length ? ": " + attempts.map((a) => `${a.account} HTTP ${a.status}`).join(", ") : ""}${hint ? `\uFF08${hint}\uFF09` : ""}`);
301
- this.status = status;
302
- this.code = code;
303
- this.attempts = attempts;
304
- this.hint = hint;
305
- }
306
- };
307
- var normalize = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "").replace(/official$/, "");
308
- function abortable(promise, signal) {
309
- if (!signal) return promise;
310
- return new Promise((resolve, reject) => {
311
- const abort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
312
- signal.addEventListener("abort", abort, { once: true });
313
- if (signal.aborted) abort();
314
- Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
315
- });
316
- }
317
- function matchModel(alias, target, catalog, effort, provider = target.provider) {
318
- const allowed = (m) => m && (effort === void 0 || reasoningModes(m, provider).includes(effort));
319
- if (target.model) return catalog.find((m) => m.id === target.model && allowed(m));
320
- if (target.models) return target.models.map((id) => catalog.find((m) => m.id === id)).find(allowed);
321
- if (alias === "ds4.1f") return catalog.find((m) => allowed(m) && [m.id, m.name ?? ""].some((x) => normalize(x) === "deepseekv41flash"));
322
- }
323
- var Router = class {
324
- constructor(config, createClient, onAttempt = () => {
325
- }, now = Date.now) {
326
- this.config = config;
327
- this.createClient = createClient;
328
- this.onAttempt = onAttempt;
329
- this.now = now;
330
- this.telemetry = null;
331
- this.scores = /* @__PURE__ */ new Map();
332
- this.monitor = null;
333
- this.clients = /* @__PURE__ */ new Map();
334
- this.cooldowns = /* @__PURE__ */ new Map();
335
- this.active = /* @__PURE__ */ new Map();
336
- this.budgets = /* @__PURE__ */ new Map();
337
- }
338
- /**
339
- * Sort candidates inside each expiring-credit group, never across the boundary.
340
- *
341
- * `group` classifies a candidate — expiring versus not — and only the order inside a
342
- * group changes. That keeps every account holding expiring credits ahead of the ones
343
- * that do not, whatever their price, while the accounts within the expiring group are
344
- * still ordered among themselves. `key` returning undefined means "no opinion" and
345
- * sorts last inside its own group rather than counting as the cheapest.
346
- */
347
- sortWithinGroups(candidates, group, key) {
348
- const groups = /* @__PURE__ */ new Map();
349
- for (const candidate of candidates) {
350
- const name = group(candidate);
351
- if (!groups.has(name)) groups.set(name, []);
352
- groups.get(name).push(candidate);
353
- }
354
- const result = [];
355
- for (const members of groups.values()) {
356
- const decorated = members.map((candidate, index) => ({ candidate, index, key: key(candidate) }));
357
- decorated.sort((a, b) => {
358
- if (a.key === b.key) return a.index - b.index;
359
- if (a.key === void 0) return 1;
360
- if (b.key === void 0) return -1;
361
- return a.key - b.key;
362
- });
363
- result.push(...decorated.map((entry) => entry.candidate));
364
- }
365
- return result;
366
- }
367
- /**
368
- * Move accounts whose observed credit spend exceeded this route's threshold behind
369
- * the ones that still have headroom, inside each expiring-credit group.
370
- *
371
- * This is a preference, not a filter: the caller keeps every candidate, so a route
372
- * whose accounts are all over budget still serves requests instead of failing. The
373
- * baseline is per route and per account, so switching accounts resets a newcomer's
374
- * counter to zero while the previous account stays demoted for the rest of the round.
375
- *
376
- * `group` splits candidates into expiring and non-expiring. Scoping the demotion to
377
- * that split keeps an account holding expiring credits ahead of one that does not,
378
- * while the expiring accounts still take turns among themselves — grouping by the exact
379
- * expiry date instead would leave each of them alone in its own group, and the allowance
380
- * would never move a request off an account that still has credits left.
381
- *
382
- * Once every candidate in a group has spent its allowance the round is over and those
383
- * baselines restart from the current total. Without that reset the counters only ever
384
- * grow, every account stays demoted forever and the ordering stops changing after one pass.
385
- *
386
- * Spend is inferred from platform balance drops, so one request may overshoot and an
387
- * account without a usable balance reading is never demoted on a guess.
388
- */
389
- demoteOverBudget(alias, route, candidates, group = () => false) {
390
- if (!candidates.length) return candidates;
391
- const threshold = route.creditBalanceThreshold ?? 1e3;
392
- const bases = this.budgets.get(alias) ?? /* @__PURE__ */ new Map();
393
- this.budgets.set(alias, bases);
394
- const live = new Set(candidates.map((c) => c.account.id));
395
- for (const id of bases.keys()) if (!live.has(id)) bases.delete(id);
396
- const groups = /* @__PURE__ */ new Map();
397
- for (const candidate of candidates) {
398
- const name = group(candidate);
399
- if (!groups.has(name)) groups.set(name, []);
400
- groups.get(name).push(candidate);
401
- }
402
- const result = [];
403
- for (const group2 of groups.values()) {
404
- const fresh = [], spent = [];
405
- for (const candidate of group2) {
406
- const id = candidate.account.id, row = this.telemetry.row("accounts", id);
407
- const observed = row.creditsObserved ?? 0;
408
- if (!bases.has(id)) bases.set(id, observed);
409
- const used = observed - bases.get(id);
410
- if (!Number.isFinite(used) || used < 0) {
411
- bases.set(id, observed);
412
- fresh.push(candidate);
413
- continue;
414
- }
415
- (used >= threshold ? spent : fresh).push(candidate);
416
- }
417
- if (!spent.length) {
418
- result.push(...fresh);
419
- continue;
420
- }
421
- if (spent.length === group2.length) {
422
- for (const candidate of group2) bases.set(candidate.account.id, this.telemetry.row("accounts", candidate.account.id).creditsObserved ?? 0);
423
- result.push(...group2);
424
- continue;
425
- }
426
- result.push(...fresh, ...spent);
427
- }
428
- return result;
429
- }
430
- updateConfig(config) {
431
- for (const old of this.config.accounts) {
432
- const next = config.accounts.find((a) => a.id === old.id);
433
- if (!next || next.provider !== old.provider || next.credentialFile !== old.credentialFile || next.identityFile !== old.identityFile) this.clients.delete(old.id);
434
- }
435
- this.config = config;
436
- for (const alias of this.active.keys()) if (!Object.hasOwn(config.routes, alias)) this.active.delete(alias);
437
- for (const alias of this.budgets.keys()) if (!Object.hasOwn(config.routes, alias)) this.budgets.delete(alias);
438
- }
439
- async client(account) {
440
- if (!this.clients.has(account.id)) this.clients.set(account.id, Promise.resolve().then(() => this.createClient(account)));
441
- try {
442
- return await this.clients.get(account.id);
443
- } catch (error) {
444
- this.clients.delete(account.id);
445
- throw error;
446
- }
447
- }
448
- status() {
449
- return this.config.accounts.map((a) => ({
450
- id: a.id,
451
- provider: a.provider,
452
- enabled: a.enabled !== false,
453
- cooldowns: [...this.cooldowns].filter(([key, time]) => key.startsWith(`${a.id}:`) && time > this.now()).map(([key, time]) => ({ route: key.slice(a.id.length + 1), remainingMs: time - this.now() })),
454
- activeFor: [...this.active].filter(([, key]) => key.split(":")[0] === a.id).map(([alias]) => alias)
455
- }));
456
- }
457
- candidates(alias) {
458
- const route = this.config.routes[alias];
459
- if (!Object.hasOwn(this.config.routes, alias)) throw new RouteError(404, "unknown_model", [], "\u63D2\u4EF6\u8DEF\u7531\u8868\u4E2D\u6CA1\u6709\u8BE5\u8C03\u7528\u540D\u79F0\uFF0C\u8BF7\u5728\u914D\u7F6E\u9875\u65B0\u5EFA\u6216\u6062\u590D\u5BF9\u5E94\u8DEF\u7531");
460
- const candidates = route.targets.flatMap((target, index) => this.config.accounts.filter((a) => a.enabled !== false && a.provider === target.provider && (!target.account || a.id === target.account)).flatMap((account) => route.freeOnly ? (this.freeModels?.(account) ?? []).filter((m) => (!target.model || m.id === target.model) && (!target.models || target.models.includes(m.id))).map((model) => ({ account, target: { ...target, model: model.id, models: void 0 }, key: `${account.id}:${index}:${model.id}` })) : [{ account, target, key: `${account.id}:${index}` }]));
461
- const active = candidates.findIndex((c) => c.key === this.active.get(alias));
462
- return active < 0 ? candidates : [...candidates.slice(active), ...candidates.slice(0, active)];
463
- }
464
- expiringAt(account, route) {
465
- const balance = this.telemetry?.row("accounts", account.id).balance;
466
- if (!balance || balance.error || balance.unit !== "credits" || this.now() - balance.fetchedAt > 12e4) return Infinity;
467
- const horizon = this.now() + (route.expiringWithinDays ?? 7) * 864e5;
468
- return Math.min(Infinity, ...(balance.packages ?? []).filter((p) => p.remaining > 0 && !p.unlimited && p.expiresAt > this.now() && p.expiresAt <= horizon).map((p) => p.expiresAt));
469
- }
470
- async syncExpiring(alias, route, candidates, below = () => true) {
471
- if (!route.preferExpiringCredits || !this.telemetry) return false;
472
- const round = this.telemetry.expiring(alias);
473
- if (round.cooldownUntil > this.now()) return false;
474
- const before = JSON.stringify(round);
475
- if (round.cooldownUntil) {
476
- round.accounts = [];
477
- round.completed = [];
478
- round.cooldownUntil = 0;
479
- round.number++;
480
- }
481
- for (const id of round.accounts) {
482
- const matches = candidates.filter((c) => c.account.id === id), balance = this.telemetry.row("accounts", id).balance;
483
- const fresh = balance && !balance.error && balance.unit === "credits" && this.now() - balance.fetchedAt <= 12e4;
484
- if (!matches.length || matches.every((c) => !below(c)) || fresh && !Number.isFinite(this.expiringAt({ id }, route))) {
485
- if (!round.completed.includes(id)) round.completed.push(id);
486
- }
487
- }
488
- for (const c of candidates) if (below(c) && Number.isFinite(this.expiringAt(c.account, route)) && !round.accounts.includes(c.account.id)) round.accounts.push(c.account.id);
489
- if (round.accounts.length && round.accounts.every((id) => round.completed.includes(id))) round.cooldownUntil = this.now() + 3 * 864e5;
490
- if (JSON.stringify(round) !== before) await this.telemetry.persist();
491
- return round.cooldownUntil <= this.now();
492
- }
493
- remainingCooldown(candidate, alias, route) {
494
- const id = candidate.account.id;
495
- const keys = [`${id}:*`, `${id}:${alias}`];
496
- if (route.freeOnly) keys.push(`${id}:${alias}:${candidate.target.model}`);
497
- return Math.max(0, ...keys.map((key) => (this.cooldowns.get(key) ?? 0) - this.now()));
498
- }
499
- exhaustHint({ alias, route, strategy, candidates, expiryCandidates, attempts }) {
500
- const kinds = new Set(attempts.map((a) => a.kind));
501
- const modelOnly = attempts.length > 0 && [...kinds].every((k) => ["model_unavailable", "reasoning_effort_unavailable", "model_no_longer_free", "context_window_unavailable"].includes(k));
502
- const cooling = candidates.map((c) => this.remainingCooldown(c, alias, route)).filter((ms) => ms > 0);
503
- const soonest = cooling.length ? Math.min(...cooling) : void 0;
504
- const countdown = soonest === void 0 ? void 0 : `\u7EA6 ${Math.max(1, Math.ceil(soonest / 1e3))} \u79D2\u540E\u53EF\u91CD\u8BD5`;
505
- if (!expiryCandidates.length) return route.freeOnly ? "\u8BE5\u8DEF\u7531\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u514D\u8D39\u6A21\u578B\uFF1A\u53EF\u80FD\u5DF2\u6062\u590D\u6536\u8D39\u3001\u9650\u514D\u5230\u671F\u6216\u76EE\u5F55\u83B7\u53D6\u5931\u8D25" : "\u8BE5\u8DEF\u7531\u6CA1\u6709\u53EF\u7528\u7684\u8D26\u53F7\u76EE\u6807\uFF1A\u8D26\u53F7\u53EF\u80FD\u5DF2\u5168\u90E8\u7981\u7528\uFF0C\u6216\u4E0E\u76EE\u6807\u6E20\u9053\u4E0D\u5339\u914D";
506
- if (kinds.has("context_window_unavailable") && modelOnly) return "\u5019\u9009\u6E20\u9053\u4E0D\u652F\u6301\u5F53\u524D\u4E0A\u4E0B\u6587\u9884\u7B97\uFF0C\u8BF7\u5237\u65B0\u76EE\u5F55\u6216\u5728\u6A21\u578B\uFF0F\u8DEF\u7531\u8BBE\u7F6E\u4E2D\u8C03\u4F4E\u9884\u7B97\uFF1B\u4E0D\u4F1A\u622A\u65AD\u4F1A\u8BDD\u6216\u964D\u7EA7\u5230\u5C0F\u5BB9\u91CF\u6E20\u9053";
507
- if (modelOnly) return `\u5DF2\u5C1D\u8BD5\u7684\u8D26\u53F7\u76EE\u5F55\u4E2D\u90FD\u6CA1\u6709\u8BE5\u6A21\u578B\u3001\u6240\u9009\u63A8\u7406\u6863\u4F4D\u6216\u514D\u8D39\u8D44\u683C\u4E0D\u6EE1\u8DB3${countdown ? `\uFF1B\u5176\u4F59\u8D26\u53F7\u51B7\u5374\u4E2D\uFF0C${countdown}` : ""}`;
508
- if (soonest !== void 0 && cooling.length >= candidates.length) {
509
- return attempts.length ? `\u6240\u6709\u5019\u9009\u8D26\u53F7\u672C\u6B21\u8C03\u7528\u5747\u5931\u8D25\u5E76\u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C${countdown}` : `\u5168\u90E8\u8D26\u53F7\u90FD\u5728\u5931\u8D25\u51B7\u5374\u4E2D\uFF0C${countdown}`;
510
- }
511
- if (soonest !== void 0) return `\u90E8\u5206\u8D26\u53F7\u4ECD\u5728\u5931\u8D25\u51B7\u5374\u4E2D\uFF0C${countdown}`;
512
- if (!attempts.length && ["tokens", "credits"].includes(strategy)) return `\u6240\u6709\u8D26\u53F7\u7684\u672C\u8F6E${strategy === "tokens" ? " Token" : "\u79EF\u5206"}\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u6216\u56E0\u7F3A\u5C11\u7528\u91CF\u8BA1\u91CF\u800C\u6682\u505C\u53C2\u4E0E\u8F6E\u6362`;
513
- return void 0;
514
- }
515
- async chat(input, signal, metadata = {}) {
516
- const alias = input.model;
517
- if (!Array.isArray(input.messages) || !input.messages.length || input.stream !== true) throw new RouteError(400, "messages_and_stream_true_required");
518
- const initialRoute = this.config.routes[alias];
519
- if (initialRoute?.freeOnly) {
520
- await abortable(this.prepareFree?.(), signal);
521
- if (!this.config.routes[alias]) throw new RouteError(503, "free_model_pool_unavailable", [], "\u672C\u8DEF\u7531\u4ECA\u5929\u6CA1\u6709\u53EF\u7528\u7684\u514D\u8D39\u6A21\u578B");
522
- }
523
- const effort = Object.hasOwn(input, "reasoning_effort") ? input.reasoning_effort : this.config.routes[alias]?.reasoningEffort;
524
- input = { ...input };
525
- delete input.reasoning_effort;
526
- if (effort !== void 0 && effort !== null) input.reasoning_effort = effort;
527
- let candidates = this.candidates(alias);
528
- const expiryCandidates = [...candidates];
529
- const route = this.config.routes[alias], strategy = route.strategy ?? "failover";
530
- const available = (c) => (this.cooldowns.get(`${c.account.id}:*`) ?? 0) <= this.now() && (this.cooldowns.get(`${c.account.id}:${alias}`) ?? 0) <= this.now() && (!route.freeOnly || (this.cooldowns.get(`${c.account.id}:${alias}:${c.target.model}`) ?? 0) <= this.now());
531
- if (route.preferExpiringCredits && this.monitor) {
532
- await Promise.all([...new Map(candidates.filter(available).map((c) => [c.account.id, c.account])).values()].map((a) => {
533
- const snapshot = this.telemetry?.row("accounts", a.id).balance;
534
- return !snapshot || snapshot.error || this.now() - snapshot.fetchedAt > 6e4 ? this.monitor(a) : void 0;
535
- }));
536
- signal?.throwIfAborted();
537
- }
538
- if (strategy === "credits") {
539
- await Promise.all([...new Map(candidates.filter(available).map((c) => [c.account.id, c.account])).values()].map((a) => this.monitor?.(a)));
540
- signal?.throwIfAborted();
541
- candidates = candidates.filter((c) => {
542
- const balance = this.telemetry?.row("accounts", c.account.id).balance;
543
- return balance?.unit === "credits" && Number.isFinite(balance.remaining) && this.now() - balance.fetchedAt < 12e4 && !balance.error;
544
- });
545
- if (!candidates.length) throw new RouteError(503, "credit_usage_unavailable", [], "\u65E0\u6CD5\u83B7\u53D6\u4EFB\u4F55\u5019\u9009\u8D26\u53F7\u7684\u79EF\u5206\u4F59\u989D\u6570\u636E");
546
- }
547
- const signature = JSON.stringify({ strategy, targets: route.targets });
548
- let below = () => true, preferActive;
549
- if (this.telemetry && ["tokens", "credits"].includes(strategy)) {
550
- const round = this.telemetry.round(alias, signature);
551
- const counters = (c) => ({ tokens: this.telemetry.count(alias, c.account.id).tokens, credits: this.telemetry.row("accounts", c.account.id).creditsObserved ?? 0 });
552
- for (const c of candidates) if (!Object.hasOwn(round.bases, c.account.id)) Object.defineProperty(round.bases, c.account.id, { value: counters(c), enumerable: true, writable: true, configurable: true });
553
- below = (c) => !round.bases[c.account.id] || counters(c)[strategy] - round.bases[c.account.id][strategy] < (strategy === "tokens" ? c.target.tokenBudget : c.target.creditBudget);
554
- preferActive = await this.syncExpiring(alias, route, expiryCandidates, below);
555
- if (candidates.length && candidates.every((c) => !below(c))) {
556
- await this.telemetry.advance(alias, candidates, signature);
557
- this.active.delete(alias);
558
- const allowed = new Set(candidates.map((c) => c.key));
559
- candidates = this.candidates(alias).filter((c) => allowed.has(c.key));
560
- }
561
- candidates = candidates.filter(below);
562
- if (strategy === "tokens" && candidates.some((c) => this.telemetry.count(alias, c.account.id).unmetered > 0)) {
563
- candidates = candidates.filter((c) => this.telemetry.count(alias, c.account.id).unmetered === 0);
564
- }
565
- }
566
- preferActive ??= await this.syncExpiring(alias, route, expiryCandidates, below);
567
- const expiry = (c) => preferActive && !this.telemetry.expiring(alias).completed.includes(c.account.id) ? this.expiringAt(c.account, route) : Infinity;
568
- const expiringGroup = (c) => Number.isFinite(expiry(c));
569
- if (preferActive) {
570
- const ordered = candidates.map((candidate, index) => ({ candidate, index }));
571
- ordered.sort((a, b) => Number(expiringGroup(b.candidate)) - Number(expiringGroup(a.candidate)) || expiry(a.candidate) - expiry(b.candidate) || a.index - b.index);
572
- candidates = ordered.map((entry) => entry.candidate);
573
- }
574
- if (route.discountOrder && !route.freeOnly) candidates = this.sortWithinGroups(candidates, (candidate) => expiry(candidate), (candidate) => {
575
- const rate = route.creditRates?.[candidate.account.id];
576
- return rate === void 0 ? void 0 : rate;
577
- });
578
- if (route.balanceCredits && !route.freeOnly && this.telemetry) candidates = this.demoteOverBudget(alias, route, candidates, expiringGroup);
579
- if (strategy === "weighted") {
580
- let eligible = candidates.filter(available);
581
- const nearest = Math.min(Infinity, ...eligible.map(expiry));
582
- if (Number.isFinite(nearest)) eligible = eligible.filter((c) => expiry(c) === nearest);
583
- if (eligible.length) {
584
- let scores = this.scores.get(alias);
585
- if (scores?.signature !== signature) {
586
- scores = { signature, values: /* @__PURE__ */ new Map() };
587
- this.scores.set(alias, scores);
588
- }
589
- let total = 0, winner;
590
- for (const c of eligible) {
591
- const weight = c.target.weight ?? 1;
592
- total += weight;
593
- scores.values.set(c.key, (scores.values.get(c.key) ?? 0) + weight);
594
- if (!winner || scores.values.get(c.key) > scores.values.get(winner.key)) winner = c;
595
- }
596
- scores.values.set(winner.key, scores.values.get(winner.key) - total);
597
- candidates = [winner, ...candidates.filter((c) => c !== winner)];
598
- }
599
- }
600
- const attempts = [];
601
- const tried = /* @__PURE__ */ new Set();
602
- for (const candidate of candidates) {
603
- signal?.throwIfAborted();
604
- const { account, target, key } = candidate;
605
- if (!available(candidate)) continue;
606
- let result, actualModel, startedAt;
607
- try {
608
- const client = await abortable(this.client(account), signal);
609
- const catalog = await abortable(client.models(signal), signal);
610
- const suitable = route.enforceContext ? catalog.filter((m) => contextInfo(m).maxContextWindow >= route.contextWindow) : catalog;
611
- const model = matchModel(alias, target, suitable, effort ?? void 0);
612
- if (!model) {
613
- attempts.push({
614
- account: account.id,
615
- provider: account.provider,
616
- status: 404,
617
- kind: matchModel(alias, target, catalog, effort ?? void 0) ? "context_window_unavailable" : matchModel(alias, target, catalog) ? "reasoning_effort_unavailable" : "model_unavailable"
618
- });
619
- continue;
620
- }
621
- if (route.freeOnly && !isFreeModel(model, this.now())) {
622
- attempts.push({ account: account.id, provider: account.provider, status: 404, kind: "model_no_longer_free" });
623
- continue;
624
- }
625
- actualModel = model.id;
626
- const dedupe = `${account.id}:${model.id}`;
627
- if (tried.has(dedupe)) continue;
628
- tried.add(dedupe);
629
- startedAt = performance.now();
630
- result = await abortable(client.chat({ ...input, model: model.id }, signal, { contextWindow: route.contextWindow }), signal);
631
- signal?.throwIfAborted();
632
- if (result.ok) {
633
- this.active.set(alias, key);
634
- this.onAttempt({ account: account.id, provider: account.provider, alias, model: model.id, status: 200 });
635
- const response = this.telemetry ? meterResponse(result.response, async (summary) => {
636
- await this.telemetry.record({ ...summary, ...metadata, account: account.id, provider: account.provider, alias, model: model.id, accepted: true, status: summary.ok ? 200 : summary.cancelled ? 499 : 502 });
637
- await this.syncExpiring(alias, route, expiryCandidates, below);
638
- if (strategy === "credits" || route.preferExpiringCredits) void this.monitor?.(account).then(() => this.syncExpiring(alias, route, expiryCandidates, below)).catch(() => {
639
- });
640
- }, signal, { startedAt }) : result.response;
641
- return { response, account: account.id, provider: account.provider };
642
- }
643
- } catch (error) {
644
- signal?.throwIfAborted();
645
- const reported = error.status ?? error.failure?.status ?? /\bhttp\s+(\d{3})\b/i.exec(error.message ?? "")?.[1];
646
- result = { ok: false, status: Number(reported) || 502, kind: error.code ?? error.failure?.code ?? "transport" };
647
- }
648
- const status = result.status >= 400 && result.status <= 599 ? result.status : 502;
649
- const attempt = { account: account.id, provider: account.provider, status, kind: result.kind };
650
- attempts.push(attempt);
651
- await this.telemetry?.record({ ...attempt, ...metadata, alias, model: actualModel, ok: false, timing: startedAt === void 0 ? null : { modelMs: Math.max(0, performance.now() - startedAt) } });
652
- this.onAttempt({ ...attempt, alias });
653
- const retryable = status < 500 || this.config.retryServerErrors;
654
- if (!retryable) throw new RouteError(status, "upstream_failed", attempts, "\u4E0A\u6E38\u8FD4\u56DE 5xx \u4E14\u672A\u5F00\u542F\u300C5xx \u548C\u7F51\u7EDC\u9519\u8BEF\u4E5F\u5207\u6362\u8D26\u53F7\u300D\uFF0C\u672C\u6B21\u8BF7\u6C42\u5DF2\u505C\u6B62");
655
- const scope = [401, 402, 429].includes(status) ? "*" : route.freeOnly && actualModel ? `${alias}:${actualModel}` : alias;
656
- this.cooldowns.set(`${account.id}:${scope}`, this.now() + this.config.cooldownMs);
657
- }
658
- const hint = this.exhaustHint({ alias, route, strategy, candidates, expiryCandidates, attempts });
659
- throw new RouteError(503, route.freeOnly ? "free_model_pool_unavailable" : "account_pool_exhausted", attempts, hint);
660
- }
661
- };
662
-
663
- export {
664
- Telemetry,
665
- SessionMetrics,
666
- contextInfo,
667
- resolveContext,
668
- qoderContextModel,
669
- RouteError,
670
- abortable,
671
- matchModel,
672
- Router
673
- };
674
- //# sourceMappingURL=chunk-4L2CBGXN.js.map