free-coding-models 0.3.55 → 0.3.56

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 (73) hide show
  1. package/CHANGELOG.md +47 -56
  2. package/README.md +236 -160
  3. package/bin/free-coding-models.js +46 -0
  4. package/package.json +2 -2
  5. package/sources.js +133 -309
  6. package/src/analysis.js +23 -10
  7. package/src/app.js +113 -7
  8. package/src/cache.js +1 -1
  9. package/src/cli-help.js +9 -0
  10. package/src/command-palette.js +16 -12
  11. package/src/config.js +199 -32
  12. package/src/endpoint-installer.js +45 -1
  13. package/src/favorites.js +22 -0
  14. package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
  15. package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
  16. package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
  17. package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
  18. package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
  19. package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
  20. package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
  21. package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
  22. package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
  23. package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
  24. package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
  25. package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
  26. package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
  27. package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
  28. package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
  29. package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
  30. package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
  31. package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
  32. package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
  33. package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
  34. package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
  35. package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
  36. package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
  37. package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
  38. package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
  39. package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
  40. package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
  41. package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
  42. package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
  43. package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
  44. package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
  45. package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
  46. package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
  47. package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
  48. package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
  49. package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
  50. package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
  51. package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
  52. package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
  53. package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
  54. package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
  55. package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
  56. package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
  57. package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
  58. package/src/key-handler.js +312 -12
  59. package/src/kilo.js +20 -1
  60. package/src/opencode.js +23 -2
  61. package/src/overlays.js +206 -5
  62. package/src/provider-metadata.js +26 -17
  63. package/src/quota-capabilities.js +6 -10
  64. package/src/render-table.js +37 -4
  65. package/src/router-daemon.js +1986 -0
  66. package/src/router-dashboard.js +893 -0
  67. package/src/sync-set.js +479 -0
  68. package/src/theme.js +4 -0
  69. package/src/tool-launchers.js +1 -0
  70. package/src/tool-metadata.js +6 -2
  71. package/src/utils.js +30 -6
  72. package/web/dist/assets/{index-C03JjCgA.js → index-DNRCaWPi.js} +2 -2
  73. package/web/dist/index.html +1 -1
@@ -33,7 +33,10 @@
33
33
 
34
34
  import { loadChangelog } from './changelog-loader.js'
35
35
  import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
36
- import { loadConfig, replaceConfigContents } from './config.js'
36
+ import { loadConfig, saveConfig, replaceConfigContents } from './config.js'
37
+ import { join, dirname } from 'node:path'
38
+ import { fileURLToPath } from 'node:url'
39
+ import { spawn } from 'node:child_process'
37
40
  import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
38
41
  import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
39
42
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
@@ -42,12 +45,23 @@ import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntrie
42
45
  import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
43
46
  import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
44
47
  import { startExternalTool } from './tool-launchers.js'
48
+ import {
49
+ clearRouterDashboardRequestLog,
50
+ closeRouterDashboardOverlay,
51
+ cycleRouterDashboardActiveSet,
52
+ cycleRouterDashboardProbeMode,
53
+ openRouterDashboardOverlay,
54
+ restartRouterDashboardDaemon,
55
+ toggleRouterDashboardProbePause,
56
+ } from './router-dashboard.js'
45
57
 
46
58
  // 📖 Some providers need an explicit probe model because the first catalog entry
47
59
  // 📖 is not guaranteed to be accepted by their chat endpoint.
48
60
  const PROVIDER_TEST_MODEL_OVERRIDES = {
49
- sambanova: ['DeepSeek-V3-0324'],
50
- nvidia: ['deepseek-ai/deepseek-v3.1-terminus', 'openai/gpt-oss-120b'],
61
+ sambanova: ['MiniMax-M2.5', 'DeepSeek-V3.1', 'DeepSeek-V3.2'],
62
+ nvidia: ['deepseek-ai/deepseek-v4-flash', 'openai/gpt-oss-120b'],
63
+ 'github-models': ['openai/gpt-4.1-mini'],
64
+ mistral: ['mistral-small-latest', 'devstral-small-latest'],
51
65
  }
52
66
 
