@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.
Files changed (44) hide show
  1. package/README.md +23 -8
  2. package/client.js +894 -6
  3. package/cordis.patch.yml +1 -1
  4. package/index.js +248 -11
  5. package/package.json +6 -2
  6. package/src/adapter.js +54 -6
  7. package/src/aggregate.js +53 -13
  8. package/src/bun-fetch.js +33 -10
  9. package/src/catalog.js +238 -58
  10. package/src/core.js +838 -0
  11. package/src/egress.js +204 -3
  12. package/src/engine-parts/win32-x64.000.part +0 -0
  13. package/src/engine-parts/win32-x64.001.part +0 -0
  14. package/src/engine-parts/win32-x64.002.part +0 -0
  15. package/src/engine-parts/win32-x64.003.part +0 -0
  16. package/src/engine-parts/win32-x64.004.part +0 -0
  17. package/src/engine-parts/win32-x64.005.part +0 -0
  18. package/src/engine-parts/win32-x64.006.part +0 -0
  19. package/src/extensions.js +1888 -0
  20. package/src/feed.js +6 -0
  21. package/src/http.js +15 -2
  22. package/src/kilo.js +55 -5
  23. package/src/magpie-engine.js +61 -2
  24. package/src/magpie-models.js +31 -3
  25. package/src/model-test.js +3 -1
  26. package/src/o2a.js +956 -0
  27. package/src/open-external.js +43 -5
  28. package/src/plugins.js +1240 -0
  29. package/src/providers/registry.json +72 -30
  30. package/src/semver.js +40 -0
  31. package/src/store.js +17 -1
  32. package/src/stream.js +39 -4
  33. package/src/subscription-completion.js +12 -3
  34. package/src/updater.js +5 -33
  35. package/src/upstream.js +283 -363
  36. package/vendor/channel-pack/NOTICE.md +1 -1
  37. package/vendor/channel-pack/pack.js +7242 -3952
  38. package/vendor/magpie/NOTICE.md +159 -6
  39. package/vendor/magpie/engine.json +58 -27
  40. package/src/core/completion.js +0 -196
  41. package/src/core/runtime.js +0 -456
  42. package/src/core/stats.js +0 -98
  43. package/src/engine-parts/win32-x64.007.part +0 -0
  44. 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(resolved.chatUrl, {
579
+ response = await egressFetch(url, {
536
580
  method: 'POST',
537
581
  headers: {
538
- ...resolved.headers,
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: bodyText,
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 ${resolved.provider.name} failed: ${redact(error?.message ?? error, resolved.apiKey)}${transportCause(error)}`,
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, resolved.apiKey)
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, resolved.apiKey))
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, resolved.apiKey).slice(0, 200)}`, CODE.server, { status: response.status })
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, resolved.apiKey)
644
+ throw classifyUpstream(response.status, rawText, setRetry, secret)
605
645
  }
606
- for (const frame of synthesisedFrames(resolved.wire, payload, model)) onData(frame)
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 a private temporary directory and reused for the
11
- * process lifetime. It speaks one JSON line on stdin and returns one metadata
12
- * line plus raw response bytes on stdout; no request content is evaluated.
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
- /** Materialize the inlined worker once; never executes request content. */
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
- const dir = mkdtempSync(join(tmpdir(), 'ofm-bun-worker-'));
44
- const file = join(dir, 'worker.mjs');
45
- writeFileSync(file, WORKER_SOURCE, { mode: 0o600 });
46
- workerPath = file;
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
- const ALWAYS_FREE = new Set(['union-alpha', 'space-bunny-free', 'big-pickle'])
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: effort low|medium|high|xhigh|max; 1M window, 524288 output.
130
- 'space-bunny-free': {
131
- vision: true, contextWindow: 1048576, maxOutput: 524288,
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
- 'mimo-v2.5-free': 'MiMo V2.5',
149
- 'muse-spark-1.3-contributor-free': 'Muse Spark 1.3',
150
- 'muse-spark-1.2-contributor-free': 'Muse Spark 1.2',
151
- 'nemotron-3-ultra-free': 'Nemotron 3 Ultra',
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
- const REGION_SENSITIVE = [/^muse.?spark/]
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 Free mark.
277
- 'opencode/deepseek-v4.1-flash-free': 'deepseek-v4.1-flash-free 1017',
278
- 'opencode/glm-5.3-flash-free': 'GLM 5.3 Flash Free',
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: published specs cross-checked against this lane where
289
- * the relay allowed a probe (2026-10-03), and the thinking-level menu copied
290
- * from ZCode's built-in provider config (`config/provider/zcode-builtin.json`,
291
- * the `modelRules`/`modelApiRules`/`providerSiteRules` buckets) — the same
292
- * per-model declaration shape that product ships: a list of levels plus a
293
- * JSON merge patch the level maps onto the request body. `vision` records what
294
- * the relay actually accepted under a direct image-input probe — DeepSeek V4.1
295
- * Flash and GLM 5.3 Flash answered a 1×1 image with its colour; the Kimi
296
- * models carry native vision encoders per their model cards but the relay's
297
- * kimi routes were down during the probe, so their flag follows the published
298
- * spec. These numbers bound the client-side truncation estimates and the sent
299
- * max_tokens; the relay enforces its own ceilings on the wire.
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
- * `efforts`/`effortDefault`/`effortPatch` mirror ZCode's declaration for the
302
- * model families it names; `effortPatch` uses `$effort` as the selected
303
- * level's placeholder, and `effortOffPatch` (when present) replaces the patch
304
- * for the off level. Models ZCode does not declare (kimi-k2.6, gpt-oss-20b)
305
- * fall back to the family-standard `reasoning_effort` field.
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
- // The OpenCode free lane. models.dev declares reasoning_options
309
- // [{type:"effort", values:["low","high","max"]}] for both ids, and the upstream
310
- // answers on its OpenAI wire, so the selected level rides `reasoning_effort`
311
- // verbatim. `disabled` maps to the upstream's own off value ("none"), which
312
- // zeroes the reasoning tokens — a level that is enforced, not a hint.
313
- // Capacities are the underlying model's own, not the free listing's rounded
314
- // floor: both are the same model the canonical entries below describe, so the
315
- // numbers are copied from them (1M window; 384K / 131K output) and were
316
- // re-checked against the upstream, which accepts max_tokens up to 384000.
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: 384000,
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: { reasoning: { enabled: false } },
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
- label: id === 'opencode/deepseek-v4.1-flash-free' ? 'deepseek-v4.1-flash-free 1017'
419
- : id.includes('/') ? id.slice(id.indexOf('/') + 1) : id,
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
+ }