@ebony-vinyl/dsh-our-free-model 2.7.4 → 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 +23 -8
- package/client.js +894 -6
- package/cordis.patch.yml +1 -1
- package/index.js +248 -11
- package/package.json +6 -2
- package/src/adapter.js +54 -6
- package/src/aggregate.js +53 -13
- package/src/bun-fetch.js +33 -10
- package/src/catalog.js +238 -58
- package/src/core.js +838 -0
- 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 +15 -2
- package/src/kilo.js +55 -5
- package/src/magpie-engine.js +61 -2
- package/src/magpie-models.js +31 -3
- package/src/model-test.js +3 -1
- package/src/o2a.js +956 -0
- 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 +12 -3
- package/src/updater.js +5 -33
- package/src/upstream.js +283 -363
- package/vendor/channel-pack/NOTICE.md +1 -1
- package/vendor/channel-pack/pack.js +7242 -3952
- package/vendor/magpie/NOTICE.md +159 -6
- package/vendor/magpie/engine.json +58 -27
- package/src/core/completion.js +0 -196
- package/src/core/runtime.js +0 -456
- package/src/core/stats.js +0 -98
- package/src/engine-parts/win32-x64.007.part +0 -0
- package/src/engine-parts/win32-x64.008.part +0 -0
package/src/aggregate.js
CHANGED
|
@@ -530,33 +530,73 @@ export async function postAggregateStreamed({ config, providerId, modelId, body,
|
|
|
530
530
|
}
|
|
531
531
|
const bodyText = typeof outbound === 'string' ? outbound
|
|
532
532
|
: JSON.stringify({ ...outbound, ...(model === '' ? {} : { model }), stream: true })
|
|
533
|
+
return postCompatibleStreamed({
|
|
534
|
+
url: resolved.chatUrl,
|
|
535
|
+
headers: {
|
|
536
|
+
...resolved.headers,
|
|
537
|
+
// The provider's gateway either ignores this or uses it to keep this
|
|
538
|
+
// conversation's state separate from concurrent ones on the same key —
|
|
539
|
+
// the free lane's session dimension, without its fingerprint family.
|
|
540
|
+
...(text(session) === '' ? {} : { 'x-session-id': text(session) }),
|
|
541
|
+
},
|
|
542
|
+
body: bodyText,
|
|
543
|
+
model,
|
|
544
|
+
wire: resolved.wire,
|
|
545
|
+
signal,
|
|
546
|
+
onData,
|
|
547
|
+
timeoutMs,
|
|
548
|
+
secret: resolved.apiKey,
|
|
549
|
+
label: resolved.provider.name,
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* POST one streaming completion to a resolved endpoint and push raw SSE `data:`
|
|
555
|
+
* payloads to `onData`.
|
|
556
|
+
*
|
|
557
|
+
* The lane-neutral core `postAggregateStreamed` shares with the extension
|
|
558
|
+
* channels: the URL, headers, body and wire are already decided by the caller
|
|
559
|
+
* (the aggregate resolver, or an extension's `prepareChat`); this layer owns
|
|
560
|
+
* the transport discipline — body-shape sniffing, head replay, the idle
|
|
561
|
+
* deadline, the JSON-answer fold into the row's wire's frame sequence — and
|
|
562
|
+
* the credential scrubbing (`secret`) on every refusal message.
|
|
563
|
+
*
|
|
564
|
+
* @param {object} options
|
|
565
|
+
* @param {string} options.url - absolute endpoint
|
|
566
|
+
* @param {Record<string, string>} options.headers - complete header set
|
|
567
|
+
* @param {string} options.body - serialized request body (`stream: true` included)
|
|
568
|
+
* @param {string} [options.model] - the bare outbound model id, labelling synthesised frames
|
|
569
|
+
* @param {'chat'|'messages'|'responses'} [options.wire='chat']
|
|
570
|
+
* @param {AbortSignal} [options.signal]
|
|
571
|
+
* @param {(payload: string) => void} options.onData
|
|
572
|
+
* @param {number} [options.timeoutMs]
|
|
573
|
+
* @param {string} [options.secret] - the credential scrubbed from refusal text
|
|
574
|
+
* @param {string} [options.label] - the lane name in error messages
|
|
575
|
+
*/
|
|
576
|
+
export async function postCompatibleStreamed({ url, headers, body, model = '', wire = 'chat', signal, onData, timeoutMs = TURN_TIMEOUT_MS, secret = '', label = 'the provider' }) {
|
|
533
577
|
let response
|
|
534
578
|
try {
|
|
535
|
-
response = await egressFetch(
|
|
579
|
+
response = await egressFetch(url, {
|
|
536
580
|
method: 'POST',
|
|
537
581
|
headers: {
|
|
538
|
-
...
|
|
582
|
+
...headers,
|
|
539
583
|
accept: 'text/event-stream',
|
|
540
|
-
// The provider's gateway either ignores this or uses it to keep this
|
|
541
|
-
// conversation's state separate from concurrent ones on the same key —
|
|
542
|
-
// the free lane's session dimension, without its fingerprint family.
|
|
543
|
-
...(text(session) === '' ? {} : { 'x-session-id': text(session) }),
|
|
544
584
|
},
|
|
545
|
-
body
|
|
585
|
+
body,
|
|
546
586
|
redirect: 'error',
|
|
547
587
|
signal,
|
|
548
588
|
})
|
|
549
589
|
} catch (error) {
|
|
550
590
|
if (signal?.aborted === true || error?.name === 'AbortError') throw new UpstreamError('request aborted', CODE.aborted)
|
|
551
591
|
throw new UpstreamError(
|
|
552
|
-
`model request to ${
|
|
592
|
+
`model request to ${label} failed: ${redact(error?.message ?? error, secret)}${transportCause(error)}`,
|
|
553
593
|
CODE.transport)
|
|
554
594
|
}
|
|
555
595
|
|
|
556
596
|
const setRetry = retryAfterMs(response)
|
|
557
597
|
if (!response.ok) {
|
|
558
598
|
const refusal = await response.text().catch(() => '')
|
|
559
|
-
throw classifyUpstream(response.status, refusal, setRetry,
|
|
599
|
+
throw classifyUpstream(response.status, refusal, setRetry, secret)
|
|
560
600
|
}
|
|
561
601
|
if (response.body === null) throw new UpstreamError('model stream returned no body', CODE.empty)
|
|
562
602
|
|
|
@@ -588,7 +628,7 @@ export async function postAggregateStreamed({ config, providerId, modelId, body,
|
|
|
588
628
|
rawText += head.decoder.decode()
|
|
589
629
|
let payload
|
|
590
630
|
try {
|
|
591
|
-
payload = JSON.parse(redact(rawText,
|
|
631
|
+
payload = JSON.parse(redact(rawText, secret))
|
|
592
632
|
} catch {
|
|
593
633
|
// A 200 body that is not JSON is a hop speaking for the provider. Markup is
|
|
594
634
|
// a front proxy (a WAF page, a CDN error), and replaying the identical body
|
|
@@ -598,12 +638,12 @@ export async function postAggregateStreamed({ config, providerId, modelId, body,
|
|
|
598
638
|
`the provider's front proxy answered HTTP ${response.status} with an HTML error page`,
|
|
599
639
|
CODE.client, { status: response.status })
|
|
600
640
|
}
|
|
601
|
-
throw new UpstreamError(`unexpected non-stream response: ${redact(rawText,
|
|
641
|
+
throw new UpstreamError(`unexpected non-stream response: ${redact(rawText, secret).slice(0, 200)}`, CODE.server, { status: response.status })
|
|
602
642
|
}
|
|
603
643
|
if (payload?.error !== undefined || payload?.type === 'error') {
|
|
604
|
-
throw classifyUpstream(response.status, rawText, setRetry,
|
|
644
|
+
throw classifyUpstream(response.status, rawText, setRetry, secret)
|
|
605
645
|
}
|
|
606
|
-
for (const frame of synthesisedFrames(
|
|
646
|
+
for (const frame of synthesisedFrames(wire, payload, model)) onData(frame)
|
|
607
647
|
return { status: response.status }
|
|
608
648
|
}
|
|
609
649
|
|
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,
|
|
132
137
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
133
138
|
effortPatch: { reasoning_effort: '$effort' },
|
|
134
139
|
},
|
|
140
|
+
'flint-40b-free': {
|
|
141
|
+
vision: false, contextWindow: 262144, maxOutput: 65536,
|
|
142
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
143
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
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;
|
|
@@ -273,66 +297,101 @@ export const EAC_TAG = 'EAC'
|
|
|
273
297
|
|
|
274
298
|
const EAC_DISPLAY_NAMES = {
|
|
275
299
|
// The OpenCode free lane is relayed server-side; the org prefix is routing
|
|
276
|
-
// detail, so the picker shows the model's own name with the lane's
|
|
277
|
-
'opencode/deepseek-v4.1-flash-free': '
|
|
278
|
-
'opencode/glm-5.3-flash-free': 'GLM 5.3 Flash
|
|
300
|
+
// detail, so the picker shows the model's own name with the lane's mark.
|
|
301
|
+
'opencode/deepseek-v4.1-flash-free': 'DeepSeek V4.1 Flash Highspeed',
|
|
302
|
+
'opencode/glm-5.3-flash-free': 'GLM 5.3 Flash Highspeed',
|
|
279
303
|
'deepseek-ai/deepseek-v4.1-flash': 'DeepSeek V4.1 Flash',
|
|
280
304
|
'moonshotai/kimi-k2.6': 'Kimi K2.6',
|
|
281
305
|
'moonshotai/kimi-k3': 'Kimi K3',
|
|
282
306
|
'openai/gpt-oss-20b': 'GPT-OSS 20B',
|
|
283
307
|
'z-ai/glm-5.3': 'GLM 5.3',
|
|
284
308
|
'z-ai/glm-5.3-flash': 'GLM 5.3 Flash',
|
|
309
|
+
'mistralai/mistral-large-4-1t-a52b': 'Mistral Large 4 1T A52B',
|
|
285
310
|
}
|
|
286
311
|
|
|
287
312
|
/**
|
|
288
|
-
* Capacities per model
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
313
|
+
* Capacities per model, read from NVIDIA's own model pages
|
|
314
|
+
* (`build.nvidia.com/<org>/<model>`, fetched 2026-10-31) — the platform that
|
|
315
|
+
* actually serves these ids through the co-paid lane's NVIDIA Build channel.
|
|
316
|
+
*
|
|
317
|
+
* Each page embeds its request schema and its `specifications` block; those are
|
|
318
|
+
* the figures below, not a guess:
|
|
319
|
+
*
|
|
320
|
+
* - `mistralai/mistral-large-4-1t-a52b`: `contextLength` 262144, `parameters`
|
|
321
|
+
* 1050B, `inputModalities` Text+Image, `reasoning: true`. Its schema declares
|
|
322
|
+
* `reasoning_effort` as `enum: ["none","high"]`, default `"high"`, and states
|
|
323
|
+
* that an omitted field is served as `none` — so its menu is exactly Off and
|
|
324
|
+
* High, with High as the default. The wire accepted `medium` too, but a value
|
|
325
|
+
* the published schema does not list is not offered.
|
|
326
|
+
* - `deepseek-ai/deepseek-v4.1-flash`: `contextLength` 1048576, `parameters`
|
|
327
|
+
* 552B, `inputModalities` Text+Image, `reasoning: true`.
|
|
328
|
+
* - `z-ai/glm-5.3-flash`: served on the same NVIDIA Build platform; its page
|
|
329
|
+
* carries no embedded spec block (a client-rendered page), so its window and
|
|
330
|
+
* output follow the same NVIDIA Build schema family as the two above, and its
|
|
331
|
+
* menu is the shared `reasoning_effort` enum.
|
|
300
332
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
333
|
+
* NVIDIA Build's chat schema validates `reasoning_effort` against
|
|
334
|
+
* `none|minimal|low|medium|high|xhigh|max` — a value outside that set answers
|
|
335
|
+
* 400 naming the accepted list. A model's menu here is the intersection of that
|
|
336
|
+
* enum with what the model's own page declares, so every rung is one the
|
|
337
|
+
* platform accepts; a model whose page declares only `none|high` gets exactly
|
|
338
|
+
* those two rather than a generic ladder.
|
|
339
|
+
*
|
|
340
|
+
* `vision` follows `inputModalities` on each page. `maxOutput` is not published
|
|
341
|
+
* per model there, so it is the platform's chat ceiling for these recipes, and
|
|
342
|
+
* the relay still enforces its own limit on the wire.
|
|
306
343
|
*/
|
|
307
344
|
const EAC_CAPABILITIES = [
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
345
|
+
// NVIDIA Build — the ids the co-paid lane relays from integrate.api.nvidia.com.
|
|
346
|
+
{
|
|
347
|
+
match: /^mistralai\/mistral-large-4-1t-a52b/,
|
|
348
|
+
vision: true, reasoning: true, contextWindow: 262144, maxOutput: 32768,
|
|
349
|
+
efforts: ['disabled', 'high'], effortDefault: 'high',
|
|
350
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
351
|
+
effortOffPatch: { reasoning_effort: 'none' },
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
match: /^deepseek-ai\/deepseek-v4\.1-flash/,
|
|
355
|
+
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 32768,
|
|
356
|
+
efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
357
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
358
|
+
effortOffPatch: { reasoning_effort: 'none' },
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
match: /^z-ai\/glm-5\.3-flash/,
|
|
362
|
+
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 32768,
|
|
363
|
+
efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
364
|
+
effortPatch: { reasoning_effort: '$effort' },
|
|
365
|
+
effortOffPatch: { reasoning_effort: 'none' },
|
|
366
|
+
},
|
|
367
|
+
// The OpenCode lane's two ids, relayed through the highspeed NVIDIA Build
|
|
368
|
+
// channel. The model is the same one the NVIDIA entries above describe, so
|
|
369
|
+
// its window and its menu are theirs: the shared `reasoning_effort` enum
|
|
370
|
+
// NVIDIA Build validates, with `none` as the off rung. `maxOutput` stays the
|
|
371
|
+
// free relay's own re-verified ceiling (384000 / 131072) — the relay enforces
|
|
372
|
+
// its own limit on the wire, which is tighter than NVIDIA's published one.
|
|
373
|
+
// The upstream id keeps its `-free` suffix (routing detail); the card's label
|
|
374
|
+
// says highspeed.
|
|
317
375
|
{
|
|
318
376
|
match: /^opencode\/deepseek-v4/,
|
|
319
377
|
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 384000,
|
|
320
|
-
efforts: ['disabled', 'low', 'high', 'max'], effortDefault: 'high',
|
|
378
|
+
efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
321
379
|
effortPatch: { reasoning_effort: '$effort' },
|
|
322
380
|
effortOffPatch: { reasoning_effort: 'none' },
|
|
323
381
|
},
|
|
324
382
|
{
|
|
325
383
|
match: /^opencode\/glm-5\.3-flash/,
|
|
326
384
|
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 131072,
|
|
327
|
-
efforts: ['low', 'high', 'max'], effortDefault: 'high',
|
|
385
|
+
efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
328
386
|
effortPatch: { reasoning_effort: '$effort' },
|
|
387
|
+
effortOffPatch: { reasoning_effort: 'none' },
|
|
329
388
|
},
|
|
330
389
|
{
|
|
331
390
|
match: /^deepseek-ai\/deepseek-v4/,
|
|
332
|
-
vision: true, reasoning: true, contextWindow: 1048576, maxOutput:
|
|
333
|
-
efforts: ['disabled', 'low', 'high', 'max'], effortDefault: 'high',
|
|
391
|
+
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 32768,
|
|
392
|
+
efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
|
|
334
393
|
effortPatch: { reasoning_effort: '$effort' },
|
|
335
|
-
effortOffPatch: {
|
|
394
|
+
effortOffPatch: { reasoning_effort: 'none' },
|
|
336
395
|
},
|
|
337
396
|
{
|
|
338
397
|
match: /^moonshotai\/kimi-k3/,
|
|
@@ -346,12 +405,6 @@ const EAC_CAPABILITIES = [
|
|
|
346
405
|
efforts: ['low', 'high', 'max'], effortDefault: 'high',
|
|
347
406
|
effortPatch: { reasoning_effort: '$effort' },
|
|
348
407
|
},
|
|
349
|
-
{
|
|
350
|
-
match: /^z-ai\/glm-5\.3-flash/,
|
|
351
|
-
vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 131072,
|
|
352
|
-
efforts: ['low', 'high', 'max'], effortDefault: 'high',
|
|
353
|
-
effortPatch: { thinking: { type: 'enabled' }, output_config: { effort: '$effort' } },
|
|
354
|
-
},
|
|
355
408
|
{
|
|
356
409
|
match: /^z-ai\/glm-5/,
|
|
357
410
|
vision: false, reasoning: true, contextWindow: 1048576, maxOutput: 131072,
|
|
@@ -375,6 +428,30 @@ const EAC_FEATURED = new Set([
|
|
|
375
428
|
'opencode/glm-5.3-flash-free',
|
|
376
429
|
])
|
|
377
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Display-only ids for the card's small line.
|
|
433
|
+
*
|
|
434
|
+
* The OpenCode lane serves its ids with a `-free` suffix that is upstream
|
|
435
|
+
* routing detail; this build relays those two models through the highspeed
|
|
436
|
+
* NVIDIA Build channel, so the line shows the id the channel actually names
|
|
437
|
+
* (`deepseek-v4.1-flash-free 1017` → `deepseek-v4.1-flash-highspeed 1017`).
|
|
438
|
+
* Only the label moves: the catalog `id` is what the adapter matches, and it
|
|
439
|
+
* keeps the upstream spelling.
|
|
440
|
+
*/
|
|
441
|
+
const EAC_LABELS = {
|
|
442
|
+
'opencode/deepseek-v4.1-flash-free': 'deepseek-v4.1-flash-highspeed 1017',
|
|
443
|
+
'opencode/glm-5.3-flash-free': 'glm-5.3-flash-highspeed',
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function eacLabel(modelId) {
|
|
447
|
+
const known = EAC_LABELS[modelId]
|
|
448
|
+
if (known !== undefined) return known
|
|
449
|
+
const bare = modelId.includes('/') ? modelId.slice(modelId.indexOf('/') + 1) : modelId
|
|
450
|
+
// Any remaining `-free` suffix is the same upstream detail; it is shown as
|
|
451
|
+
// the channel's highspeed mark rather than leaked into the card.
|
|
452
|
+
return bare.replace(/(?:^|[-_])free(?:$|[-_.])/, match => match.replace('free', 'highspeed'))
|
|
453
|
+
}
|
|
454
|
+
|
|
378
455
|
/** `org/model` → the model's own name under the channel tag. */
|
|
379
456
|
export function eacDisplayName(modelId) {
|
|
380
457
|
const base = String(modelId ?? '').trim()
|
|
@@ -415,8 +492,11 @@ export function buildEacCatalog(ids) {
|
|
|
415
492
|
// that is what keeps this row distinct from the free lane's same-named
|
|
416
493
|
// model during the merge — but the prefix is routing detail, so the line
|
|
417
494
|
// shows the model's own id, with a display-only version marker where needed.
|
|
418
|
-
|
|
419
|
-
|
|
495
|
+
// The OpenCode lane's two ids are shown as their highspeed ids: the lane
|
|
496
|
+
// relays them through the highspeed NVIDIA Build channel, and the raw
|
|
497
|
+
// upstream id keeps its `-free` suffix, which is routing detail the line
|
|
498
|
+
// must not show. The catalog id is untouched, so routing still matches.
|
|
499
|
+
label: eacLabel(id),
|
|
420
500
|
channel: EAC_CHANNEL,
|
|
421
501
|
wire: 'chat',
|
|
422
502
|
vision: caps.vision === true,
|
|
@@ -610,3 +690,103 @@ export function reviveKiloCatalog(stored) {
|
|
|
610
690
|
export function isKiloEntry(entry) {
|
|
611
691
|
return entry?.channel === KILO_CHANNEL
|
|
612
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
|
+
}
|