53
67
  // 📖 Settings key tests retry retryable failures across several models so a
@@ -67,6 +81,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
67
81
  cerebras: { url: 'https://api.cerebras.ai/v1/models', method: 'GET' },
68
82
  sambanova: { url: 'https://api.sambanova.ai/v1/models', method: 'GET' },
69
83
  openrouter: { url: 'https://openrouter.ai/api/v1/models', method: 'GET' },
84
+ mistral: { url: 'https://api.mistral.ai/v1/models', method: 'GET' },
70
85
  huggingface: { url: 'https://router.huggingface.co/v1/models', method: 'GET' },
71
86
  deepinfra: { url: 'https://api.deepinfra.com/v1/models', method: 'GET' },
72
87
  fireworks: { url: 'https://api.fireworks.ai/v1/models', method: 'GET' },
@@ -79,6 +94,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
79
94
  ovhcloud: { url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models', method: 'GET' },
80
95
  qwen: { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', method: 'GET' },
81
96
  iflow: { url: 'https://apis.iflow.cn/v1/models', method: 'GET' },
97
+ 'github-models': null, // 📖 GitHub Models catalog is public; use chat ping to validate the token.
82
98
  replicate: null, // 📖 Replicate has no /models endpoint; use chat completions ping
83
99
  cloudflare: null, // 📖 Workers AI has no auth-check endpoint; use ping only
84
100
  zai: null, // 📖 ZAI undocumented; use ping only
@@ -363,6 +379,40 @@ export function createKeyHandler(ctx) {
363
379
  })
364
380
  }
365
381
 
