@ebony-vinyl/dsh-our-free-model 2.8.0 → 2.8.2
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/README.md +12 -7
- package/client.js +894 -6
- package/index.js +245 -8
- package/package.json +4 -1
- package/src/adapter.js +54 -6
- package/src/aggregate.js +53 -13
- package/src/bun-fetch.js +33 -10
- package/src/catalog.js +140 -16
- package/src/core.js +122 -33
- package/src/egress.js +204 -3
- package/src/engine-parts/win32-x64.000.part +0 -0
- package/src/engine-parts/win32-x64.001.part +0 -0
- package/src/engine-parts/win32-x64.002.part +0 -0
- package/src/engine-parts/win32-x64.003.part +0 -0
- package/src/engine-parts/win32-x64.004.part +0 -0
- package/src/engine-parts/win32-x64.005.part +0 -0
- package/src/engine-parts/win32-x64.006.part +0 -0
- package/src/extensions.js +1888 -0
- package/src/feed.js +6 -0
- package/src/http.js +14 -3
- package/src/kilo.js +55 -5
- package/src/magpie-engine.js +56 -2
- package/src/magpie-models.js +31 -3
- package/src/model-test.js +3 -1
- package/src/o2a.js +207 -8
- package/src/open-external.js +43 -5
- package/src/plugins.js +1240 -0
- package/src/providers/registry.json +72 -30
- package/src/semver.js +40 -0
- package/src/store.js +17 -1
- package/src/stream.js +39 -4
- package/src/subscription-completion.js +11 -2
- package/src/updater.js +5 -33
- package/src/upstream.js +43 -0
- package/vendor/channel-pack/NOTICE.md +1 -1
- package/vendor/channel-pack/pack.js +7242 -3952
- package/vendor/magpie/NOTICE.md +7 -2
- package/vendor/magpie/engine.json +18 -12
package/src/bun-fetch.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { accessSync, constants, statSync, writeFileSync, mkdtempSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync, statSync, writeFileSync, mkdtempSync } from 'node:fs';
|
|
2
2
|
import { homedir, tmpdir } from 'node:os';
|
|
3
3
|
import { delimiter, join, resolve } from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { PassThrough, Readable } from 'node:stream';
|
|
6
|
+
import { DATA_DIR_NAME, resolveDshHome } from './store.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The worker source, inlined so the published package stays inside the
|
|
9
10
|
* historical updater's 80-file ceiling (adding a second file overran it).
|
|
10
|
-
* It is written once to
|
|
11
|
-
*
|
|
12
|
-
* line
|
|
11
|
+
* It is written once to the plugin data directory and reused for the process
|
|
12
|
+
* lifetime, re-created when an external sweep deletes it. It speaks one JSON
|
|
13
|
+
* line on stdin and returns one metadata line plus raw response bytes on
|
|
14
|
+
* stdout; no request content is evaluated.
|
|
13
15
|
*/
|
|
14
16
|
const WORKER_SOURCE = `import { Readable } from 'node:stream';
|
|
15
17
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -37,13 +39,34 @@ try {
|
|
|
37
39
|
|
|
38
40
|
let workerPath;
|
|
39
41
|
|
|
40
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Materialize the inlined worker; never executes request content.
|
|
44
|
+
*
|
|
45
|
+
* The file lives under the plugin's own data directory rather than the OS temp
|
|
46
|
+
* directory, and its existence is re-checked on every call. Both details are
|
|
47
|
+
* load-bearing: the path used to be memoized into `tmpdir()`, so a temp sweep
|
|
48
|
+
* that deleted `ofm-bun-worker-*` left every later request in that process
|
|
49
|
+
* failing with `Bun worker failed before completing the response` — bun exits 1
|
|
50
|
+
* on a missing entry script, before it can reach the network — until the whole
|
|
51
|
+
* plugin was restarted. Re-materializing a missing file makes the sweep
|
|
52
|
+
* self-healing, and an unwritable home still falls back to temp so the lane
|
|
53
|
+
* degrades to the previous behavior rather than failing to start.
|
|
54
|
+
*/
|
|
41
55
|
export function bunWorkerPath() {
|
|
42
|
-
if (workerPath !== undefined) return workerPath;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
if (workerPath !== undefined && existsSync(workerPath)) return workerPath;
|
|
57
|
+
try {
|
|
58
|
+
const dir = join(resolveDshHome(), DATA_DIR_NAME, 'run');
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
const file = join(dir, 'worker.mjs');
|
|
61
|
+
writeFileSync(file, WORKER_SOURCE, { mode: 0o600 });
|
|
62
|
+
workerPath = file;
|
|
63
|
+
} catch {
|
|
64
|
+
// mkdir may succeed for an existing directory even when writing the worker
|
|
65
|
+
// fails. Both operations must be covered by the fallback.
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), 'ofm-bun-worker-'));
|
|
67
|
+
workerPath = join(dir, 'worker.mjs');
|
|
68
|
+
writeFileSync(workerPath, WORKER_SOURCE, { mode: 0o600 });
|
|
69
|
+
}
|
|
47
70
|
return workerPath;
|
|
48
71
|
}
|
|
49
72
|
|
package/src/catalog.js
CHANGED
|
@@ -17,8 +17,11 @@ import { baseModelId, isResponsesModel } from './upstream.js'
|
|
|
17
17
|
/** Free-tier ids the listing does not mark with a `-free` suffix. `big-pickle`
|
|
18
18
|
* is the lane's own long-standing free id (9router keeps the same id in its
|
|
19
19
|
* `KNOWN_FREE_OPENCODE_MODELS` for exactly this reason); models.dev prices it
|
|
20
|
-
* at 0/0.
|
|
21
|
-
|
|
20
|
+
* at 0/0. `union-alpha` (the lane's Messages-wire id) and `space-bunny-free`
|
|
21
|
+
* retired from the live listing in the 2026-10-31 churn and are no longer
|
|
22
|
+
* advertised — they stay classified here so a listing that revives them is
|
|
23
|
+
* still served, and so the wire tests keep an honest Messages vehicle. */
|
|
24
|
+
const ALWAYS_FREE = new Set(['big-pickle', 'union-alpha', 'space-bunny-free'])
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Local capability baseline. `contextWindow`/`maxOutput` are the provider's
|
|
@@ -126,12 +129,36 @@ const FREE_ID_OVERRIDES = {
|
|
|
126
129
|
effortPatch: { reasoning_effort: '$effort' },
|
|
127
130
|
effortOffPatch: { reasoning_effort: 'none' },
|
|
128
131
|
},
|
|
129
|
-
// models.dev:
|
|
130
|
-
'
|
|
131
|
-
|
|
132
|
+
// models.dev (2026-11-06 roster): the live free set as the Oct-31 churn left
|
|
133
|
+
// it. Numbers are models.dev's published limits; region-gated ids (beam,
|
|
134
|
+
// crouton, pulse) are marked in REGION_SENSITIVE, not here.
|
|
135
|
+
'beam-free': {
|
|
136
|
+
vision: false, contextWindow: 1048576, maxOutput: 131072,
|
|
137
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
138
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
139
|
+
},
|
|
140
|
+
'flint-40b-free': {
|
|
141
|
+
vision: false, contextWindow: 262144, maxOutput: 65536,
|
|
132
142
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
133
143
|
effortPatch: { reasoning_effort: '$effort' },
|
|
134
144
|
},
|
|
145
|
+
'pulse-alpha-free': {
|
|
146
|
+
vision: true, contextWindow: 1048576, maxOutput: 131072,
|
|
147
|
+
efforts: ['low', 'high', 'max'], effortDefault: 'high',
|
|
148
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
149
|
+
},
|
|
150
|
+
// models.dev: 1M window, 270000 output, effort minimal..max (no `none` —
|
|
151
|
+
// thinking does not switch off in practice, so the menu starts at minimal).
|
|
152
|
+
'crouton-alpha-free': {
|
|
153
|
+
vision: true, contextWindow: 1048576, maxOutput: 262144,
|
|
154
|
+
efforts: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
155
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
156
|
+
},
|
|
157
|
+
// models.dev: 262144/262144, reasoning_options empty → no level menu, so the
|
|
158
|
+
// free lane's token ladder applies.
|
|
159
|
+
'nemotron-3.5-lightning-free': {
|
|
160
|
+
vision: false, contextWindow: 262144, maxOutput: 262144,
|
|
161
|
+
},
|
|
135
162
|
// models.dev: 1M window, 131072 output, reasoning_options `toggle` only →
|
|
136
163
|
// no level menu, so the free lane's token ladder applies.
|
|
137
164
|
'longcat-2.5-preview-free': {
|
|
@@ -145,22 +172,19 @@ const FREE_ID_OVERRIDES = {
|
|
|
145
172
|
/** Human-facing display names, so a raw upstream id never reaches the picker. */
|
|
146
173
|
const DISPLAY_NAMES = {
|
|
147
174
|
'mimo-v2.6-flash-free': 'MiMo V2.6 Flash',
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
175
|
+
'beam-free': 'Beam Free',
|
|
176
|
+
'flint-40b-free': 'Flint 40B Free',
|
|
177
|
+
'pulse-alpha-free': 'Pulse Alpha Free',
|
|
178
|
+
'crouton-alpha-free': 'Crouton Alpha Free',
|
|
152
179
|
'nemotron-3.5-lightning-free': 'Nemotron 3.5 Lightning',
|
|
153
|
-
'ling-3.0-flash-fin-free': 'Ling 3.0 Flash Fin',
|
|
154
|
-
'space-bunny-free': 'Space Bunny',
|
|
155
|
-
'union-alpha': 'Union Alpha',
|
|
156
|
-
'deepseek-v4-flash-free': 'DeepSeek V4 Flash',
|
|
157
180
|
'deepseek-v4.1-flash-free': 'DeepSeek V4.1 Flash',
|
|
158
181
|
'glm-5.3-flash-free': 'GLM 5.3 Flash',
|
|
159
|
-
'jev-1.13-free': 'Jev 1.13',
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
/** Ids whose regional availability is known to be egress-dependent.
|
|
163
|
-
|
|
184
|
+
/** Ids whose regional availability is known to be egress-dependent. The 2.2
|
|
185
|
+
* docs name Beam Free, Crouton Alpha Free and Pulse Alpha Free as region-gated;
|
|
186
|
+
* `muse-spark` kept its marker from the era it served. */
|
|
187
|
+
const REGION_SENSITIVE = [/^muse.?spark/, /^beam/, /crouton/, /pulse-alpha/]
|
|
164
188
|
|
|
165
189
|
/**
|
|
166
190
|
* Is this id on the免密 lane? The gateway's listing mixes paid and free ids;
|
|
@@ -666,3 +690,103 @@ export function reviveKiloCatalog(stored) {
|
|
|
666
690
|
export function isKiloEntry(entry) {
|
|
667
691
|
return entry?.channel === KILO_CHANNEL
|
|
668
692
|
}
|
|
693
|
+
|
|
694
|
+
// ── the extension lane ─────────────────────────────────────────────────────────
|
|
695
|
+
|
|
696
|
+
/** Catalog tag for models an installed extension serves. */
|
|
697
|
+
export const EXTENSION_CHANNEL = 'extension'
|
|
698
|
+
|
|
699
|
+
/** The built-in lanes an extension may take over (`replaces`). Kept in step
|
|
700
|
+
* with the marketplace's policy and with the runtime's takeover: only `kilo`
|
|
701
|
+
* is actually stood down (`laneStandsDown` in src/core/runtime.js), and the
|
|
702
|
+
* marketplace validator refuses any other claim — listing a lane here that
|
|
703
|
+
* nothing implements would hand an extension the built-in ids and then lose
|
|
704
|
+
* its rows to the built-in they collide with. */
|
|
705
|
+
const REPLACEABLE_LANES = new Set(['kilo'])
|
|
706
|
+
|
|
707
|
+
/** Wire spellings an extension channel may speak. */
|
|
708
|
+
const EXTENSION_WIRES = new Set(['chat', 'responses', 'messages'])
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The catalog id one extension model answers to. Extension models are
|
|
712
|
+
* namespaced (`ext:<plugin>/<model>`) so a third-party roster can never shadow
|
|
713
|
+
* a built-in id by accident — unless the extension `replaces` that lane, in
|
|
714
|
+
* which case its models keep the lane's own ids and the built-in rows stand
|
|
715
|
+
* down (the kilo-free official extension demonstrates exactly this).
|
|
716
|
+
*/
|
|
717
|
+
export function extensionModelId(manifestId, modelId, replaces) {
|
|
718
|
+
const clean = String(modelId ?? '').trim()
|
|
719
|
+
if (replaces !== undefined && REPLACEABLE_LANES.has(replaces)) return clean
|
|
720
|
+
return `ext:${String(manifestId ?? '').trim()}/${clean}`
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Is this catalog row served by an installed extension? */
|
|
724
|
+
export function isExtensionEntry(entry) {
|
|
725
|
+
return entry?.channel === EXTENSION_CHANNEL
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Normalize one `listModels`/manifest model row into a catalog entry, or null. */
|
|
729
|
+
function extensionEntryOf(manifest, row) {
|
|
730
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return null
|
|
731
|
+
const modelId = typeof row.id === 'string' ? row.id.trim() : ''
|
|
732
|
+
if (modelId === '' || modelId.length > 128) return null
|
|
733
|
+
const wire = EXTENSION_WIRES.has(row.wire) ? row.wire : 'chat'
|
|
734
|
+
const contextWindow = number(row.contextWindow) ?? 131072
|
|
735
|
+
const entry = {
|
|
736
|
+
id: extensionModelId(manifest.id, modelId, manifest.replaces),
|
|
737
|
+
name: typeof row.name === 'string' && row.name.trim() !== '' ? row.name.trim() : displayModelName(modelId),
|
|
738
|
+
channel: EXTENSION_CHANNEL,
|
|
739
|
+
extensionId: manifest.id,
|
|
740
|
+
extensionName: typeof manifest.name === 'string' ? manifest.name : manifest.id,
|
|
741
|
+
modelId,
|
|
742
|
+
wire,
|
|
743
|
+
vision: row.vision === true,
|
|
744
|
+
reasoning: row.reasoning !== false,
|
|
745
|
+
contextWindow,
|
|
746
|
+
maxOutput: number(row.maxOutput) ?? Math.min(32768, contextWindow),
|
|
747
|
+
canDisableThinking: true,
|
|
748
|
+
regionSensitive: false,
|
|
749
|
+
source: 'manifest',
|
|
750
|
+
}
|
|
751
|
+
if (Array.isArray(row.efforts) && row.efforts.length > 0) {
|
|
752
|
+
const efforts = row.efforts.filter(level => typeof level === 'string' && level !== '').slice(0, 8)
|
|
753
|
+
if (efforts.length > 0) {
|
|
754
|
+
entry.efforts = efforts
|
|
755
|
+
entry.effortDefault = efforts.includes(row.effortDefault) ? row.effortDefault : efforts[efforts.length - 1]
|
|
756
|
+
// The declared level rides the wire as the model's own effort field — the
|
|
757
|
+
// extension names its own spelling via `effortKey` when it is not the
|
|
758
|
+
// common `reasoning_effort`/`reasoning.effort` shapes.
|
|
759
|
+
const key = typeof row.effortKey === 'string' && row.effortKey.trim() !== '' ? row.effortKey.trim() : null
|
|
760
|
+
entry.effortPatch = key !== null ? { [key]: '$effort' } : { reasoning_effort: '$effort' }
|
|
761
|
+
entry.canDisableThinking = efforts.includes('disabled') || efforts.includes('none')
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return entry
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Build the extension lane's catalog rows from one installed channel
|
|
769
|
+
* extension's model list. Rows that repeat an id are dropped.
|
|
770
|
+
*/
|
|
771
|
+
export function buildExtensionCatalog(manifest, rows) {
|
|
772
|
+
if (!manifest || typeof manifest !== 'object' || typeof manifest.id !== 'string') return []
|
|
773
|
+
const seen = new Set()
|
|
774
|
+
const entries = []
|
|
775
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
776
|
+
const entry = extensionEntryOf(manifest, row)
|
|
777
|
+
if (entry === null || seen.has(entry.id)) continue
|
|
778
|
+
seen.add(entry.id)
|
|
779
|
+
entries.push(entry)
|
|
780
|
+
}
|
|
781
|
+
return entries
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Which built-in lane an enabled extension takes over, or null. Only one
|
|
786
|
+
* extension may replace a lane; the first enabled claim wins and the rest are
|
|
787
|
+
* ignored until the conflict is resolved.
|
|
788
|
+
*/
|
|
789
|
+
export function extensionReplaces(manifest) {
|
|
790
|
+
const replaces = typeof manifest?.replaces === 'string' ? manifest.replaces : ''
|
|
791
|
+
return REPLACEABLE_LANES.has(replaces) ? replaces : null
|
|
792
|
+
}
|
package/src/core.js
CHANGED
|
@@ -199,15 +199,14 @@ import { fetchKiloListing } from "./kilo.js";
|
|
|
199
199
|
import { sessionForConversation } from "./upstream.js";
|
|
200
200
|
import { isSubscriptionEntry, subscriptionCatalog } from "./subscription-adapter.js";
|
|
201
201
|
var FALLBACK_CATALOG = buildCatalog([
|
|
202
|
+
"deepseek-v4.1-flash-free",
|
|
203
|
+
"glm-5.3-flash-free",
|
|
202
204
|
"mimo-v2.6-flash-free",
|
|
203
|
-
"mimo-v2.5-free",
|
|
204
|
-
"ling-3.0-flash-fin-free",
|
|
205
|
-
"nemotron-3-ultra-free",
|
|
206
205
|
"nemotron-3.5-lightning-free",
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"muse-spark-1.2-contributor-free"
|
|
206
|
+
"nemotron-3.5-super-vl-free",
|
|
207
|
+
"flint-40b-free"
|
|
210
208
|
]);
|
|
209
|
+
var LANE_QUOTA_DEFAULT_MS = 6 * 60 * 60 * 1e3;
|
|
211
210
|
function createRuntimeStores({ dataDir, logger = console }) {
|
|
212
211
|
if (typeof dataDir !== "string" || !path.isAbsolute(dataDir)) {
|
|
213
212
|
throw new TypeError("dataDir must be an absolute path");
|
|
@@ -252,7 +251,8 @@ function createModelRuntime({
|
|
|
252
251
|
},
|
|
253
252
|
onTopology = () => {
|
|
254
253
|
},
|
|
255
|
-
subscriptionRuntime
|
|
254
|
+
subscriptionRuntime,
|
|
255
|
+
extensionRuntime = null
|
|
256
256
|
}) {
|
|
257
257
|
let catalog = materializeCatalog(catalogStore.get().entries ?? []);
|
|
258
258
|
let sealedCatalog = sealedCredential() === null ? [] : buildEacCatalog(catalogStore.get().sealIds ?? []);
|
|
@@ -260,6 +260,7 @@ function createModelRuntime({
|
|
|
260
260
|
let aggregateCatalog = [];
|
|
261
261
|
let subscriptionEntries = [];
|
|
262
262
|
let comboCatalog = [];
|
|
263
|
+
let extensionCatalog = [];
|
|
263
264
|
let freeCatalog = catalog;
|
|
264
265
|
let egress = availability.get().egress ?? null;
|
|
265
266
|
let gatewayLatency = { ms: 0, at: 0 };
|
|
@@ -267,10 +268,13 @@ function createModelRuntime({
|
|
|
267
268
|
const lifetime = new AbortController();
|
|
268
269
|
let egressGeneration = 0;
|
|
269
270
|
const rebuildCatalog = () => {
|
|
271
|
+
const replacedLane = extensionRuntime?.replacesLane ?? null;
|
|
272
|
+
const laneStandsDown = (entry) => replacedLane === "kilo" && isKiloEntry(entry);
|
|
270
273
|
const seen = /* @__PURE__ */ new Set();
|
|
271
274
|
const merged = [];
|
|
272
|
-
for (const entry of [...freeCatalog, ...sealedCatalog, ...kiloCatalog, ...aggregateCatalog, ...subscriptionEntries]) {
|
|
275
|
+
for (const entry of [...freeCatalog, ...sealedCatalog, ...kiloCatalog, ...aggregateCatalog, ...extensionCatalog, ...subscriptionEntries]) {
|
|
273
276
|
if (entry === null || entry === void 0 || seen.has(entry.id)) continue;
|
|
277
|
+
if (laneStandsDown(entry)) continue;
|
|
274
278
|
seen.add(entry.id);
|
|
275
279
|
merged.push(entry);
|
|
276
280
|
}
|
|
@@ -287,6 +291,7 @@ function createModelRuntime({
|
|
|
287
291
|
const mergeCatalogs = () => {
|
|
288
292
|
aggregateCatalog = buildAggregateCatalog(settings.get().aggregate ?? {});
|
|
289
293
|
subscriptionEntries = subscriptionCatalog(subscriptionRuntime);
|
|
294
|
+
extensionCatalog = extensionRuntime === null ? [] : extensionRuntime.catalog ?? [];
|
|
290
295
|
rebuildCatalog();
|
|
291
296
|
};
|
|
292
297
|
mergeCatalogs();
|
|
@@ -295,6 +300,7 @@ function createModelRuntime({
|
|
|
295
300
|
membership: computeMembership(catalog, availability.get(), settings.get()),
|
|
296
301
|
settings: settings.get(),
|
|
297
302
|
attributionUserAgent,
|
|
303
|
+
lane: laneRow(),
|
|
298
304
|
routing: {
|
|
299
305
|
quota: subscriptionRuntime?.routingQuota?.() ?? {},
|
|
300
306
|
usage: Object.fromEntries(catalog.map((row) => [row.id, stats.get().models?.[row.id]?.calls ?? 0]))
|
|
@@ -303,11 +309,71 @@ function createModelRuntime({
|
|
|
303
309
|
const emitTopology = () => {
|
|
304
310
|
if (!disposed) onTopology();
|
|
305
311
|
};
|
|
312
|
+
function laneRow() {
|
|
313
|
+
return availability.get().lane ?? null;
|
|
314
|
+
}
|
|
315
|
+
function setLaneState(laneState, extra = {}) {
|
|
316
|
+
availability.edit((value) => ({ ...value, lane: { state: laneState, at: Date.now(), ...extra } }));
|
|
317
|
+
availability.flush();
|
|
318
|
+
emitTopology();
|
|
319
|
+
}
|
|
320
|
+
function laneGate() {
|
|
321
|
+
const lane = laneRow();
|
|
322
|
+
if (!lane || lane.state === "ok" || lane.state === void 0) return { ok: true };
|
|
323
|
+
if (lane.state === "quota") {
|
|
324
|
+
if (!Number.isFinite(lane.retryAt) || lane.retryAt <= Date.now()) return { ok: true };
|
|
325
|
+
return { ok: false, state: "quota", retryAt: lane.retryAt, message: lane.message ?? "" };
|
|
326
|
+
}
|
|
327
|
+
if (lane.state === "locked") {
|
|
328
|
+
return { ok: false, state: "locked", message: lane.message ?? "" };
|
|
329
|
+
}
|
|
330
|
+
return { ok: true };
|
|
331
|
+
}
|
|
332
|
+
function clearLaneBreaker() {
|
|
333
|
+
const lane = laneRow();
|
|
334
|
+
if (!lane || lane.state === "ok") return false;
|
|
335
|
+
setLaneState("ok");
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
function recordPassiveResult(model, resultState, extra = {}) {
|
|
339
|
+
availability.edit((value) => ({
|
|
340
|
+
...value,
|
|
341
|
+
results: {
|
|
342
|
+
...value.results,
|
|
343
|
+
[model]: { ...value.results?.[model] ?? {}, state: resultState, source: "passive", at: Date.now(), ...extra }
|
|
344
|
+
}
|
|
345
|
+
}));
|
|
346
|
+
availability.flush();
|
|
347
|
+
}
|
|
348
|
+
function noteFreeLaneOutcome({ entry, ok, error } = {}) {
|
|
349
|
+
if (disposed || !entry || entry.channel) return;
|
|
350
|
+
if (ok === true) {
|
|
351
|
+
const lane = laneRow();
|
|
352
|
+
if (lane && lane.state !== "ok") setLaneState("ok");
|
|
353
|
+
recordPassiveResult(entry.id, STATE.available);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const code = error?.code;
|
|
357
|
+
if (code === CODE.quota) {
|
|
358
|
+
const retryAfter = Number(error?.providerRetryAfterMs);
|
|
359
|
+
const retryAt = Number.isFinite(retryAfter) && retryAfter > 0 ? Date.now() + retryAfter : Date.now() + LANE_QUOTA_DEFAULT_MS;
|
|
360
|
+
setLaneState("quota", { retryAt, message: String(error?.message ?? "free usage limit reached").slice(0, 300) });
|
|
361
|
+
recordPassiveResult(entry.id, STATE.throttled);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (code === CODE.laneLocked) {
|
|
365
|
+
setLaneState("locked", { message: String(error?.message ?? "the free tier answers this client only").slice(0, 300) });
|
|
366
|
+
recordPassiveResult(entry.id, STATE.unavailable);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (code === CODE.region) recordPassiveResult(entry.id, STATE.regionBlocked);
|
|
370
|
+
}
|
|
306
371
|
const adapter = new FreeModelAdapter({
|
|
307
372
|
state,
|
|
308
373
|
resolveImage,
|
|
309
374
|
subscriptionRuntime,
|
|
310
375
|
sealedCredential,
|
|
376
|
+
extensionRuntime,
|
|
311
377
|
recordUsage: (record) => {
|
|
312
378
|
if (disposed) return;
|
|
313
379
|
recordUsage(stats, record);
|
|
@@ -316,7 +382,12 @@ function createModelRuntime({
|
|
|
316
382
|
recordTurn: (record) => {
|
|
317
383
|
if (!disposed) recordTurn(stats, record);
|
|
318
384
|
},
|
|
319
|
-
warn: (message) => logger.warn?.(message) ?? logger.log?.(message)
|
|
385
|
+
warn: (message) => logger.warn?.(message) ?? logger.log?.(message),
|
|
386
|
+
freeLaneGate: laneGate,
|
|
387
|
+
onFreeLaneOutcome: noteFreeLaneOutcome,
|
|
388
|
+
onRegionBlocked: (modelId) => {
|
|
389
|
+
if (!disposed) recordPassiveResult(modelId, STATE.regionBlocked);
|
|
390
|
+
}
|
|
320
391
|
// Automatic probing is disabled by owner policy: a region refusal from a
|
|
321
392
|
// real turn stays a turn failure and never fires a background probe.
|
|
322
393
|
});
|
|
@@ -324,7 +395,7 @@ function createModelRuntime({
|
|
|
324
395
|
let catalogRefreshForced = false;
|
|
325
396
|
let catalogRefreshProbed = false;
|
|
326
397
|
async function refreshCatalog(opts) {
|
|
327
|
-
const { probe = false, force = false, report = false } = opts ?? {};
|
|
398
|
+
const { probe = false, force = false, report = false, throttleListing = false } = opts ?? {};
|
|
328
399
|
const result = (value) => report ? value : value.catalog;
|
|
329
400
|
if (disposed) return result({ catalog, sources: {} });
|
|
330
401
|
while (catalogRefresh !== null) {
|
|
@@ -334,7 +405,7 @@ function createModelRuntime({
|
|
|
334
405
|
const value = await shared;
|
|
335
406
|
if (disposed || (!force || sharedForced) && (!probe || sharedProbed)) return result(value);
|
|
336
407
|
}
|
|
337
|
-
const run = refreshCatalogOnce({ probe, force });
|
|
408
|
+
const run = refreshCatalogOnce({ probe, force, throttleListing });
|
|
338
409
|
catalogRefresh = run;
|
|
339
410
|
catalogRefreshForced = force;
|
|
340
411
|
catalogRefreshProbed = probe;
|
|
@@ -348,13 +419,21 @@ function createModelRuntime({
|
|
|
348
419
|
}
|
|
349
420
|
}
|
|
350
421
|
}
|
|
351
|
-
|
|
422
|
+
const LISTING_MIN_INTERVAL_MS = 10 * 6e4;
|
|
423
|
+
let lastListingAt = 0;
|
|
424
|
+
async function refreshCatalogOnce({ probe, force, throttleListing }) {
|
|
352
425
|
const sources = {};
|
|
353
426
|
let ids = [];
|
|
354
|
-
|
|
355
|
-
ids =
|
|
356
|
-
|
|
357
|
-
|
|
427
|
+
if (throttleListing === true && !force && Date.now() - lastListingAt < LISTING_MIN_INTERVAL_MS && freeCatalog.length > 0) {
|
|
428
|
+
ids = freeCatalog.map((entry) => entry.id);
|
|
429
|
+
sources.free = { ok: true, count: freeCatalog.length, skipped: "recent" };
|
|
430
|
+
} else {
|
|
431
|
+
try {
|
|
432
|
+
ids = parseListing(await fetchListing());
|
|
433
|
+
lastListingAt = Date.now();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!disposed) logger.warn?.(`our-free-model: model listing refresh failed (${error?.message ?? error}); keeping the cached catalog`);
|
|
436
|
+
}
|
|
358
437
|
}
|
|
359
438
|
if (disposed) return { catalog, sources };
|
|
360
439
|
if (ids.length > 0) {
|
|
@@ -365,7 +444,7 @@ function createModelRuntime({
|
|
|
365
444
|
} else {
|
|
366
445
|
freeCatalog = materializeCatalog(catalogStore.get().entries ?? []);
|
|
367
446
|
}
|
|
368
|
-
sources.free = { ok: ids.length > 0, count: freeCatalog.length };
|
|
447
|
+
if (sources.free === void 0) sources.free = { ok: ids.length > 0, count: freeCatalog.length };
|
|
369
448
|
sources.sealed = await refreshSealedRoster();
|
|
370
449
|
if (disposed) return { catalog, sources };
|
|
371
450
|
sources.kilo = { ok: await refreshKiloRoster(), count: kiloCatalog.length };
|
|
@@ -436,12 +515,13 @@ function createModelRuntime({
|
|
|
436
515
|
}
|
|
437
516
|
let probeRound = null;
|
|
438
517
|
let probeRoundGeneration = 0;
|
|
439
|
-
let probeThrottleStreak = 0;
|
|
440
518
|
let probeBackoffUntil = 0;
|
|
441
|
-
async function runProbeRound() {
|
|
519
|
+
async function runProbeRound(onlyModel) {
|
|
442
520
|
const generation = egressGeneration;
|
|
443
|
-
const probeable = catalog.filter((entry) => !
|
|
444
|
-
const
|
|
521
|
+
const probeable = catalog.filter((entry) => !entry.channel && !isComboEntry(entry));
|
|
522
|
+
const targets = typeof onlyModel === "string" && onlyModel !== "" ? probeable.filter((entry) => entry.id === onlyModel) : probeable.filter((entry) => entry.pinned === true).slice(0, 1).length > 0 ? probeable.filter((entry) => entry.pinned === true).slice(0, 1) : probeable.slice(0, 1);
|
|
523
|
+
if (targets.length === 0) return {};
|
|
524
|
+
const results = await probeCatalog(targets, { attributionUserAgent, signal: lifetime.signal }, (id, result) => {
|
|
445
525
|
if (disposed || generation !== egressGeneration) return;
|
|
446
526
|
availability.edit((value) => ({
|
|
447
527
|
...value,
|
|
@@ -449,6 +529,7 @@ function createModelRuntime({
|
|
|
449
529
|
...value.results,
|
|
450
530
|
[id]: {
|
|
451
531
|
state: result.state,
|
|
532
|
+
source: "manual",
|
|
452
533
|
...result.detail === void 0 ? {} : { detail: result.detail },
|
|
453
534
|
...result.ttftMs === void 0 ? {} : { ttftMs: result.ttftMs },
|
|
454
535
|
latencyMs: result.latencyMs,
|
|
@@ -457,28 +538,29 @@ function createModelRuntime({
|
|
|
457
538
|
}
|
|
458
539
|
}
|
|
459
540
|
}));
|
|
460
|
-
},
|
|
541
|
+
}, 1);
|
|
461
542
|
if (disposed || generation !== egressGeneration) return {};
|
|
462
543
|
availability.update({ at: Date.now(), egress });
|
|
463
544
|
availability.flush();
|
|
464
545
|
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
546
|
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
547
|
if (allThrottled) {
|
|
472
|
-
|
|
548
|
+
probeBackoffUntil = Date.now() + 30 * 6e4;
|
|
549
|
+
logger.warn?.("our-free-model: the manual probe hit the lane quota; the breaker now carries the window");
|
|
550
|
+
} else {
|
|
551
|
+
probeBackoffUntil = 0;
|
|
473
552
|
}
|
|
474
553
|
emitTopology();
|
|
475
554
|
return results;
|
|
476
555
|
}
|
|
477
|
-
async function refreshAvailability(force = false) {
|
|
478
|
-
if (disposed
|
|
556
|
+
async function refreshAvailability(force = false, { model } = {}) {
|
|
557
|
+
if (disposed) return {};
|
|
558
|
+
if (!force && probeBackoffUntil > Date.now()) return {};
|
|
559
|
+
const gate = laneGate();
|
|
560
|
+
if (!gate.ok) return { skipped: gate.state, retryAt: gate.retryAt, message: gate.message };
|
|
479
561
|
if (probeRound !== null && probeRoundGeneration === egressGeneration) return probeRound;
|
|
480
562
|
probeRoundGeneration = egressGeneration;
|
|
481
|
-
const round = runProbeRound();
|
|
563
|
+
const round = runProbeRound(model);
|
|
482
564
|
probeRound = round;
|
|
483
565
|
try {
|
|
484
566
|
return await round;
|
|
@@ -501,10 +583,10 @@ function createModelRuntime({
|
|
|
501
583
|
egress: seen,
|
|
502
584
|
at: 0,
|
|
503
585
|
results: expire(value.results),
|
|
504
|
-
manualResults: expire(value.manualResults)
|
|
586
|
+
manualResults: expire(value.manualResults),
|
|
587
|
+
lane: { state: "ok", at: Date.now() }
|
|
505
588
|
}));
|
|
506
589
|
availability.flush();
|
|
507
|
-
probeThrottleStreak = 0;
|
|
508
590
|
probeBackoffUntil = 0;
|
|
509
591
|
emitTopology();
|
|
510
592
|
logger.info?.(`our-free-model: egress changed to ${seen.ip}${seen.country ? ` (${seen.country})` : ""}; previous availability marked stale`);
|
|
@@ -558,6 +640,8 @@ function createModelRuntime({
|
|
|
558
640
|
refreshAvailability,
|
|
559
641
|
watchEgress,
|
|
560
642
|
fetchListing,
|
|
643
|
+
laneGate,
|
|
644
|
+
clearLaneBreaker,
|
|
561
645
|
complete: createForwardCompletion({
|
|
562
646
|
adapter,
|
|
563
647
|
signal: lifetime.signal,
|
|
@@ -581,6 +665,11 @@ function createModelRuntime({
|
|
|
581
665
|
emitTopology();
|
|
582
666
|
return catalog;
|
|
583
667
|
},
|
|
668
|
+
// An extension install/enable/disable/uninstall lands here: the extension
|
|
669
|
+
// runtime has already rebuilt its own rows, so this is a merge + announce.
|
|
670
|
+
syncExtensionCatalog() {
|
|
671
|
+
return this.syncAggregateCatalog();
|
|
672
|
+
},
|
|
584
673
|
get catalog() {
|
|
585
674
|
return catalog;
|
|
586
675
|
},
|