@ebony-vinyl/dsh-our-free-model 2.8.2 → 2.8.4

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/index.js CHANGED
@@ -43,7 +43,7 @@ import { clearEacUser, readEacUser, writeEacUser } from './src/eac-user.js'
43
43
  import { createEacLoginPoller } from './src/eac-login.js'
44
44
  import { unlockSealedLane } from './src/vault.js'
45
45
  import { DEFAULT_LEVEL, budgetLadder, hasDeclaredEffortMenu } from './src/effort.js'
46
- import { modelAvailability } from './src/model-test.js'
46
+ import { readPerf, readSeries, siteRootFor, modelAvailability } from './src/model-test.js'
47
47
  import { AnnouncementFeed } from './src/feed.js'
48
48
  import { createPickStats } from './src/picks.js'
49
49
  import { PluginUpdater, installationIsManaged } from './src/updater.js'
@@ -170,6 +170,19 @@ export function apply(ctx, config) {
170
170
  if (generation > 1) {
171
171
  settings.update({ reloadedAt: Date.now(), reloadCount: generation - 1 })
172
172
  settings.flush()
173
+ } else {
174
+ // Before issue #761 the only "already seen" marker lived in the browser's
175
+ // origin-scoped localStorage. A settings file from that build can carry an
176
+ // old reloadedAt with no acknowledgement beside it; adopting it here keeps
177
+ // a plain process restart (a new desktop port is a new origin) from
178
+ // replaying a stale notice. A genuinely new reload writes a new timestamp.
179
+ const stored = settings.get()
180
+ const at = Number(stored.reloadedAt ?? 0)
181
+ const seen = Number(stored.reloadNoticeSeen ?? 0)
182
+ if (Number.isSafeInteger(at) && at > 0 && seen === 0) {
183
+ settings.update({ reloadNoticeSeen: at })
184
+ settings.flush()
185
+ }
173
186
  }
174
187
 
175
188
  /** `managed` stands down everything that would rewrite the installed bytes. */
@@ -1152,6 +1165,10 @@ export function apply(ctx, config) {
1152
1165
  },
1153
1166
  subscriptions,
1154
1167
  onSubscriptionsChanged: syncAggregateCatalog,
1168
+ // The sealed credential names the lane's base (`…/eac/v1`); the perf route
1169
+ // derives the site root from it so the metrics endpoint is found without a
1170
+ // second hard-coded address.
1171
+ sealedCredential: sealedCredentialOf,
1155
1172
  // ── the aggregate lane's settings surface ────────────────────────────────
1156
1173
  // The registry is static data; the configured half is the settings file with
1157
1174
  // every key masked. Writing one provider rebuilds the catalog and re-emits
@@ -1404,6 +1421,7 @@ export function apply(ctx, config) {
1404
1421
  generation,
1405
1422
  reloadedAt: settings.get().reloadedAt ?? 0,
1406
1423
  reloadCount: settings.get().reloadCount ?? 0,
1424
+ reloadNoticeSeen: settings.get().reloadNoticeSeen ?? 0,
1407
1425
  dataDir,
1408
1426
  autoReloadWatch: settings.get().autoReloadWatch === true,
1409
1427
  lastReload: globalThis[Symbol.for('our-free-model.last-reload')] ?? undefined,
@@ -1816,6 +1834,29 @@ function imageResolver(ctx, logger) {
1816
1834
  /** The fixed failure classes the pool proxy may surface to the client. */
1817
1835
  const POOL_REASONS = new Set(['no-lane', 'gateway-status', 'malformed', 'unreachable'])
1818
1836
 
1837
+ // ── per-model performance figures ─────────────────────────────────────────────
1838
+ // The roster asks for these on every mount, and they are hourly samples: a
1839
+ // minute of staleness costs nothing, while an outbound hop per page load is
1840
+ // pure load on the site. The in-flight promise is shared as well, so a burst of
1841
+ // mounts costs one request rather than one each.
1842
+ const PERF_FRESH_MS = 60_000
1843
+ let perfCache = { at: 0, data: null }
1844
+ let perfPending = null
1845
+ function perfSnapshot(root, fetchImpl) {
1846
+ if (root === null || root === undefined) return Promise.resolve({ models: {} })
1847
+ if (perfCache.data !== null && Date.now() - perfCache.at < PERF_FRESH_MS) return Promise.resolve(perfCache.data)
1848
+ if (perfPending !== null) return perfPending
1849
+ perfPending = readPerf({ root, fetch: fetchImpl })
1850
+ .then(data => {
1851
+ // Only a non-empty answer is worth keeping: caching "the site did not
1852
+ // answer" would hide a recovery for a full minute.
1853
+ if (Object.keys(data.models).length > 0) perfCache = { at: Date.now(), data }
1854
+ return data
1855
+ })
1856
+ .finally(() => { perfPending = null })
1857
+ return perfPending
1858
+ }
1859
+
1819
1860
  export function createApiRoutes(deps) {
1820
1861
  return async function handler(req, res) {
1821
1862
  const url = new URL(req.url ?? '/', 'http://localhost')
@@ -1876,6 +1917,30 @@ export function createApiRoutes(deps) {
1876
1917
  if (method === 'GET' && routePath === '/stats') {
1877
1918
  return send(200, buildStats(deps.stats.get(), deps.catalog()))
1878
1919
  }
1920
+ // Per-model performance figures for the roster's cards. The site's own
1921
+ // metrics endpoint is public but sends no CORS headers, so the page
1922
+ // cannot call it directly — this is the server-side hop. One request
1923
+ // answers every model, and the map holds only models whose figure is
1924
+ // trustworthy (see src/model-test.js); an absent model draws no strip.
1925
+ //
1926
+ // Cached for a minute and de-duplicated while in flight: the roster asks
1927
+ // on every mount, and the figures are hourly samples — a fresh outbound
1928
+ // request per page load buys nothing but load on the site.
1929
+ if (method === 'GET' && routePath === '/perf') {
1930
+ const credential = deps.sealedCredential?.() ?? null
1931
+ const root = siteRootFor(credential?.base)
1932
+ return send(200, await perfSnapshot(root, deps.perfFetch ?? directFetch))
1933
+ }
1934
+ // One model's hourly trend series, for the charts behind 详情. Separate
1935
+ // from `/perf` because it is per model: the roster would otherwise pay
1936
+ // twelve requests to draw twelve strips nobody has expanded yet.
1937
+ if (method === 'GET' && routePath === '/perf/series') {
1938
+ const modelId = url.searchParams.get('model') ?? ''
1939
+ const credential = deps.sealedCredential?.() ?? null
1940
+ const root = siteRootFor(credential?.base)
1941
+ const series = await readSeries({ root, modelId, fetch: deps.perfFetch ?? directFetch })
1942
+ return send(200, series === null ? { series: null } : series)
1943
+ }
1879
1944
  if (method === 'GET' && routePath === '/outlet') {
1880
1945
  return send(200, await deps.outletStatus())
1881
1946
  }
@@ -1967,6 +2032,21 @@ export function createApiRoutes(deps) {
1967
2032
  if (method === 'GET' && routePath === '/meta') {
1968
2033
  return send(200, { ...deps.meta(), feed: { fetchedAt: deps.announcements.view().fetchedAt, source: deps.announcements.view().source, error: deps.announcements.view().error }, update: deps.update.status() })
1969
2034
  }
2035
+ if (method === 'POST' && routePath === '/reload-notice/ack') {
2036
+ const body = await readJson(req)
2037
+ const at = body?.at
2038
+ const current = Number(deps.settings.get().reloadedAt ?? 0)
2039
+ // Only the timestamp this process announced may be acknowledged: an
2040
+ // older page or a hand-written request must not pre-mark a future
2041
+ // reload as seen.
2042
+ if (typeof at !== 'number' || !Number.isSafeInteger(at) || at <= 0 || at !== current) {
2043
+ return send(400, { error: 'at must match the current reloadedAt' })
2044
+ }
2045
+ deps.settings.update({ reloadNoticeSeen: at })
2046
+ deps.settings.flush()
2047
+ if (deps.settings.writeFailed === true) return send(500, { error: 'could not persist settings' })
2048
+ return send(200, { ok: true, reloadNoticeSeen: at })
2049
+ }
1970
2050
  if (method === 'POST' && routePath === '/eac/login/start') {
1971
2051
  const started = await deps.eacAuth.start()
1972
2052
  return send(started.error === undefined ? 200 : 404, started.error === undefined ? started : { error: started.error })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebony-vinyl/dsh-our-free-model",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "type": "module",
5
5
  "description": "Our Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 DeepSeek V4.1 Flash、Kimi K3 在内的前沿模型,桌面端另有 EAC 协付渠道;2.0 起接入 LobsterAI、Qoder、TRAE、Cline、Loomy、Raccoon、MiniMax、ZCode、Gemini 十个账号渠道(原有匿名免费通道保留)(登录、账号池、积分领取、本地 OpenAI 网关与局域网转发、数据看板与运行日志),六页毛玻璃界面统一呈现。",
6
6
  "repository": {
@@ -18,6 +18,28 @@
18
18
  "./package.json": "./package.json",
19
19
  "./locale/*.json": "./locale/*.json"
20
20
  },
21
+ "scripts": {
22
+ "pack:standalone": "node scripts/pack-standalone.mjs",
23
+ "test:package": "node scripts/standalone-package-test.mjs",
24
+ "start:standalone": "node packages/standalone/cli.mjs",
25
+ "test:standalone": "node scripts/standalone-test.mjs",
26
+ "test:management": "node scripts/standalone-management-test.mjs",
27
+ "build:standalone-channels": "node scripts/build-standalone-channels.mjs",
28
+ "build:standalone-ui": "node scripts/build-standalone-ui.mjs",
29
+ "build:magpie": "node scripts/build-magpie-engine.mjs",
30
+ "update:magpie": "node scripts/update-magpie-engine.mjs",
31
+ "typecheck:standalone": "npm --prefix packages/standalone run typecheck",
32
+ "test:standalone-channels": "node scripts/standalone-channels-test.mjs",
33
+ "test": "node scripts/test-all.mjs",
34
+ "test:contributor": "node scripts/test-all.mjs --mode contributor",
35
+ "test:release": "node scripts/test-all.mjs --mode release",
36
+ "manifest": "node scripts/build-manifest.mjs",
37
+ "channel-pack": "node scripts/build-channel-pack.mjs",
38
+ "manifest:check": "node scripts/build-manifest.mjs --check",
39
+ "selftest": "node scripts/host-selftest.mjs",
40
+ "typecheck": "tsc --noEmit -p tsconfig.json",
41
+ "build:legacy-payload": "node scripts/build-legacy-payload.mjs"
42
+ },
21
43
  "files": [
22
44
  "index.js",
23
45
  "client.js",
@@ -117,7 +139,20 @@
117
139
  "@deepseek-ai/dsh-llm": "^0.1.2-rc.1 || ^0.1.7-rc.2 || ^0.2.0-rc.1",
118
140
  "@deepseek-ai/schemastery": "^3.18.4"
119
141
  },
120
- "publishConfig": {
121
- "access": "public"
142
+ "devDependencies": {
143
+ "@gfx/zopfli": "^1.0.15",
144
+ "dsh-llm-legacy": "npm:@deepseek-ai/dsh-llm@0.1.5-rc.2",
145
+ "esbuild": "0.25.9",
146
+ "typescript": "^5.9.2",
147
+ "undici": "8.12.2"
148
+ },
149
+ "overrides": {
150
+ "dsh-llm-legacy": {
151
+ "@deepseek-ai/dsh-brand": "0.1.5-rc.2",
152
+ "@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
153
+ "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.2",
154
+ "@deepseek-ai/dsh-util-crypto": "0.1.5-rc.2",
155
+ "@deepseek-ai/dsh-util-values": "0.1.5-rc.2"
156
+ }
122
157
  }
123
158
  }
package/src/catalog.js CHANGED
@@ -296,17 +296,33 @@ export const EAC_CHANNEL = 'eac'
296
296
  export const EAC_TAG = 'EAC'
297
297
 
298
298
  const EAC_DISPLAY_NAMES = {
299
- // The OpenCode free lane is relayed server-side; the org prefix is routing
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',
299
+ // These two ids are the lane's routing names; the models behind them are
300
+ // Dahl Inference's (`inference.dahl.global`), whose own docs name them
301
+ // "DeepSeek V4 Flash" and "GLM 5.3 Flash". The card shows that name, not the
302
+ // relay's internal spelling — the org prefix and the `-free` suffix are both
303
+ // routing detail.
304
+ 'opencode/deepseek-v4.1-flash-free': 'DeepSeek V4 Flash',
305
+ 'opencode/glm-5.3-flash-free': 'GLM 5.3 Flash',
303
306
  'deepseek-ai/deepseek-v4.1-flash': 'DeepSeek V4.1 Flash',
304
- 'moonshotai/kimi-k2.6': 'Kimi K2.6',
307
+ // Kimi K2.6 was removed from the NVIDIA channel on 2026-11-14 (its listing
308
+ // there never served this lane), so it is no longer named here either — an
309
+ // entry the catalog cannot receive is a name the picker never shows.
305
310
  'moonshotai/kimi-k3': 'Kimi K3',
306
311
  'openai/gpt-oss-20b': 'GPT-OSS 20B',
307
312
  'z-ai/glm-5.3': 'GLM 5.3',
308
313
  'z-ai/glm-5.3-flash': 'GLM 5.3 Flash',
309
314
  'mistralai/mistral-large-4-1t-a52b': 'Mistral Large 4 1T A52B',
315
+ // AMD Radeon (developer.amd.com.cn). The lane namespaces its ids `amd/…`;
316
+ // the card drops the prefix the same way it drops `org/` elsewhere.
317
+ 'amd/DeepSeek-V4.1-Flash': 'DeepSeek V4.1 Flash (AMD)',
318
+ 'amd/DeepSeek-V4-Flash': 'DeepSeek V4 Flash (AMD)',
319
+ 'amd/Gemma-4-31B-IT': 'Gemma 4 31B IT (AMD)',
320
+ 'amd/GLM-5.3-Flash': 'GLM 5.3 Flash (AMD)',
321
+ 'amd/MiMo-V2.6-Flash': 'MiMo V2.6 Flash (AMD)',
322
+ 'amd/MiniCPM5-2B': 'MiniCPM5 2B (AMD)',
323
+ 'amd/MiniCPM-V-4.7-1B': 'MiniCPM-V 4.7 1B (AMD)',
324
+ 'amd/MiniCPM-V-4.7-35B-A3B': 'MiniCPM-V 4.7 35B A3B (AMD)',
325
+ 'amd/Qwen3.8-27B': 'Qwen3.8 27B (AMD)',
310
326
  }
311
327
 
312
328
  /**
@@ -364,24 +380,126 @@ const EAC_CAPABILITIES = [
364
380
  effortPatch: { reasoning_effort: '$effort' },
365
381
  effortOffPatch: { reasoning_effort: 'none' },
366
382
  },
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.
383
+ // The OpenCode lane's two ids, served by Dahl Inference
384
+ // (`inference.dahl.global`, docs read 2026-11-14). Both are the broker's own
385
+ // ids: DeepSeek-V4-Flash-0731 and GLM-5.3-Flash.
386
+ //
387
+ // - Window: Dahl's `/docs/models/` publishes 400K for both.
388
+ // - Vision: Dahl states plainly that vision "is not offered on current
389
+ // models", and `image_url` content is rejected with a 400 — so `vision` is
390
+ // false, not the NVIDIA Build `true` its namesakes carry.
391
+ // - Menu: Dahl validates `reasoning_effort` against
392
+ // none|minimal|low|medium|high|xhigh|max and rejects `disabled` by name
393
+ // ("unsupported value: got \"disabled\""), so the off rung is `none` — the
394
+ // `effortOffPatch` below, which is what the plugin sends for `disabled`.
395
+ // - Output: DeepSeek's ceiling is measured, not guessed — a 5M `max_tokens`
396
+ // request came back `finish_reason: length` at 8192 completion tokens, so
397
+ // the broker caps output there regardless of what the caller asks for. GLM
398
+ // shares that broker cap; Dahl publishes no per-model figure.
375
399
  {
376
400
  match: /^opencode\/deepseek-v4/,
377
- vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 384000,
401
+ vision: false, reasoning: true, contextWindow: 409600, maxOutput: 8192,
378
402
  efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
379
403
  effortPatch: { reasoning_effort: '$effort' },
380
404
  effortOffPatch: { reasoning_effort: 'none' },
381
405
  },
382
406
  {
407
+ // GLM answers with reasoning even at `none` (measured: `none` still spent
408
+ // 57 reasoning tokens), so its menu has no off rung and the card shows the
409
+ // shared-budget tag rather than promising a switch it cannot honour.
383
410
  match: /^opencode\/glm-5\.3-flash/,
384
- vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 131072,
411
+ vision: false, reasoning: true, contextWindow: 409600, maxOutput: 8192,
412
+ efforts: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
413
+ effortPatch: { reasoning_effort: '$effort' },
414
+ },
415
+ // ── AMD Radeon (developer.amd.com.cn) ───────────────────────────────────────
416
+ //
417
+ // Two sources, each used for what it actually publishes:
418
+ //
419
+ // - The platform's own listing (`GET /radeon/api/v1/models`, read 2026-11-14)
420
+ // carries `context_length`, `architecture.input_modalities`, and a
421
+ // per-provider block with `vision`, `reasoning` and `tools`.
422
+ // `supported_parameters` lists `reasoning_effort`, so the level is the real
423
+ // control on the wire.
424
+ // - The output ceiling comes from **each model's own vendor docs**, because
425
+ // the platform publishes none. Where a vendor publishes one it is used
426
+ // verbatim; where a vendor publishes none either, `maxOutput` falls back to
427
+ // the platform's window and the comment says so — a guess is worse than a
428
+ // generous ceiling the vendor may clamp.
429
+ //
430
+ // Per-model vendor sources:
431
+ // - DeepSeek V4.1 Flash / V4 Flash: api-docs.deepseek.com model table —
432
+ // 1M context, 384K max output (`deepseek-flash`, "DeepSeek-V4.1-Flash-1017";
433
+ // the older v4-flash name is served by the same model).
434
+ // - GLM-5.3-Flash: docs.z.ai/guides/llm/glm-5.3-flash — 1M context, 128K max
435
+ // output. (The platform's listing reports 262144 for this id; the vendor's
436
+ // own figure is the larger one, so the window below is the platform's, which
437
+ // is the one requests actually hit.)
438
+ // - Gemma 4 31B IT: ai.google.dev model card — 256K context; Google publishes
439
+ // no output ceiling, so it inherits the window.
440
+ // - MiMo V2.6 Flash / Qwen3.8 27B / MiniCPM-V 4.7 / MiniCPM5 2B: neither the
441
+ // platform nor the vendor publishes an output ceiling (checked 2026-11-14),
442
+ // so each inherits its window and the card shows the window as the ceiling.
443
+ {
444
+ match: /^amd\/DeepSeek-V4\.1-Flash/,
445
+ vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 393216,
446
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
447
+ effortPatch: { reasoning_effort: '$effort' },
448
+ effortOffPatch: { reasoning_effort: 'none' },
449
+ },
450
+ {
451
+ match: /^amd\/DeepSeek-V4-Flash/,
452
+ vision: false, reasoning: true, contextWindow: 1048576, maxOutput: 393216,
453
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
454
+ effortPatch: { reasoning_effort: '$effort' },
455
+ effortOffPatch: { reasoning_effort: 'none' },
456
+ },
457
+ {
458
+ match: /^amd\/GLM-5\.3-Flash/,
459
+ vision: false, reasoning: true, contextWindow: 262144, maxOutput: 131072,
460
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
461
+ effortPatch: { reasoning_effort: '$effort' },
462
+ effortOffPatch: { reasoning_effort: 'none' },
463
+ },
464
+ {
465
+ // Xiaomi publishes no output ceiling for MiMo V2.6 Flash, so it inherits
466
+ // the window rather than a guessed 32K.
467
+ match: /^amd\/MiMo-V2\.6-Flash/,
468
+ vision: true, reasoning: true, contextWindow: 1048576, maxOutput: 1048576,
469
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
470
+ effortPatch: { reasoning_effort: '$effort' },
471
+ effortOffPatch: { reasoning_effort: 'none' },
472
+ },
473
+ {
474
+ // Google's Gemma 4 model card states 256K of context and no output ceiling.
475
+ match: /^amd\/Gemma-4-31B-IT/,
476
+ vision: true, reasoning: true, contextWindow: 262144, maxOutput: 262144,
477
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
478
+ effortPatch: { reasoning_effort: '$effort' },
479
+ effortOffPatch: { reasoning_effort: 'none' },
480
+ },
481
+ {
482
+ // Alibaba publishes no output ceiling for Qwen3.8-27B on the model-studio
483
+ // pages; it inherits the window.
484
+ match: /^amd\/Qwen3\.8-27B/,
485
+ vision: true, reasoning: true, contextWindow: 262144, maxOutput: 262144,
486
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
487
+ effortPatch: { reasoning_effort: '$effort' },
488
+ effortOffPatch: { reasoning_effort: 'none' },
489
+ },
490
+ {
491
+ // OpenBMB publishes no output ceiling for the MiniCPM-V 4.7 pair either.
492
+ match: /^amd\/MiniCPM-V/,
493
+ vision: true, reasoning: true, contextWindow: 262144, maxOutput: 262144,
494
+ efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
495
+ effortPatch: { reasoning_effort: '$effort' },
496
+ effortOffPatch: { reasoning_effort: 'none' },
497
+ },
498
+ {
499
+ // MiniCPM5-2B: the project README states "Context Length 131,072" and no
500
+ // output ceiling.
501
+ match: /^amd\/MiniCPM5-2B/,
502
+ vision: false, reasoning: true, contextWindow: 131072, maxOutput: 131072,
385
503
  efforts: ['disabled', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'], effortDefault: 'high',
386
504
  effortPatch: { reasoning_effort: '$effort' },
387
505
  effortOffPatch: { reasoning_effort: 'none' },
@@ -431,25 +549,25 @@ const EAC_FEATURED = new Set([
431
549
  /**
432
550
  * Display-only ids for the card's small line.
433
551
  *
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.
552
+ * The lane serves its ids under its own routing names, with a `-free` suffix
553
+ * that is upstream detail. Behind them are Dahl Inference's models, and the
554
+ * line shows the id Dahl itself publishes (`GET /v1/models`, read 2026-11-14)
555
+ * — that is the string a reader can paste into a Dahl-compatible client. Only
556
+ * the label moves: the catalog `id` is what the adapter matches, and it keeps
557
+ * the lane's spelling.
440
558
  */
441
559
  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',
560
+ 'opencode/deepseek-v4.1-flash-free': 'deepseek-ai/DeepSeek-V4-Flash-0731',
561
+ 'opencode/glm-5.3-flash-free': 'zai-org/GLM-5.3-Flash',
444
562
  }
445
563
 
446
564
  function eacLabel(modelId) {
447
565
  const known = EAC_LABELS[modelId]
448
566
  if (known !== undefined) return known
449
567
  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'))
568
+ // Any remaining `-free` suffix is the lane's own routing detail; the line
569
+ // drops it rather than leaking it into the card.
570
+ return bare.replace(/(?:^|[-_])free(?:$|[-_.])/, '')
453
571
  }
454
572
 
455
573
  /** `org/model` → the model's own name under the channel tag. */
@@ -491,11 +609,10 @@ export function buildEacCatalog(ids) {
491
609
  // The card's small line under the name. The `id` keeps its `org/` prefix —
492
610
  // that is what keeps this row distinct from the free lane's same-named
493
611
  // model during the merge — but the prefix is routing detail, so the line
494
- // shows the model's own id, with a display-only version marker where needed.
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.
612
+ // shows the id the serving platform itself publishes: for the lane's two
613
+ // models that is Dahl's own `deepseek-ai/DeepSeek-V4-Flash-0731` /
614
+ // `zai-org/GLM-5.3-Flash`, which a reader can paste into any Dahl client.
615
+ // The catalog id is untouched, so routing still matches.
499
616
  label: eacLabel(id),
500
617
  channel: EAC_CHANNEL,
501
618
  wire: 'chat',
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
  import { randomBytes, createHash } from 'node:crypto'
3
3
  import { existsSync, readFileSync, createReadStream, statSync } from 'node:fs'
4
- import { mkdir, readFile, writeFile, rename, link, unlink, realpath } from 'node:fs/promises'
4
+ import { mkdir, readFile, writeFile, rename, link, unlink, realpath, rm } from 'node:fs/promises'
5
5
  import http from 'node:http'
6
6
  import net from 'node:net'
7
7
  import path from 'node:path'
@@ -780,6 +780,22 @@ export function createMagpieEngine({ dataDir, logger, binary } = {}) {
780
780
  for (const dir of [agentHome, agentRoaming, agentLocal]) {
781
781
  await mkdir(dir, { recursive: true, mode: 0o700 })
782
782
  }
783
+ // The engine goes straight to -p for a while after Claude Code's
784
+ // interactive mode failed to start (its claude-tui-broken mark, under
785
+ // this engine's LOCALAPPDATA). A mark earned by the previous engine
786
+ // says nothing about this one: after an engine upgrade that carries a
787
+ // start fix, a stale mark would keep the TUI off for up to a day.
788
+ // Forget it when the engine's revision changes.
789
+ const engineRevision = metadata.revision || ''
790
+ if (engineRevision !== '') {
791
+ const revisionFile = path.join(home, '.ofm-engine-revision')
792
+ let previousRevision = ''
793
+ try { previousRevision = (await readFile(revisionFile, 'utf8')).trim() } catch { /* first boot: no mark to forget either */ }
794
+ if (previousRevision !== engineRevision) {
795
+ await rm(path.join(agentLocal, 'magpie', 'claude-tui-broken'), { force: true }).catch(() => {})
796
+ await writeFile(revisionFile, `${engineRevision}\n`).catch(() => {})
797
+ }
798
+ }
783
799
  // Windows folds every ancestor of a session folder into a 260-character
784
800
  // budget; this redirection adds ~40, and claude appends a slug of the
785
801
  // whole project path on top. A long or non-ASCII data home is the class