382
+ async function syncFavoritesToRouter(selected) {
383
+ if (state.config?.router?.enabled !== true) return
384
+ const favorites = state.config.favorites || []
385
+ const selKey = toFavoriteKey(selected.providerKey, selected.modelId)
386
+ const chain = [selKey, ...favorites.filter((f) => f !== selKey)]
387
+ const models = chain.map((f, i) => {
388
+ const slashIdx = f.indexOf('/')
389
+ const provider = slashIdx >= 0 ? f.slice(0, slashIdx) : '?'
390
+ const model = slashIdx >= 0 ? f.slice(slashIdx + 1) : f
391
+ return { provider, model, priority: i + 1 }
392
+ })
393
+ try {
394
+ const port = await readDaemonPort()
395
+ if (!port) return
396
+ const baseUrl = `http://127.0.0.1:${port}`
397
+ const setPayload = { name: 'fast-coding', models, created: new Date().toISOString() }
398
+ await globalThis.fetch(`${baseUrl}/sets/fast-coding`, {
399
+ method: 'PUT',
400
+ headers: { 'Content-Type': 'application/json' },
401
+ body: JSON.stringify(setPayload),
402
+ })
403
+ await globalThis.fetch(`${baseUrl}/sets/fast-coding/activate`, { method: 'POST' })
404
+ } catch {}
405
+ }
406
+
407
+ async function readDaemonPort() {
408
+ try {
409
+ const { readFileSync } = await import('node:fs')
410
+ const raw = readFileSync(`${process.env.HOME}/.free-coding-models-daemon.port`, 'utf8').trim()
411
+ if (/^\d+$/.test(raw)) return Number(raw)
412
+ } catch {}
413
+ return null
414
+ }
415
+
366
416
  async function launchSelectedModel(selected, options = {}) {
367
417
  const { uiAlreadyStopped = false } = options
368
418
  userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
@@ -373,7 +423,9 @@ export function createKeyHandler(ctx) {
373
423
  stopUi()
374
424
  }
375
425
 
376
- // 📖 Show selection status before handing control to the target tool.
426
+ // 📖 If router is enabled, push [selected, ...favorites] to daemon as the active set
427
+ await syncFavoritesToRouter(selected)
428
+
377
429
  if (selected.status === 'timeout') {
378
430
  console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
379
431
  } else if (selected.status === 'down') {
@@ -928,6 +980,51 @@ export function createKeyHandler(ctx) {
928
980
  }
929
981
  }
930
982
 
983
+
984
+ // 📖 Token Usage screen — Shift+T from main table. Fetches daily token history
985
+ // 📖 from the daemon and renders a 7-day chart plus today/all-time breakdowns.
986
+ async function openTokenUsageOverlay() {
987
+ state.tokenUsageOpen = true
988
+ state.tokenUsageScrollOffset = 0
989
+ state.tokenUsageError = null
990
+ state.tokenUsageData = null
991
+ // 📖 Discover daemon port
992
+ let port = 19280
993
+ try {
994
+ const { readFileSync: rfs } = await import('node:fs')
995
+ const portPath = `${process.env.HOME}/.free-coding-models-daemon.port`
996
+ const savedPort = rfs(portPath, 'utf8').trim()
997
+ if (/^\d+$/.test(savedPort)) port = Number(savedPort)
998
+ } catch {}
999
+ state.tokenUsageLastFetchAt = Date.now()
1000
+ try {
1001
+ const controller = new AbortController()
1002
+ const timer = setTimeout(() => controller.abort(), 2000)
1003
+ const res = await globalThis.fetch(`http://127.0.0.1:${port}/stats/tokens`, { signal: controller.signal })
1004
+ clearTimeout(timer)
1005
+ if (!res.ok) {
1006
+ state.tokenUsageError = `Daemon returned HTTP ${res.status} — is the router running?`
1007
+ return
1008
+ }
1009
+ // 📖 Guard: res.json() can throw on malformed response body
1010
+ const text = await res.text()
1011
+ try {
1012
+ state.tokenUsageData = JSON.parse(text)
1013
+ } catch {
1014
+ state.tokenUsageError = 'Daemon returned invalid JSON — try restarting the daemon'
1015
+ }
1016
+ } catch (err) {
1017
+ state.tokenUsageError = err?.name === 'AbortError' ? 'Request timed out — is the router daemon running?' : (err?.message || 'Failed to fetch token stats')
1018
+ }
1019
+ }
1020
+
1021
+ function closeTokenUsageOverlay() {
1022
+ state.tokenUsageOpen = false
1023
+ state.tokenUsageScrollOffset = 0
1024
+ state.tokenUsageError = null
1025
+ }
1026
+
1027
+
931
1028
  function cycleToolMode() {
932
1029
  const modeOrder = getToolModeOrder()
933
1030
  const currentIndex = modeOrder.indexOf(state.mode)
@@ -1016,6 +1113,7 @@ export function createKeyHandler(ctx) {
1016
1113
  || state.installEndpointsOpen
1017
1114
  || state.toolInstallPromptOpen
1018
1115
  || state.installedModelsOpen
1116
+ || state.routerDashboardOpen
1019
1117
  || state.recommendOpen
1020
1118
  || state.feedbackOpen
1021
1119
  || state.helpVisible
@@ -1207,6 +1305,8 @@ export function createKeyHandler(ctx) {
1207
1305
  case 'open-changelog': return openChangelogOverlay()
1208
1306
  case 'open-feedback': return openFeedbackOverlay()
1209
1307
  case 'open-recommend': return openRecommendOverlay()
1308
+ case 'open-router-dashboard': return openRouterDashboardOverlay(state)
1309
+ case 'open-token-usage': return openTokenUsageOverlay()
1210
1310
  case 'open-install-endpoints': return openInstallEndpointsOverlay()
1211
1311
  case 'open-installed-models': return openInstalledModelsOverlay()
1212
1312
  case 'action-cycle-theme': return cycleGlobalTheme()
@@ -1340,6 +1440,59 @@ export function createKeyHandler(ctx) {
1340
1440
 
1341
1441
  // 📖 Profile system removed - API keys now persist permanently across all sessions
1342
1442
 
1443
+ // 📖 Router Dashboard captures local dashboard controls while open so keys
1444
+ // 📖 like S/I/R/C do not leak through to sort/filter actions in the table.
1445
+ if (state.routerDashboardOpen) {
1446
+ if (key.ctrl && key.name === 'c') { exit(0); return }
1447
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
1448
+
1449
+ if (key.name === 'escape') {
1450
+ closeRouterDashboardOverlay(state)
1451
+ return
1452
+ }
1453
+ if (key.name === 'up' || key.name === 'k') {
1454
+ state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - 1)
1455
+ return
1456
+ }
1457
+ if (key.name === 'down' || key.name === 'j') {
1458
+ state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + 1
1459
+ return
1460
+ }
1461
+ if (key.name === 'pageup') {
1462
+ state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - pageStep)
1463
+ return
1464
+ }
1465
+ if (key.name === 'pagedown') {
1466
+ state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + pageStep
1467
+ return
1468
+ }
1469
+ if (key.name === 'home') {
1470
+ state.routerDashboardScrollOffset = 0
1471
+ return
1472
+ }
1473
+ if (key.name === 's') {
1474
+ try { await cycleRouterDashboardActiveSet(state) } catch {}
1475
+ return
1476
+ }
1477
+ if (key.name === 'i') {
1478
+ try { await cycleRouterDashboardProbeMode(state) } catch {}
1479
+ return
1480
+ }
1481
+ if (key.name === 'r') {
1482
+ restartRouterDashboardDaemon(state)
1483
+ return
1484
+ }
1485
+ if (key.name === 'c') {
1486
+ clearRouterDashboardRequestLog(state)
1487
+ return
1488
+ }
1489
+ if (key.name === 'p') {
1490
+ toggleRouterDashboardProbePause(state)
1491
+ return
1492
+ }
1493
+ return
1494
+ }
1495
+
1343
1496
  // 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
1344
1497
  if (state.installEndpointsOpen) {
1345
1498
  if (key.ctrl && key.name === 'c') { exit(0); return }
@@ -1820,6 +1973,116 @@ export function createKeyHandler(ctx) {
1820
1973
  return
1821
1974
  }
1822
1975
 
1976
+ // 📖 Token Usage overlay: Shift+T shows token history chart and today/all-time breakdowns.
1977
+ if (state.tokenUsageOpen) {
1978
+ if (key.ctrl && key.name === 'c') { exit(0); return }
1979
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
1980
+ if (key.name === 'escape') {
1981
+ closeTokenUsageOverlay()
1982
+ return
1983
+ }
1984
+ if (key.name === 'up' || key.name === 'k') {
1985
+ state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - 1)
1986
+ return
1987
+ }
1988
+ if (key.name === 'down' || key.name === 'j') {
1989
+ state.tokenUsageScrollOffset += 1
1990
+ return
1991
+ }
1992
+ if (key.name === 'pageup') {
1993
+ state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - pageStep)
1994
+ return
1995
+ }
1996
+ if (key.name === 'pagedown') {
1997
+ state.tokenUsageScrollOffset += pageStep
1998
+ return
1999
+ }
2000
+ if (key.name === 'home') {
2001
+ state.tokenUsageScrollOffset = 0
2002
+ return
2003
+ }
2004
+ if (key.name === 'end') {
2005
+ state.tokenUsageScrollOffset = Number.MAX_SAFE_INTEGER
2006
+ return
2007
+ }
2008
+ return
2009
+ }
2010
+
2011
+ // 📖 Router Onboarding overlay: shown on first launch. Y=yes enable, N=not now, Esc=cancel.
2012
+ if (state.routerOnboardingOpen) {
2013
+ if (key.ctrl && key.name === 'c') { exit(0); return }
2014
+ if (state.routerOnboardingPhase === 'loading' || state.routerOnboardingPhase === 'success' || state.routerOnboardingPhase === 'error') {
2015
+ if (key.name === 'escape' || key.name === 'return') {
2016
+ state.routerOnboardingOpen = false
2017
+ // 📖 Mark onboarding as seen (don't show again)
2018
+ if (state.config?.router) {
2019
+ state.config.router.onboardingSeen = true
2020
+ }
2021
+ return
2022
+ }
2023
+ return
2024
+ }
2025
+ if (key.name === 'escape' || key.name === 'n') {
2026
+ state.routerOnboardingOpen = false
2027
+ // 📖 Mark as seen and disabled
2028
+ if (state.config?.router) {
2029
+ state.config.router.onboardingSeen = true
2030
+ state.config.router.enabled = false
2031
+ }
2032
+ return
2033
+ }
2034
+ if (key.name === 'up' || key.name === 'k') {
2035
+ state.routerOnboardingCursor = 0
2036
+ return
2037
+ }
2038
+ if (key.name === 'down' || key.name === 'j') {
2039
+ state.routerOnboardingCursor = 1
2040
+ return
2041
+ }
2042
+ if (key.name === 'return' || key.name === 'y') {
2043
+ const shouldEnable = key.name === 'return' ? true : (state.routerOnboardingCursor === 0)
2044
+ if (!shouldEnable) {
2045
+ state.routerOnboardingOpen = false
2046
+ if (state.config?.router) {
2047
+ state.config.router.onboardingSeen = true
2048
+ state.config.router.enabled = false
2049
+ }
2050
+ return
2051
+ }
2052
+ // 📖 Enable router: start daemon in background and mark onboarding seen
2053
+ state.routerOnboardingPhase = 'loading'
2054
+ state.routerOnboardingError = null
2055
+ void (async () => {
2056
+ try {
2057
+ const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
2058
+ const child = spawn('node', [binPath, '--daemon-bg'], {
2059
+ detached: true,
2060
+ stdio: 'ignore',
2061
+ })
2062
+ child.unref()
2063
+ await new Promise((r) => setTimeout(r, 2000))
2064
+ if (state.routerOnboardingPhase === 'loading') {
2065
+ state.routerOnboardingPhase = 'success'
2066
+ if (state.config?.router) {
2067
+ state.config.router.enabled = true
2068
+ state.config.router.onboardingSeen = true
2069
+ saveConfig(state.config)
2070
+ }
2071
+ trackTelemetryEvent('app_router_install', { router_version: '0.4.0' })
2072
+ await new Promise((r) => setTimeout(r, 1500))
2073
+ state.routerOnboardingOpen = false
2074
+ openRouterDashboardOverlay(state)
2075
+ }
2076
+ } catch (err) {
2077
+ state.routerOnboardingPhase = 'error'
2078
+ state.routerOnboardingError = err?.message || 'Failed to start router'
2079
+ }
2080
+ })()
2081
+ return
2082
+ }
2083
+ return
2084
+ }
2085
+
1823
2086
  // 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
1824
2087
  if (state.changelogOpen) {
1825
2088
  const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
@@ -2366,9 +2629,15 @@ export function createKeyHandler(ctx) {
2366
2629
 
2367
2630
  // 📖 Profile system removed - API keys now persist permanently across all sessions
2368
2631
 
2369
- // 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
2370
- if (key.name === 'r' && key.shift) {
2371
- resetViewSettings()
2632
+ // 📖 Shift+R: open the Smart Model Router dashboard from the main table.
2633
+ if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
2634
+ openRouterDashboardOverlay(state)
2635
+ return
2636
+ }
2637
+
2638
+ // 📖 Shift+T: open the Token Usage screen.
2639
+ if (key.name === 't' && key.shift && !key.ctrl && !key.meta) {
2640
+ openTokenUsageOverlay()
2372
2641
  return
2373
2642
  }
2374
2643
 
@@ -2383,7 +2652,7 @@ export function createKeyHandler(ctx) {
2383
2652
  // 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
2384
2653
  // 📖 X clears the active custom text filter.
2385
2654
  // 📖 D is now reserved for provider filter cycling
2386
- // 📖 Shift+R is reserved for reset view settings
2655
+ // 📖 Shift+R is reserved for the Router Dashboard; reset view remains in Ctrl+P.
2387
2656
  const sortKeys = {
2388
2657
  'r': 'rank', 'o': 'origin', 'm': 'model',
2389
2658
  'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime'
@@ -2502,6 +2771,28 @@ export function createKeyHandler(ctx) {
2502
2771
  return
2503
2772
  }
2504
2773
 
2774
+ if (key.shift && key.name === 'up') {
2775
+ const selected = state.visibleSorted?.[state.cursor]
2776
+ if (selected?.isFavorite) {
2777
+ reorderFavorite(state.config, selected.providerKey, selected.modelId, 'up')
2778
+ syncFavoriteFlags(state.results, state.config)
2779
+ applyTierFilter()
2780
+ refreshVisibleSorted({ resetCursor: false })
2781
+ }
2782
+ return
2783
+ }
2784
+
2785
+ if (key.shift && key.name === 'down') {
2786
+ const selected = state.visibleSorted?.[state.cursor]
2787
+ if (selected?.isFavorite) {
2788
+ reorderFavorite(state.config, selected.providerKey, selected.modelId, 'down')
2789
+ syncFavoriteFlags(state.results, state.config)
2790
+ applyTierFilter()
2791
+ refreshVisibleSorted({ resetCursor: false })
2792
+ }
2793
+ return
2794
+ }
2795
+
2505
2796
  if (key.name === 'up' || key.name === 'k') {
2506
2797
  // 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
2507
2798
  const count = state.visibleSorted.length
@@ -2527,12 +2818,9 @@ export function createKeyHandler(ctx) {
2527
2818
  }
2528
2819
 
2529
2820
  if (key.name === 'return') { // Enter
2530
- // 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
2531
2821
  const selected = state.visibleSorted[state.cursor]
2532
- if (!selected) return // 📖 Guard: empty visible list (all filtered out)
2822
+ if (!selected) return
2533
2823
 
2534
- // 📖 Incompatibility intercept — if the model can't run on the active tool,
2535
- // 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
2536
2824
  if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
2537
2825
  const compatTools = getCompatibleTools(selected.providerKey)
2538
2826
  const similarModels = findSimilarCompatibleModels(
@@ -2763,6 +3051,11 @@ export function createMouseEventHandler(ctx) {
2763
3051
  }
2764
3052
  return
2765
3053
  }
3054
+ if (state.routerDashboardOpen) {
3055
+ const step = evt.type === 'scroll-up' ? -3 : 3
3056
+ state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) + step)
3057
+ return
3058
+ }
2766
3059
 
2767
3060
  // 📖 Main table scroll: move cursor up/down with wrap-around
2768
3061
  const count = state.visibleSorted.length
@@ -2843,6 +3136,11 @@ export function createMouseEventHandler(ctx) {
2843
3136
  return
2844
3137
  }
2845
3138
 
3139
+ if (state.routerDashboardOpen) {
3140
+ closeRouterDashboardOverlay(state)
3141
+ return
3142
+ }
3143
+
2846
3144
  if (state.incompatibleFallbackOpen) {
2847
3145
  // 📖 Incompatible fallback: click closes
2848
3146
  state.incompatibleFallbackOpen = false
@@ -3004,6 +3302,8 @@ export function createMouseEventHandler(ctx) {
3004
3302
  // 📖 Most are single-character keys; special cases like ctrl+p need special handling.
3005
3303
  if (zone.key === 'ctrl+p') {
3006
3304
  process.stdin.emit('keypress', '\x10', { name: 'p', ctrl: true, meta: false, shift: false })
3305
+ } else if (zone.key === 'shift+r') {
3306
+ process.stdin.emit('keypress', 'R', { name: 'r', ctrl: false, meta: false, shift: true })
3007
3307
  } else {
3008
3308
  process.stdin.emit('keypress', zone.key, { name: zone.key, ctrl: false, meta: false, shift: false })
3009
3309
  }
package/src/kilo.js CHANGED
@@ -9,6 +9,7 @@ import { loadKiloConfig, saveKiloConfig, getKiloConfigPath } from './kilo-config
9
9
  import { getApiKey } from './config.js'
10
10
  import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP } from './provider-metadata.js'
11
11
  import { resolveToolBinaryPath } from './tool-bootstrap.js'
12
+ import { sources } from '../sources.js'
12
13
 
13
14
  // 📖 Map source model IDs to Kilo built-in IDs (same as OpenCode).
14
15
  function getKiloModelId(providerKey, modelId) {
@@ -17,6 +18,21 @@ function getKiloModelId(providerKey, modelId) {
17
18
  return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
18
19
  }
19
20
 
21
+ function buildOpenAiCompatibleProviderConfig(providerKey) {
22
+ const source = sources[providerKey]
23
+ const envVarName = ENV_VAR_NAMES[providerKey]
24
+ if (!source?.url || !envVarName) return null
25
+ const baseURL = source.url
26
+ .replace(/\/chat\/completions$/i, '')
27
+ .replace(/\/responses$/i, '')
28
+ return {
29
+ npm: '@ai-sdk/openai-compatible',
30
+ name: source.name || providerKey,
31
+ options: { baseURL, apiKey: `{env:${envVarName}}` },
32
+ models: {},
33
+ }
34
+ }
35
+
20
36
  // 📖 spawnKilo: Resolve API keys + spawn kilo CLI with correct env.
21
37
  async function spawnKilo(args, providerKey, fcmConfig) {
22
38
  const envVarName = ENV_VAR_NAMES[providerKey]
@@ -120,7 +136,7 @@ export async function startKilo(model, fcmConfig) {
120
136
  config.provider.codestral = {
121
137
  npm: '@ai-sdk/openai-compatible',
122
138
  name: 'Mistral Codestral',
123
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
139
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
124
140
  models: {}
125
141
  }
126
142
  } else if (providerKey === 'hyperbolic') {
@@ -179,6 +195,9 @@ export async function startKilo(model, fcmConfig) {
179
195
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
180
196
  models: {}
181
197
  }
198
+ } else {
199
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
200
+ if (providerConfig) config.provider[providerKey] = providerConfig
182
201
  }
183
202
  }
184
203
 
package/src/opencode.js CHANGED
@@ -98,6 +98,21 @@ function getOpenCodeModelId(providerKey, modelId) {
98
98
  return OPENCODE_MODEL_MAP[providerKey]?.[modelId] || modelId
99
99
  }
100
100
 
101
+ function buildOpenAiCompatibleProviderConfig(providerKey) {
102
+ const source = sources[providerKey]
103
+ const envVarName = ENV_VAR_NAMES[providerKey]
104
+ if (!source?.url || !envVarName) return null
105
+ const baseURL = source.url
106
+ .replace(/\/chat\/completions$/i, '')
107
+ .replace(/\/responses$/i, '')
108
+ return {
109
+ npm: '@ai-sdk/openai-compatible',
110
+ name: source.name || providerKey,
111
+ options: { baseURL, apiKey: `{env:${envVarName}}` },
112
+ models: {},
113
+ }
114
+ }
115
+
101
116
  // ─── ZAI proxy bridge ─────────────────────────────────────────────────────────
102
117
 
103
118
  // 📖 createZaiProxy: Localhost reverse proxy bridging ZAI's non-standard API paths
@@ -434,7 +449,7 @@ export async function startOpenCode(model, fcmConfig) {
434
449
  config.provider.codestral = {
435
450
  npm: '@ai-sdk/openai-compatible',
436
451
  name: 'Mistral Codestral',
437
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
452
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
438
453
  models: {}
439
454
  }
440
455
  } else if (providerKey === 'hyperbolic') {
@@ -514,6 +529,9 @@ export async function startOpenCode(model, fcmConfig) {
514
529
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
515
530
  models: {}
516
531
  }
532
+ } else {
533
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
534
+ if (providerConfig) config.provider[providerKey] = providerConfig
517
535
  }
518
536
  }
519
537
 
@@ -799,7 +817,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
799
817
  config.provider.codestral = {
800
818
  npm: '@ai-sdk/openai-compatible',
801
819
  name: 'Mistral Codestral',
802
- options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:CODESTRAL_API_KEY}' },
820
+ options: { baseURL: 'https://api.mistral.ai/v1', apiKey: '{env:MISTRAL_API_KEY}' },
803
821
  models: {}
804
822
  }
805
823
  } else if (providerKey === 'hyperbolic') {
@@ -879,6 +897,9 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
879
897
  options: { baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1', apiKey: '{env:OVH_AI_ENDPOINTS_ACCESS_TOKEN}' },
880
898
  models: {}
881
899
  }
900
+ } else {
901
+ const providerConfig = buildOpenAiCompatibleProviderConfig(providerKey)
902
+ if (providerConfig) config.provider[providerKey] = providerConfig
882
903
  }
883
904
  }
884
905