@ebony-vinyl/dsh-our-free-model 2.7.4 → 2.8.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.
package/src/core.js ADDED
@@ -0,0 +1,749 @@
1
+ // src/core/completion.js
2
+ import { ROUTE_MAIN, ROUTE_REGION } from "./adapter.js";
3
+ import { toOpenAiUsage } from "./forward.js";
4
+ import { carryFailure } from "./failure.js";
5
+ import { effortsFor, defaultEffortFor } from "./effort.js";
6
+ function routableModelIds(snapshot) {
7
+ const membership = new Set(snapshot.membership[ROUTE_MAIN] ?? []);
8
+ if (snapshot.settings.exposeRegionModels !== false) {
9
+ for (const id of snapshot.membership[ROUTE_REGION] ?? []) membership.add(id);
10
+ }
11
+ return membership;
12
+ }
13
+ function publicModelRows(snapshot) {
14
+ const membership = routableModelIds(snapshot);
15
+ return snapshot.catalog.filter((entry) => membership.has(entry.id)).map((entry) => {
16
+ const efforts = effortsFor(entry, void 0, snapshot.settings.defaultMaxTokens);
17
+ return {
18
+ id: entry.id,
19
+ object: "model",
20
+ created: Math.floor(Date.now() / 1e3),
21
+ owned_by: "our-free-model",
22
+ ...entry.contextWindow === void 0 ? {} : { context_window: entry.contextWindow },
23
+ ...efforts === void 0 ? {} : {
24
+ x_ofm_efforts: efforts.map((effort) => effort.id),
25
+ x_ofm_effort_default: defaultEffortFor(entry)
26
+ }
27
+ };
28
+ });
29
+ }
30
+ function createForwardCompletion({ adapter, state, signal }) {
31
+ return async (request, onChunk) => {
32
+ if (signal?.aborted) throw Object.assign(new Error("model runtime is stopped"), { statusCode: 503 });
33
+ const snapshot = state();
34
+ const entry = snapshot.catalog.find((candidate) => candidate.id === request.model);
35
+ if (entry === void 0 || !routableModelIds(snapshot).has(entry.id)) {
36
+ throw Object.assign(new Error(`model "${request.model}" not found`), { statusCode: 404 });
37
+ }
38
+ const openAi = request.openAi ?? {};
39
+ const messages = Array.isArray(request.messages) ? request.messages : fromOpenAiMessages(openAi, request.responses === true, entry.id);
40
+ const tools = Array.isArray(request.tools) ? request.tools : (openAi.tools ?? []).map(normalizeTool).filter(Boolean);
41
+ const handler = typeof onChunk === "function" ? onChunk : () => {
42
+ };
43
+ const outcome = { text: "", toolCalls: [], usage: void 0, truncated: false, error: void 0 };
44
+ const maxTokens = Number.isFinite(request.maxTokens) ? request.maxTokens : Number.isFinite(openAi.max_tokens) ? openAi.max_tokens : void 0;
45
+ const temperature = Number.isFinite(request.temperature) ? request.temperature : Number.isFinite(openAi.temperature) ? openAi.temperature : void 0;
46
+ const reasoningEffort = typeof request.reasoningEffort === "string" ? request.reasoningEffort : typeof openAi.reasoning_effort === "string" ? openAi.reasoning_effort : void 0;
47
+ const toolChoice = request.toolChoice === void 0 ? openAi.tool_choice : request.toolChoice;
48
+ const options = {
49
+ provider: ROUTE_MAIN,
50
+ model: entry.id,
51
+ messages,
52
+ tools: tools.length > 0 ? tools : void 0,
53
+ ...temperature === void 0 ? {} : { temperature },
54
+ ...maxTokens === void 0 ? {} : { maxTokens },
55
+ ...reasoningEffort === void 0 ? {} : { reasoningEffort },
56
+ ...toolChoice === void 0 ? {} : { toolChoice },
57
+ sessionId: `forward:${String(openAi.user ?? openAi.conversation ?? request.sessionId ?? "shared")}`,
58
+ signal: request.signal && signal ? AbortSignal.any([request.signal, signal]) : request.signal ?? signal
59
+ };
60
+ for await (const chunk of adapter.stream(options, entry, snapshot)) {
61
+ handler(chunk);
62
+ foldForwardOutcome(outcome, chunk);
63
+ }
64
+ if (outcome.truncated === true) {
65
+ outcome.toolCalls = outcome.toolCalls.filter((call) => {
66
+ try {
67
+ JSON.parse(call.arguments === "" ? "{}" : call.arguments);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ });
73
+ }
74
+ return outcome;
75
+ };
76
+ }
77
+ function fromOpenAiMessages(body, isResponses, modelId) {
78
+ const out = [];
79
+ const rows = isResponses ? normaliseResponsesInput(body.input) : Array.isArray(body.messages) ? body.messages : [];
80
+ for (const row of rows) {
81
+ const role = row.role ?? "user";
82
+ const content = [];
83
+ if (typeof row.content === "string") {
84
+ if (row.content !== "") content.push({ type: "text", text: row.content });
85
+ } else if (Array.isArray(row.content)) {
86
+ for (const part of row.content) {
87
+ if (typeof part === "string") {
88
+ if (part !== "") content.push({ type: "text", text: part });
89
+ continue;
90
+ }
91
+ const text = part?.text ?? part?.input_text ?? part?.output_text;
92
+ if (typeof text === "string" && text !== "") content.push({ type: "text", text });
93
+ const image = part?.image_url?.url ?? part?.image_url;
94
+ if (typeof image === "string" && image !== "") {
95
+ content.push({ type: "image", attachment: { attachmentId: `url:${image.slice(0, 64)}`, mediaType: "image/png", bytes: 0, width: 0, height: 0, url: image } });
96
+ }
97
+ }
98
+ }
99
+ if (role === "tool") {
100
+ out.push({ role: "tool", content: [{ type: "text", text: typeof row.content === "string" ? row.content : JSON.stringify(row.content ?? "") }], toolCallId: row.tool_call_id ?? "", source: { kind: "tool", callId: row.tool_call_id ?? "" } });
101
+ continue;
102
+ }
103
+ if (role === "assistant" && Array.isArray(row.tool_calls)) {
104
+ for (const call of row.tool_calls) {
105
+ content.push({ type: "tool-call", id: call.id ?? "", name: call.function?.name ?? "", arguments: call.function?.arguments ?? "{}" });
106
+ }
107
+ }
108
+ if (content.length === 0) continue;
109
+ out.push({
110
+ role: role === "developer" ? "developer" : role === "system" ? "system" : role === "assistant" ? "assistant" : "user",
111
+ content,
112
+ ...role === "assistant" && typeof row.reasoning_content === "string" ? { reasoning_content: row.reasoning_content } : {},
113
+ ...role === "assistant" ? { source: { kind: "model", provider: ROUTE_MAIN, model: String(modelId ?? "") } } : {}
114
+ });
115
+ }
116
+ return out;
117
+ }
118
+ function normaliseResponsesInput(input) {
119
+ if (typeof input === "string") return [{ role: "user", content: input }];
120
+ if (!Array.isArray(input)) return [];
121
+ return input.map((row) => {
122
+ if (typeof row === "string") return { role: "user", content: row };
123
+ if (row.type === "function_call") return { role: "assistant", content: [], tool_calls: [{ id: row.call_id, function: { name: row.name, arguments: row.arguments } }] };
124
+ if (row.type === "function_call_output") return { role: "tool", content: String(row.output ?? ""), tool_call_id: row.call_id };
125
+ return row;
126
+ });
127
+ }
128
+ function normalizeTool(tool) {
129
+ const name = tool?.name ?? tool?.function?.name;
130
+ if (typeof name !== "string" || name.trim() === "") return null;
131
+ const parameters = tool?.parameters ?? tool?.function?.parameters ?? { type: "object", properties: {} };
132
+ return { name, description: String(tool?.description ?? tool?.function?.description ?? ""), parameters };
133
+ }
134
+ function foldForwardOutcome(outcome, chunk) {
135
+ switch (chunk.type) {
136
+ case "text-delta":
137
+ outcome.text += chunk.text;
138
+ break;
139
+ case "tool-call-delta": {
140
+ let call = outcome.toolCalls.find((candidate) => candidate.slot === chunk.index);
141
+ if (call === void 0) {
142
+ call = { slot: chunk.index, id: chunk.id ?? "", name: chunk.name ?? "", arguments: chunk.argumentsDelta ?? "" };
143
+ outcome.toolCalls.push(call);
144
+ } else call.arguments += chunk.argumentsDelta ?? "";
145
+ if (chunk.name) call.name = chunk.name;
146
+ if (chunk.id) call.id = chunk.id;
147
+ break;
148
+ }
149
+ case "block-end":
150
+ if (chunk.block?.type === "tool-call") {
151
+ const bySlot = outcome.toolCalls.find((candidate) => candidate.slot === chunk.index);
152
+ if (bySlot !== void 0) {
153
+ if (typeof chunk.block.id === "string" && chunk.block.id !== "") bySlot.id = chunk.block.id;
154
+ if (typeof chunk.block.name === "string" && chunk.block.name !== "") bySlot.name = chunk.block.name;
155
+ if (typeof chunk.block.arguments === "string") bySlot.arguments = chunk.block.arguments;
156
+ break;
157
+ }
158
+ const byId = outcome.toolCalls.find((candidate) => typeof candidate.id === "string" && candidate.id !== "" && candidate.id === chunk.block.id);
159
+ if (byId === void 0) {
160
+ outcome.toolCalls.push({
161
+ slot: chunk.index,
162
+ id: typeof chunk.block.id === "string" ? chunk.block.id : "",
163
+ name: typeof chunk.block.name === "string" ? chunk.block.name : "",
164
+ arguments: typeof chunk.block.arguments === "string" ? chunk.block.arguments : ""
165
+ });
166
+ }
167
+ }
168
+ break;
169
+ case "usage":
170
+ outcome.usage = toOpenAiUsage(chunk.usage);
171
+ break;
172
+ case "finish":
173
+ if (chunk.reason?.kind === "max-tokens") outcome.truncated = true;
174
+ if (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted") {
175
+ outcome.failure = carryFailure(chunk.reason.failure, chunk.reason.kind);
176
+ outcome.error = outcome.failure.message;
177
+ }
178
+ break;
179
+ default:
180
+ break;
181
+ }
182
+ return outcome;
183
+ }
184
+
185
+ // src/core/runtime.js
186
+ import fs from "node:fs";
187
+ import path from "node:path";
188
+ import { FreeModelAdapter, ROUTE_MAIN as ROUTE_MAIN2, ROUTE_REGION as ROUTE_REGION2 } from "./adapter.js";
189
+ import { buildCatalog, buildEacCatalog, buildKiloCatalog, isEacEntry, isKiloEntry, parseListing, reviveKiloCatalog } from "./catalog.js";
190
+ import { buildAggregateCatalog, isAggregateEntry } from "./aggregate.js";
191
+ import { buildComboCatalog, isComboEntry, normalizeCombos, pruneRotation } from "./combos.js";
192
+ import { JsonStore, SETTINGS_INITIAL, STATS_INITIAL, STATS_VERSION, migrateStats, pruneDays, recordTurn, recordUsage, isModelHidden } from "./store.js";
193
+ import { STATE, detectEgress, probeCatalog } from "./probe.js";
194
+ import { CODE, UpstreamError, getJson } from "./http.js";
195
+ import { runModelTest, modelAvailability } from "./model-test.js";
196
+ import { DEFAULT_LEVEL } from "./effort.js";
197
+ import { fetchSealedListing } from "./eac.js";
198
+ import { fetchKiloListing } from "./kilo.js";
199
+ import { sessionForConversation } from "./upstream.js";
200
+ import { isSubscriptionEntry, subscriptionCatalog } from "./subscription-adapter.js";
201
+ var FALLBACK_CATALOG = buildCatalog([
202
+ "mimo-v2.6-flash-free",
203
+ "mimo-v2.5-free",
204
+ "ling-3.0-flash-fin-free",
205
+ "nemotron-3-ultra-free",
206
+ "nemotron-3.5-lightning-free",
207
+ "space-bunny-free",
208
+ "muse-spark-1.3-contributor-free",
209
+ "muse-spark-1.2-contributor-free"
210
+ ]);
211
+ function createRuntimeStores({ dataDir, logger = console }) {
212
+ if (typeof dataDir !== "string" || !path.isAbsolute(dataDir)) {
213
+ throw new TypeError("dataDir must be an absolute path");
214
+ }
215
+ fs.mkdirSync(dataDir, { recursive: true, mode: 448 });
216
+ const open = (file, initial) => new JsonStore(path.join(dataDir, file), structuredClone(initial), {
217
+ log: (message) => logger.warn?.(message)
218
+ });
219
+ const settingsInitial = structuredClone(SETTINGS_INITIAL);
220
+ const settingsPath = path.join(dataDir, "settings.json");
221
+ if (fs.existsSync(settingsPath)) settingsInitial.combos = [];
222
+ const settings = new JsonStore(settingsPath, settingsInitial, {
223
+ log: (message) => logger.warn?.(message)
224
+ });
225
+ const stats = open("stats.json", STATS_INITIAL);
226
+ const availability = open("availability.json", { version: 1, at: 0, egress: null, results: {} });
227
+ const catalogStore = open("catalog.json", { version: 1, at: 0, entries: FALLBACK_CATALOG.map((entry) => entry.id) });
228
+ if (stats.get().version !== STATS_VERSION) stats.edit(migrateStats);
229
+ return {
230
+ settings,
231
+ stats,
232
+ availability,
233
+ catalogStore,
234
+ dispose() {
235
+ settings.dispose();
236
+ stats.dispose();
237
+ availability.dispose();
238
+ catalogStore.dispose();
239
+ }
240
+ };
241
+ }
242
+ function createModelRuntime({
243
+ settings,
244
+ stats,
245
+ availability,
246
+ catalogStore,
247
+ logger = console,
248
+ attributionUserAgent = "our-free-model",
249
+ resolveImage,
250
+ sealedCredential = () => null,
251
+ onSealedUnavailable = () => {
252
+ },
253
+ onTopology = () => {
254
+ },
255
+ subscriptionRuntime
256
+ }) {
257
+ let catalog = materializeCatalog(catalogStore.get().entries ?? []);
258
+ let sealedCatalog = sealedCredential() === null ? [] : buildEacCatalog(catalogStore.get().sealIds ?? []);
259
+ let kiloCatalog = reviveKiloCatalog(catalogStore.get().kiloRows);
260
+ let aggregateCatalog = [];
261
+ let subscriptionEntries = [];
262
+ let comboCatalog = [];
263
+ let freeCatalog = catalog;
264
+ let egress = availability.get().egress ?? null;
265
+ let gatewayLatency = { ms: 0, at: 0 };
266
+ let disposed = false;
267
+ const lifetime = new AbortController();
268
+ let egressGeneration = 0;
269
+ const rebuildCatalog = () => {
270
+ const seen = /* @__PURE__ */ new Set();
271
+ const merged = [];
272
+ for (const entry of [...freeCatalog, ...sealedCatalog, ...kiloCatalog, ...aggregateCatalog, ...subscriptionEntries]) {
273
+ if (entry === null || entry === void 0 || seen.has(entry.id)) continue;
274
+ seen.add(entry.id);
275
+ merged.push(entry);
276
+ }
277
+ const combos = normalizeCombos(settings.get().combos);
278
+ comboCatalog = buildComboCatalog(combos, merged);
279
+ pruneRotation(combos);
280
+ for (const entry of comboCatalog) {
281
+ if (seen.has(entry.id)) continue;
282
+ seen.add(entry.id);
283
+ merged.push(entry);
284
+ }
285
+ catalog = merged;
286
+ };
287
+ const mergeCatalogs = () => {
288
+ aggregateCatalog = buildAggregateCatalog(settings.get().aggregate ?? {});
289
+ subscriptionEntries = subscriptionCatalog(subscriptionRuntime);
290
+ rebuildCatalog();
291
+ };
292
+ mergeCatalogs();
293
+ const state = () => ({
294
+ catalog,
295
+ membership: computeMembership(catalog, availability.get(), settings.get()),
296
+ settings: settings.get(),
297
+ attributionUserAgent,
298
+ routing: {
299
+ quota: subscriptionRuntime?.routingQuota?.() ?? {},
300
+ usage: Object.fromEntries(catalog.map((row) => [row.id, stats.get().models?.[row.id]?.calls ?? 0]))
301
+ }
302
+ });
303
+ const emitTopology = () => {
304
+ if (!disposed) onTopology();
305
+ };
306
+ const adapter = new FreeModelAdapter({
307
+ state,
308
+ resolveImage,
309
+ subscriptionRuntime,
310
+ sealedCredential,
311
+ recordUsage: (record) => {
312
+ if (disposed) return;
313
+ recordUsage(stats, record);
314
+ stats.edit((value) => pruneDays(value, 120));
315
+ },
316
+ recordTurn: (record) => {
317
+ if (!disposed) recordTurn(stats, record);
318
+ },
319
+ warn: (message) => logger.warn?.(message) ?? logger.log?.(message)
320
+ // Automatic probing is disabled by owner policy: a region refusal from a
321
+ // real turn stays a turn failure and never fires a background probe.
322
+ });
323
+ let catalogRefresh = null;
324
+ let catalogRefreshForced = false;
325
+ let catalogRefreshProbed = false;
326
+ async function refreshCatalog(opts) {
327
+ const { probe = false, force = false, report = false } = opts ?? {};
328
+ const result = (value) => report ? value : value.catalog;
329
+ if (disposed) return result({ catalog, sources: {} });
330
+ while (catalogRefresh !== null) {
331
+ const shared = catalogRefresh;
332
+ const sharedForced = catalogRefreshForced;
333
+ const sharedProbed = catalogRefreshProbed;
334
+ const value = await shared;
335
+ if (disposed || (!force || sharedForced) && (!probe || sharedProbed)) return result(value);
336
+ }
337
+ const run = refreshCatalogOnce({ probe, force });
338
+ catalogRefresh = run;
339
+ catalogRefreshForced = force;
340
+ catalogRefreshProbed = probe;
341
+ try {
342
+ return result(await run);
343
+ } finally {
344
+ if (catalogRefresh === run) {
345
+ catalogRefresh = null;
346
+ catalogRefreshForced = false;
347
+ catalogRefreshProbed = false;
348
+ }
349
+ }
350
+ }
351
+ async function refreshCatalogOnce({ probe, force }) {
352
+ const sources = {};
353
+ let ids = [];
354
+ try {
355
+ ids = parseListing(await fetchListing());
356
+ } catch (error) {
357
+ if (!disposed) logger.warn?.(`our-free-model: model listing refresh failed (${error?.message ?? error}); keeping the cached catalog`);
358
+ }
359
+ if (disposed) return { catalog, sources };
360
+ if (ids.length > 0) {
361
+ freeCatalog = buildCatalog(ids);
362
+ catalogStore.update({ at: Date.now(), entries: freeCatalog.map((entry) => entry.id) });
363
+ catalogStore.flush();
364
+ settings.update({ catalogSyncedAt: Date.now() });
365
+ } else {
366
+ freeCatalog = materializeCatalog(catalogStore.get().entries ?? []);
367
+ }
368
+ sources.free = { ok: ids.length > 0, count: freeCatalog.length };
369
+ sources.sealed = await refreshSealedRoster();
370
+ if (disposed) return { catalog, sources };
371
+ sources.kilo = { ok: await refreshKiloRoster(), count: kiloCatalog.length };
372
+ if (disposed) return { catalog, sources };
373
+ mergeCatalogs();
374
+ if (probe) await refreshAvailability(force);
375
+ emitTopology();
376
+ return { catalog, sources };
377
+ }
378
+ async function refreshSealedRoster() {
379
+ const credential = sealedCredential();
380
+ if (credential === null) {
381
+ sealedCatalog = [];
382
+ onSealedUnavailable();
383
+ return { ok: true, count: 0, skipped: true };
384
+ }
385
+ try {
386
+ const ids = parseListing(await fetchSealedListing(credential, { signal: lifetime.signal }));
387
+ if (disposed) return;
388
+ if (Object.hasOwn(credential, "userToken") && sealedCredential()?.userToken !== credential.userToken) return { ok: false, count: sealedCatalog.length, skipped: true };
389
+ if (ids.length > 0) {
390
+ sealedCatalog = buildEacCatalog(ids);
391
+ catalogStore.update({ sealIds: sealedCatalog.map((entry) => entry.id) });
392
+ }
393
+ return { ok: ids.length > 0, count: sealedCatalog.length };
394
+ } catch (error) {
395
+ if (disposed) return;
396
+ if (Object.hasOwn(credential, "userToken") && sealedCredential()?.userToken !== credential.userToken) return { ok: false, count: sealedCatalog.length, skipped: true };
397
+ if (error?.code === CODE.credential) {
398
+ sealedCatalog = [];
399
+ catalogStore.update({ sealIds: [] });
400
+ logger.warn?.("our-free-model: the sealed lane refused its credential; its models are hidden until it is accepted again");
401
+ return { ok: false, count: 0 };
402
+ }
403
+ logger.warn?.(`our-free-model: sealed lane listing failed (${error?.code ?? "unknown"}); keeping its cached roster`);
404
+ return { ok: false, count: sealedCatalog.length };
405
+ }
406
+ }
407
+ async function refreshSealedLane() {
408
+ await refreshSealedRoster();
409
+ if (disposed) return;
410
+ mergeCatalogs();
411
+ emitTopology();
412
+ }
413
+ async function refreshKiloRoster() {
414
+ try {
415
+ const payload = await fetchKiloListing({ signal: lifetime.signal });
416
+ if (disposed) return;
417
+ if (payload?.error || !Array.isArray(payload?.data)) throw new Error("invalid Kilo model listing");
418
+ kiloCatalog = buildKiloCatalog(payload.data);
419
+ catalogStore.update({ kiloRows: kiloCatalog });
420
+ return true;
421
+ } catch (error) {
422
+ if (!disposed) logger.warn?.(`our-free-model: Kilo channel listing failed (${error?.code ?? error?.message ?? "unknown"}); keeping its cached roster`);
423
+ return false;
424
+ }
425
+ }
426
+ async function fetchListing() {
427
+ if (disposed) return { data: [] };
428
+ const started = Date.now();
429
+ const listing = await getJson("/inference/v1/models", {
430
+ session: sessionForConversation("catalog:our-free-model"),
431
+ attributionUserAgent,
432
+ signal: lifetime.signal
433
+ });
434
+ if (!disposed) gatewayLatency = { ms: Date.now() - started, at: Date.now() };
435
+ return listing;
436
+ }
437
+ let probeRound = null;
438
+ let probeRoundGeneration = 0;
439
+ let probeThrottleStreak = 0;
440
+ let probeBackoffUntil = 0;
441
+ async function runProbeRound() {
442
+ const generation = egressGeneration;
443
+ const probeable = catalog.filter((entry) => !isEacEntry(entry) && !isKiloEntry(entry) && !isAggregateEntry(entry) && !isComboEntry(entry) && !isSubscriptionEntry(entry));
444
+ const results = await probeCatalog(probeable, { attributionUserAgent, signal: lifetime.signal }, (id, result) => {
445
+ if (disposed || generation !== egressGeneration) return;
446
+ availability.edit((value) => ({
447
+ ...value,
448
+ results: {
449
+ ...value.results,
450
+ [id]: {
451
+ state: result.state,
452
+ ...result.detail === void 0 ? {} : { detail: result.detail },
453
+ ...result.ttftMs === void 0 ? {} : { ttftMs: result.ttftMs },
454
+ latencyMs: result.latencyMs,
455
+ startedAt: result.startedAt,
456
+ at: Date.now()
457
+ }
458
+ }
459
+ }));
460
+ }, 2);
461
+ if (disposed || generation !== egressGeneration) return {};
462
+ availability.update({ at: Date.now(), egress });
463
+ availability.flush();
464
+ const verdicts = Object.values(results);
465
+ if (verdicts.length > 0 && verdicts.every((row) => row.state === STATE.unavailable)) {
466
+ logger.warn?.(`our-free-model: the gateway refused all ${verdicts.length} models this round (${verdicts[0].detail ?? "no detail"}); keeping them advertised`);
467
+ }
468
+ const allThrottled = verdicts.length > 0 && verdicts.every((row) => row.state === STATE.throttled);
469
+ probeThrottleStreak = allThrottled ? probeThrottleStreak + 1 : 0;
470
+ probeBackoffUntil = allThrottled ? Date.now() + Math.min(30 * 2 ** (probeThrottleStreak - 1), 120) * 6e4 : 0;
471
+ if (allThrottled) {
472
+ logger.warn?.(`our-free-model: the probe round hit the lane's quota; availability probes pause for ${Math.round((probeBackoffUntil - Date.now()) / 6e4)} minutes (your own requests are unaffected, and the reprobe button forces a round)`);
473
+ }
474
+ emitTopology();
475
+ return results;
476
+ }
477
+ async function refreshAvailability(force = false) {
478
+ if (disposed || !force && probeBackoffUntil > Date.now()) return {};
479
+ if (probeRound !== null && probeRoundGeneration === egressGeneration) return probeRound;
480
+ probeRoundGeneration = egressGeneration;
481
+ const round = runProbeRound();
482
+ probeRound = round;
483
+ try {
484
+ return await round;
485
+ } finally {
486
+ if (probeRound === round) probeRound = null;
487
+ }
488
+ }
489
+ async function watchEgress() {
490
+ if (disposed) return false;
491
+ const seen = await detectEgress({ signal: lifetime.signal });
492
+ if (disposed || seen === void 0) return false;
493
+ const previous = availability.get().egress;
494
+ const changed = previous === null || previous === void 0 || previous.ip !== seen.ip || seen.country !== void 0 && previous.country !== seen.country;
495
+ egress = seen;
496
+ if (changed) {
497
+ egressGeneration++;
498
+ const expire = (rows) => Object.fromEntries(Object.entries(rows ?? {}).map(([id, row]) => [id, { ...row, stale: true }]));
499
+ availability.edit((value) => ({
500
+ ...value,
501
+ egress: seen,
502
+ at: 0,
503
+ results: expire(value.results),
504
+ manualResults: expire(value.manualResults)
505
+ }));
506
+ availability.flush();
507
+ probeThrottleStreak = 0;
508
+ probeBackoffUntil = 0;
509
+ emitTopology();
510
+ logger.info?.(`our-free-model: egress changed to ${seen.ip}${seen.country ? ` (${seen.country})` : ""}; previous availability marked stale`);
511
+ }
512
+ return true;
513
+ }
514
+ const latestTests = /* @__PURE__ */ new Map();
515
+ async function testModel(id, effort = DEFAULT_LEVEL, { signal } = {}) {
516
+ if (disposed) throw new UpstreamError("Model runtime stopped", CODE.aborted);
517
+ if (settings.get().enabled === false) throw new UpstreamError("Inference is paused", CODE.client);
518
+ const entry = catalog.find((candidate) => candidate.id === id);
519
+ if (!entry) throw new UpstreamError('Unknown model "' + id + '"', CODE.client, { status: 404 });
520
+ const token = {};
521
+ latestTests.set(id, token);
522
+ const generation = egressGeneration;
523
+ const selectedEffort = effort === void 0 || effort === "" ? DEFAULT_LEVEL : effort;
524
+ const linkedSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
525
+ const persist = (row) => {
526
+ if (disposed || linkedSignal.aborted || latestTests.get(id) !== token || generation !== egressGeneration) return false;
527
+ availability.edit((value) => ({ ...value, manualResults: { ...value.manualResults, [id]: row } }));
528
+ availability.flush();
529
+ emitTopology();
530
+ return !availability.writeFailed;
531
+ };
532
+ try {
533
+ const measured = await runModelTest((testSignal) => adapter.stream({
534
+ provider: ROUTE_MAIN2,
535
+ model: entry.id,
536
+ messages: [{ role: "user", content: "Reply with exactly: OK" }],
537
+ reasoningEffort: selectedEffort,
538
+ sessionId: "bench:" + entry.id + ":" + selectedEffort,
539
+ signal: testSignal
540
+ }, entry, state()), { signal: linkedSignal });
541
+ if (linkedSignal.aborted || disposed) throw new UpstreamError("Text test cancelled", CODE.aborted);
542
+ if (generation !== egressGeneration) throw new UpstreamError("Network egress changed during the text test; please test again", CODE.aborted);
543
+ const persisted = persist({ state: STATE.available, source: "manual", at: Date.now(), ttftMs: measured.ttftMs, latencyMs: measured.totalMs });
544
+ return { model: entry.id, effort: selectedEffort, ok: true, ...measured, persisted };
545
+ } catch (error) {
546
+ if (error?.code !== CODE.aborted) persist({ state: STATE.unknown, source: "manual", at: Date.now(), detail: String(error?.message ?? error).slice(0, 200) });
547
+ throw error;
548
+ } finally {
549
+ if (latestTests.get(id) === token) latestTests.delete(id);
550
+ }
551
+ }
552
+ return {
553
+ testModel,
554
+ state,
555
+ adapter,
556
+ refreshCatalog,
557
+ refreshSealedLane,
558
+ refreshAvailability,
559
+ watchEgress,
560
+ fetchListing,
561
+ complete: createForwardCompletion({
562
+ adapter,
563
+ signal: lifetime.signal,
564
+ state: () => {
565
+ const snapshot = state();
566
+ return { ...snapshot, membership: computeMembership(snapshot.catalog, availability.get(), {
567
+ ...snapshot.settings,
568
+ hiddenModelIds: [],
569
+ hideFreeModels: false,
570
+ hideEacModels: false
571
+ }) };
572
+ }
573
+ }),
574
+ publicModelRows: () => publicModelRows(state()),
575
+ // A settings write that changes the aggregate lane or the chains (a key
576
+ // saved, a subscription imported/toggled/removed, a combo edited) rebuilds
577
+ // the merged view without an upstream listing round.
578
+ syncAggregateCatalog() {
579
+ if (disposed) return catalog;
580
+ mergeCatalogs();
581
+ emitTopology();
582
+ return catalog;
583
+ },
584
+ get catalog() {
585
+ return catalog;
586
+ },
587
+ get egress() {
588
+ return egress;
589
+ },
590
+ get gatewayLatency() {
591
+ return gatewayLatency;
592
+ },
593
+ setAttributionUserAgent(value) {
594
+ attributionUserAgent = value;
595
+ },
596
+ dispose() {
597
+ if (disposed) return;
598
+ disposed = true;
599
+ lifetime.abort();
600
+ }
601
+ };
602
+ }
603
+ function materializeCatalog(ids) {
604
+ const rebuilt = buildCatalog(ids);
605
+ return rebuilt.length > 0 ? rebuilt : FALLBACK_CATALOG;
606
+ }
607
+ function computeMembership(catalog, availabilitySnapshot, settings) {
608
+ const expose = settings?.exposeRegionModels !== false;
609
+ const verdictOf = (entry) => modelAvailability(entry.id, availabilitySnapshot, Boolean(entry.channel)).availability;
610
+ const probeable = catalog.filter((entry) => !entry.channel);
611
+ const refusedAll = probeable.length > 0 && probeable.every((entry) => verdictOf(entry) === STATE.unavailable);
612
+ let usable = catalog.filter((entry) => verdictOf(entry) !== STATE.unavailable);
613
+ if (refusedAll) usable = catalog;
614
+ const main = [];
615
+ const region = [];
616
+ for (const entry of usable) {
617
+ if (isModelHidden(entry, settings)) continue;
618
+ const verdict = verdictOf(entry);
619
+ if (verdict !== STATE.regionBlocked) main.push(entry.id);
620
+ else if (expose) region.push(entry.id);
621
+ }
622
+ const membership = {};
623
+ if (main.length > 0) membership[ROUTE_MAIN2] = main;
624
+ if (region.length > 0) membership[ROUTE_REGION2] = region;
625
+ return membership;
626
+ }
627
+
628
+ // src/core/stats.js
629
+ import { MIN_DECODE_MS } from "./store.js";
630
+ import { describeSavingsTotal, emptySavings, savingsPercent } from "./token-saver.js";
631
+ function buildStats(stats, catalog) {
632
+ const days = stats.days ?? {};
633
+ const series = Object.keys(days).sort().map((day) => ({
634
+ day,
635
+ total: days[day].total ?? 0,
636
+ models: Object.entries(days[day].models ?? {}).map(([model, value]) => ({ model, ...value }))
637
+ }));
638
+ const totals = {};
639
+ for (const entry of series) for (const row of entry.models) {
640
+ const previous = totals[row.model] ?? {
641
+ model: row.model,
642
+ input: 0,
643
+ output: 0,
644
+ reasoning: 0,
645
+ calls: 0,
646
+ failed: 0,
647
+ ttftMs: 0,
648
+ ttftSamples: 0,
649
+ decodeMs: 0,
650
+ decodeTokens: 0
651
+ };
652
+ totals[row.model] = {
653
+ ...previous,
654
+ input: previous.input + row.input,
655
+ output: previous.output + row.output,
656
+ reasoning: previous.reasoning + row.reasoning,
657
+ calls: previous.calls + row.calls,
658
+ failed: previous.failed + row.failed,
659
+ ttftMs: previous.ttftMs + (row.ttftMs ?? 0),
660
+ ttftSamples: previous.ttftSamples + (row.ttftSamples ?? 0),
661
+ decodeMs: previous.decodeMs + (row.decodeMs ?? 0),
662
+ decodeTokens: previous.decodeTokens + (row.decodeTokens ?? 0)
663
+ };
664
+ }
665
+ const logical = stats.logical ?? {};
666
+ const logicalModels = logical.models ?? {};
667
+ const logicalReady = Number.isSafeInteger(logical.turns);
668
+ const lifetimeModels = stats.models ?? {};
669
+ const empty = (model) => ({
670
+ model,
671
+ input: 0,
672
+ output: 0,
673
+ reasoning: 0,
674
+ cacheRead: 0,
675
+ calls: 0,
676
+ failed: 0,
677
+ ttftMs: 0,
678
+ ttftSamples: 0,
679
+ decodeMs: 0,
680
+ decodeTokens: 0
681
+ });
682
+ const modelIds = /* @__PURE__ */ new Set([...Object.keys(totals), ...Object.keys(lifetimeModels), ...Object.keys(logicalModels)]);
683
+ const named = [...modelIds].map((model) => {
684
+ const row = totals[model] ?? empty(model);
685
+ const lifetime = lifetimeModels[model] ?? {};
686
+ const physical = {
687
+ ...row,
688
+ input: Number.isSafeInteger(lifetime.input) ? lifetime.input : row.input,
689
+ output: Number.isSafeInteger(lifetime.output) ? lifetime.output : row.output,
690
+ reasoning: Number.isSafeInteger(lifetime.reasoning) ? lifetime.reasoning : row.reasoning,
691
+ cacheRead: Number.isSafeInteger(lifetime.cacheRead) ? lifetime.cacheRead : row.cacheRead,
692
+ calls: Number.isSafeInteger(lifetime.calls) ? lifetime.calls : row.calls,
693
+ failed: Number.isSafeInteger(lifetime.failed) ? lifetime.failed : row.failed
694
+ };
695
+ return {
696
+ ...physical,
697
+ turns: logicalModels[model]?.turns ?? (logicalReady ? 0 : row.calls),
698
+ failedTurns: logicalModels[model]?.failed ?? (logicalReady ? 0 : row.failed),
699
+ recoveredTurns: logicalModels[model]?.recovered ?? 0,
700
+ name: catalog.find((entry) => entry.id === model)?.name ?? model,
701
+ // A rate over too few measurable calls is a rounding error with a unit on it.
702
+ tps: row.decodeMs >= MIN_DECODE_MS ? Math.round(row.decodeTokens / (row.decodeMs / 1e3)) : null,
703
+ avgTtftMs: row.ttftSamples > 0 ? Math.round(row.ttftMs / row.ttftSamples) : null
704
+ };
705
+ });
706
+ const physicalFailed = named.reduce((sum, row) => sum + row.failed, 0);
707
+ const turns = logicalReady ? logical.turns : named.reduce((sum, row) => sum + row.turns, 0);
708
+ const failedTurns = Number.isSafeInteger(logical.failed) ? logical.failed : named.reduce((sum, row) => sum + row.failedTurns, 0);
709
+ const recoveredTurns = Number.isSafeInteger(logical.recovered) ? logical.recovered : named.reduce((sum, row) => sum + row.recoveredTurns, 0);
710
+ const hasLifetimeFailures = Number.isSafeInteger(stats.failedRequests);
711
+ const savings = { ...emptySavings(), ...stats.savings ?? {} };
712
+ return {
713
+ requests: stats.requests ?? 0,
714
+ requestFailures: hasLifetimeFailures ? stats.failedRequests : physicalFailed,
715
+ requestFailuresEstimated: stats.failedRequestsEstimated === true || !hasLifetimeFailures,
716
+ logicalEstimated: logical.estimated === true,
717
+ turns,
718
+ failedTurns,
719
+ recoveredTurns,
720
+ days: series,
721
+ models: named,
722
+ samples: (stats.samples ?? []).slice(-200),
723
+ // What the token savers measured. The percentage is computed here, from the
724
+ // same bytes the rows above counted, so the panel never has to derive a rate
725
+ // from two numbers that were rounded separately.
726
+ savings: { ...savings, percent: savingsPercent(savings), summary: describeSavingsTotal(savings) },
727
+ grand: {
728
+ input: named.reduce((sum, row) => sum + row.input, 0),
729
+ output: named.reduce((sum, row) => sum + row.output, 0),
730
+ reasoning: named.reduce((sum, row) => sum + row.reasoning, 0),
731
+ calls: named.reduce((sum, row) => sum + row.calls, 0),
732
+ failed: hasLifetimeFailures ? stats.failedRequests : physicalFailed,
733
+ turns,
734
+ failedTurns,
735
+ recoveredTurns
736
+ }
737
+ };
738
+ }
739
+ export {
740
+ buildStats,
741
+ computeMembership,
742
+ createForwardCompletion,
743
+ createModelRuntime,
744
+ createRuntimeStores,
745
+ foldForwardOutcome,
746
+ fromOpenAiMessages,
747
+ publicModelRows,
748
+ routableModelIds
749
+ };