free-coding-models 0.3.60 → 0.3.63

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/CHANGELOG.md CHANGED
@@ -1,10 +1,20 @@
1
- ## [0.3.58] - 2026-05-04
1
+ ## [0.3.63] - 2026-05-05
2
+
3
+ ### Fixed
4
+
5
+ - **Router daemon auth error false positives** — Fixed `health.every([])` returning `true` for empty arrays, which caused all models to be incorrectly marked as AUTH_ERROR when the daemon had no health data yet. Added explicit `health.length > 0` guards before `.every()` checks.
6
+ - **Daemon API key priority** — `getApiKeyForProvider` no longer falls back to shell env vars when a config key exists. The daemon now exclusively uses keys from `~/.free-coding-models.json`, preventing test/fake keys in shell env from overriding real configured keys.
2
7
 
3
8
  ### Changed
4
9
 
5
- - **E key filter ("Working only") now also hides `noauth` and `auth_error` models** Previously E only filtered by missing API key. Now it also filters out models whose provider returned a 401/403 auth rejection, while still keeping `timeout` and `429` (rate-limited) models visible.
6
- - **Google AI Studio renamed to Google AI** — Shortened to avoid column overflow in the provider column.
10
+ - **Shift+R now launches OpenCode** Pressing Shift+R opens the Router Dashboard AND launches OpenCode with the currently selected model from the main table. If dashboard is already open, just resets scroll. Previously Shift+R only opened the dashboard without launching anything.
11
+ - **Favorites sync to router on launch** — When a model is launched from the TUI, it and the user's full favorites chain are synced to the daemon as the active set via `/sets/fast-coding`.
12
+ - **Router Dashboard install flow** — The "Install Router Endpoint to CLI Tool" button now opens the Install Endpoints overlay directly with `fcm_router` pre-selected, skipping the provider selection phase.
7
13
 
8
- ### Fixed
14
+ ### Added
15
+
16
+ - **README_ROUTER.md** — New comprehensive documentation for the FCM Router daemon, covering setup, endpoints, routing behavior, and tool configuration.
17
+
18
+ ### Changed (general)
9
19
 
10
- - **NVIDIA NIM column label** — The provider column in the TUI table now shows `NVIDIA NIM` instead of `NIM` to match the official provider branding.
20
+ - **CTX column gradient improved** — Context window colorization now goes from red (32k) orange (64k) yellow (128k) green (256k) → cyan/teal fluo (400k) → bold cyan+underline (1M+) so the biggest context windows stand out visually.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.60",
3
+ "version": "0.3.63",
4
4
  "description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",
package/src/app.js CHANGED
@@ -111,7 +111,7 @@ import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getP
111
111
  import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
112
112
  import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
113
113
  import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry } from '../src/telemetry.js'
114
- import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite } from '../src/favorites.js'
114
+ import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite, pruneOrphanedFavorites } from '../src/favorites.js'
115
115
  import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification, fetchLastReleaseDate } from './updater.js'
116
116
  import { promptApiKey } from '../src/setup.js'
117
117
  import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
@@ -372,6 +372,9 @@ export async function runApp(cliArgs, config) {
372
372
  hidden: false, // 📖 Simple flag to hide/show models
373
373
  }))
374
374
  syncFavoriteFlags(results, config)
375
+ // 📖 Garbage-collect favorites that reference models no longer in sources.js,
376
+ // 📖 so the router dashboard only shows real, launchable models.
377
+ pruneOrphanedFavorites(results, config)
375
378
 
376
379
  // 📖 Load usage data from token-stats.json and attach usagePercent to each result row.
377
380
  // 📖 usagePercent is the quota percent remaining (0–100). undefined = no data available.
@@ -544,6 +547,7 @@ export async function runApp(cliArgs, config) {
544
547
  routerDashboardNoticeTimer: null,
545
548
  routerOnboardingScrollOffset: 0,
546
549
  routerDashboardEverOpened: false, // 📖 Set to true the first time dashboard opens (used for upgrade-path telemetry)
550
+ routerDashboardCursorIndex: 0, // 📖 Cursor index for navigating the favorites list in router dashboard
547
551
  // 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
548
552
  customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
549
553
  }
@@ -578,7 +582,8 @@ export async function runApp(cliArgs, config) {
578
582
  const scheduleNextPing = () => {
579
583
  clearTimeout(state.pingIntervalObj)
580
584
  const elapsed = Date.now() - state.lastPingTime
581
- const delay = Math.max(0, state.pingInterval - elapsed)
585
+ const interval = state.routerDashboardOpen ? 1000 : state.pingInterval
586
+ const delay = Math.max(0, interval - elapsed)
582
587
  state.pingIntervalObj = setTimeout(runPingCycle, delay)
583
588
  }
584
589
 
@@ -1223,6 +1228,13 @@ if (unconfiguredHide) {
1223
1228
  }
1224
1229
 
1225
1230
  state.results.forEach(r => {
1231
+ // 📖 When router dashboard is open, ONLY ping favorites every second
1232
+ // 📖 to prevent massive rate limiting across the entire 90+ model catalog.
1233
+ if (state.routerDashboardOpen) {
1234
+ const favKey = `${r.providerKey}/${r.modelId}`
1235
+ if (!state.config.favorites.includes(favKey)) return
1236
+ }
1237
+
1226
1238
  pingModel(r).catch(() => {
1227
1239
  // Individual ping failures don't crash the loop
1228
1240
  })
@@ -136,10 +136,12 @@ function getManagedProviderId(providerKey) {
136
136
  }
137
137
 
138
138
  function getProviderLabel(providerKey) {
139
+ if (providerKey === 'fcm_router') return 'Smart Router Daemon'
139
140
  return PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
140
141
  }
141
142
 
142
143
  function getManagedProviderLabel(providerKey) {
144
+ if (providerKey === 'fcm_router') return 'FCM Smart Router'
143
145
  return `FCM ${getProviderLabel(providerKey)}`
144
146
  }
145
147
 
@@ -157,6 +159,10 @@ function getDefaultMaxTokens(contextWindow) {
157
159
  }
158
160
 
159
161
  function resolveProviderBaseUrl(providerKey) {
162
+ if (providerKey === 'fcm_router') {
163
+ return `http://localhost:${process.env.FCM_ROUTER_PORT || '19280'}/v1`
164
+ }
165
+
160
166
  const providerUrl = sources[providerKey]?.url
161
167
  if (!providerUrl) return null
162
168
 
@@ -173,6 +179,9 @@ function resolveProviderBaseUrl(providerKey) {
173
179
  }
174
180
 
175
181
  function resolveGooseBaseUrl(providerKey) {
182
+ if (providerKey === 'fcm_router') {
183
+ return `http://localhost:${process.env.FCM_ROUTER_PORT || '19280'}/v1`
184
+ }
176
185
  const providerUrl = sources[providerKey]?.url
177
186
  if (!providerUrl) return null
178
187
  if (providerKey === 'cloudflare') {
@@ -184,6 +193,7 @@ function resolveGooseBaseUrl(providerKey) {
184
193
  }
185
194
 
186
195
  function getDirectInstallSupport(providerKey) {
196
+ if (providerKey === 'fcm_router') return { supported: true, reason: null }
187
197
  if (!sources[providerKey]) {
188
198
  return { supported: false, reason: 'Unknown provider' }
189
199
  }
@@ -220,6 +230,12 @@ function buildCatalogModel(modelId, label, tier, sweScore, ctx) {
220
230
  }
221
231
 
222
232
  export function getProviderCatalogModels(providerKey) {
233
+ if (providerKey === 'fcm_router') {
234
+ return [
235
+ buildCatalogModel('fcm', 'FCM Smart Router', 'S+', 100, '200k')
236
+ ]
237
+ }
238
+
223
239
  const seen = new Set()
224
240
  return MODELS
225
241
  .filter((entry) => entry[5] === providerKey)
@@ -252,6 +268,7 @@ export function getInstallTargetModes() {
252
268
  }
253
269
 
254
270
  function requireConfiguredProviderKey(config, providerKey) {
271
+ if (providerKey === 'fcm_router') return 'fcm-local'
255
272
  const apiKey = getApiKey(config, providerKey)
256
273
  if (!apiKey) {
257
274
  throw new Error(`No configured API key found for ${getProviderLabel(providerKey)}`)
package/src/favorites.js CHANGED
@@ -21,9 +21,10 @@
21
21
  * → toFavoriteKey(providerKey, modelId) — Build the canonical "providerKey/modelId" string
22
22
  * → syncFavoriteFlags(results, config) — Attach isFavorite/favoriteRank to result rows
23
23
  * → toggleFavoriteModel(config, providerKey, modelId) — Add/remove favorite and persist
24
+ * → pruneOrphanedFavorites(results, config) — Remove favorites referencing models no longer in sources
24
25
  *
25
26
  * @exports
26
- * ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel
27
+ * ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, pruneOrphanedFavorites
27
28
  *
28
29
  * @see src/config.js — load/save helpers keep favorite persistence atomic and merge-safe
29
30
  * @see bin/free-coding-models.js — calls syncFavoriteFlags on startup and toggleFavoriteModel on F key
@@ -125,3 +126,23 @@ export function reorderFavorite(config, providerKey, modelId, direction) {
125
126
  if (saveResult.success) replaceConfigContents(config, latestConfig)
126
127
  return true
127
128
  }
129
+
130
+ /**
131
+ * 📖 Remove favorites that reference models no longer present in the active sources.
132
+ * 📖 Called once at startup so the router dashboard does not show stale/removed models.
133
+ * 📖 Persists immediately if any orphaned entries are found.
134
+ * @param {Array<Record<string, unknown>>} results — the full result rows from sources
135
+ * @param {Record<string, unknown>} config
136
+ * @returns {number} count of removed orphaned entries
137
+ */
138
+ export function pruneOrphanedFavorites(results, config) {
139
+ ensureFavoritesConfig(config)
140
+ const validKeys = new Set(results.map(r => toFavoriteKey(r.providerKey, r.modelId)))
141
+ const before = config.favorites.length
142
+ config.favorites = config.favorites.filter(key => validKeys.has(key))
143
+ const removed = before - config.favorites.length
144
+ if (removed > 0) {
145
+ saveConfig(config, { replaceFavorites: true })
146
+ }
147
+ return removed
148
+ }
@@ -269,6 +269,7 @@ export function createKeyHandler(ctx) {
269
269
  installProviderEndpoints,
270
270
  syncFavoriteFlags,
271
271
  toggleFavoriteModel,
272
+ reorderFavorite,
272
273
  sortResultsWithPinnedFavorites,
273
274
  adjustScrollOffset,
274
275
  applyTierFilter,
@@ -1434,22 +1435,108 @@ export function createKeyHandler(ctx) {
1434
1435
 
1435
1436
  // 📖 Profile system removed - API keys now persist permanently across all sessions
1436
1437
 
1437
- // 📖 Router Dashboard captures local dashboard controls while open so keys
1438
- // 📖 like S/I/R/C do not leak through to sort/filter actions in the table.
1438
+ // 📖 Router Dashboard: ↑↓ navigate favorites list, Ctrl+↑↓ reorder,
1439
+ // 📖 I cycles health check speed, C clears log, Esc goes back.
1439
1440
  if (state.routerDashboardOpen) {
1440
1441
  if (key.ctrl && key.name === 'c') { exit(0); return }
1442
+ const favorites = Array.isArray(state.config?.favorites) ? state.config.favorites : []
1443
+ // 📖 maxCursor accounts for the favorites list + 2 buttons (Start/Stop Daemon and Install Endpoint)
1444
+ const maxCursor = Math.max(0, favorites.length + 1)
1441
1445
  const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
1442
1446
 
1443
1447
  if (key.name === 'escape') {
1444
1448
  closeRouterDashboardOverlay(state)
1445
1449
  return
1446
1450
  }
1451
+
1452
+ // 📖 Shift+↑: move the selected favorite UP in fallback priority
1453
+ if (key.shift && key.name === 'up') {
1454
+ const cursorIdx = state.routerDashboardCursorIndex ?? 0
1455
+ if (favorites.length > 0 && cursorIdx < favorites.length) {
1456
+ const favKey = favorites[cursorIdx]
1457
+ if (favKey) {
1458
+ const slashIdx = favKey.indexOf('/')
1459
+ const providerKey = slashIdx >= 0 ? favKey.slice(0, slashIdx) : favKey
1460
+ const modelId = slashIdx >= 0 ? favKey.slice(slashIdx + 1) : favKey
1461
+ const moved = reorderFavorite(state.config, providerKey, modelId, 'up')
1462
+ if (moved) {
1463
+ state.routerDashboardCursorIndex = Math.max(0, cursorIdx - 1)
1464
+ syncFavoriteFlags(state.results, state.config)
1465
+ }
1466
+ }
1467
+ }
1468
+ return
1469
+ }
1470
+
1471
+ // 📖 Shift+↓: move the selected favorite DOWN in fallback priority
1472
+ if (key.shift && key.name === 'down') {
1473
+ const cursorIdx = state.routerDashboardCursorIndex ?? 0
1474
+ if (favorites.length > 0 && cursorIdx < favorites.length) {
1475
+ const favKey = favorites[cursorIdx]
1476
+ if (favKey) {
1477
+ const slashIdx = favKey.indexOf('/')
1478
+ const providerKey = slashIdx >= 0 ? favKey.slice(0, slashIdx) : favKey
1479
+ const modelId = slashIdx >= 0 ? favKey.slice(slashIdx + 1) : favKey
1480
+ const moved = reorderFavorite(state.config, providerKey, modelId, 'down')
1481
+ if (moved) {
1482
+ state.routerDashboardCursorIndex = Math.min(favorites.length - 1, cursorIdx + 1)
1483
+ syncFavoriteFlags(state.results, state.config)
1484
+ }
1485
+ }
1486
+ }
1487
+ return
1488
+ }
1489
+
1490
+ // 📖 S: Toggle daemon start/stop
1491
+ if (key.name === 's') {
1492
+ const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
1493
+ const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
1494
+ const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
1495
+
1496
+ state.routerDashboardStatus = 'loading'
1497
+ const child = spawn('node', [binPath, ...args], {
1498
+ detached: true,
1499
+ stdio: 'ignore',
1500
+ })
1501
+ child.unref()
1502
+ return
1503
+ }
1504
+
1505
+ // 📖 Enter/Return: Toggle daemon or open Install Endpoints if cursor is on a button
1506
+ if (key.name === 'return' || key.name === 'enter') {
1507
+ const btnCursor = favorites.length
1508
+ const installBtnCursor = favorites.length + 1
1509
+
1510
+ if ((state.routerDashboardCursorIndex ?? 0) === btnCursor) {
1511
+ const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
1512
+ const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
1513
+ const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
1514
+
1515
+ state.routerDashboardStatus = 'loading'
1516
+ const child = spawn('node', [binPath, ...args], {
1517
+ detached: true,
1518
+ stdio: 'ignore',
1519
+ })
1520
+ child.unref()
1521
+ } else if ((state.routerDashboardCursorIndex ?? 0) === installBtnCursor) {
1522
+ state.routerDashboardOpen = false
1523
+ state.installEndpointsOpen = true
1524
+ state.installEndpointsPhase = 'tools' // skip the provider selection phase
1525
+ state.installEndpointsCursor = 0
1526
+ state.installEndpointsProviderKey = 'fcm_router' // special provider key handled by endpoint-installer
1527
+ state.installEndpointsScrollOffset = 0
1528
+ state.installEndpointsErrorMsg = null
1529
+ }
1530
+ return
1531
+ }
1532
+
1533
+ // 📖 ↑/↓: navigate the favorites list cursor
1447
1534
  if (key.name === 'up' || key.name === 'k') {
1448
- state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - 1)
1535
+ state.routerDashboardCursorIndex = Math.max(0, (state.routerDashboardCursorIndex ?? 0) - 1)
1449
1536
  return
1450
1537
  }
1451
1538
  if (key.name === 'down' || key.name === 'j') {
1452
- state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + 1
1539
+ state.routerDashboardCursorIndex = Math.min(maxCursor, (state.routerDashboardCursorIndex ?? 0) + 1)
1453
1540
  return
1454
1541
  }
1455
1542
  if (key.name === 'pageup') {
@@ -1461,29 +1548,18 @@ export function createKeyHandler(ctx) {
1461
1548
  return
1462
1549
  }
1463
1550
  if (key.name === 'home') {
1551
+ state.routerDashboardCursorIndex = 0
1464
1552
  state.routerDashboardScrollOffset = 0
1465
1553
  return
1466
1554
  }
1467
- if (key.name === 's') {
1468
- try { await cycleRouterDashboardActiveSet(state) } catch {}
1469
- return
1470
- }
1471
1555
  if (key.name === 'i') {
1472
1556
  try { await cycleRouterDashboardProbeMode(state) } catch {}
1473
1557
  return
1474
1558
  }
1475
- if (key.name === 'r') {
1476
- restartRouterDashboardDaemon(state)
1477
- return
1478
- }
1479
1559
  if (key.name === 'c') {
1480
1560
  clearRouterDashboardRequestLog(state)
1481
1561
  return
1482
1562
  }
1483
- if (key.name === 'p') {
1484
- toggleRouterDashboardProbePause(state)
1485
- return
1486
- }
1487
1563
  return
1488
1564
  }
1489
1565
 
@@ -1492,7 +1568,7 @@ export function createKeyHandler(ctx) {
1492
1568
  if (key.ctrl && key.name === 'c') { exit(0); return }
1493
1569
 
1494
1570
  const providerChoices = getConfiguredInstallableProviders(state.config)
1495
- const toolChoices = getInstallTargetModes()
1571
+ const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
1496
1572
  const modelChoices = state.installEndpointsProviderKey
1497
1573
  ? getProviderCatalogModels(state.installEndpointsProviderKey)
1498
1574
  : []
@@ -1534,10 +1610,19 @@ export function createKeyHandler(ctx) {
1534
1610
  if (key.name === 'escape') {
1535
1611
  state.installEndpointsErrorMsg = null
1536
1612
  if (state.installEndpointsPhase === 'providers' || state.installEndpointsPhase === 'result') {
1613
+ const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
1537
1614
  resetInstallEndpointsOverlay()
1615
+ if (wasFcmRouter) {
1616
+ state.routerDashboardOpen = true
1617
+ }
1538
1618
  return
1539
1619
  }
1540
1620
  if (state.installEndpointsPhase === 'tools') {
1621
+ if (state.installEndpointsProviderKey === 'fcm_router') {
1622
+ resetInstallEndpointsOverlay()
1623
+ state.routerDashboardOpen = true
1624
+ return
1625
+ }
1541
1626
  state.installEndpointsPhase = 'providers'
1542
1627
  state.installEndpointsCursor = 0
1543
1628
  state.installEndpointsScrollOffset = 0
@@ -1582,10 +1667,25 @@ export function createKeyHandler(ctx) {
1582
1667
  if (!selectedToolMode) return
1583
1668
  state.installEndpointsToolMode = selectedToolMode
1584
1669
  state.installEndpointsConnectionMode = 'direct'
1585
- state.installEndpointsPhase = 'scope'
1586
- state.installEndpointsCursor = 0
1587
- state.installEndpointsScrollOffset = 0
1588
- state.installEndpointsErrorMsg = null
1670
+
1671
+ if (state.installEndpointsProviderKey === 'fcm_router') {
1672
+ state.installEndpointsScope = 'all'
1673
+ try {
1674
+ await runInstallEndpointsFlow()
1675
+ } catch (error) {
1676
+ state.installEndpointsResult = {
1677
+ type: 'error',
1678
+ title: 'Install failed',
1679
+ lines: [error instanceof Error ? error.message : String(error)],
1680
+ }
1681
+ state.installEndpointsPhase = 'result'
1682
+ }
1683
+ } else {
1684
+ state.installEndpointsPhase = 'scope'
1685
+ state.installEndpointsCursor = 0
1686
+ state.installEndpointsScrollOffset = 0
1687
+ state.installEndpointsErrorMsg = null
1688
+ }
1589
1689
  }
1590
1690
  return
1591
1691
  }
@@ -1660,7 +1760,11 @@ export function createKeyHandler(ctx) {
1660
1760
 
1661
1761
  if (state.installEndpointsPhase === 'result') {
1662
1762
  if (key.name === 'return' || key.name === 'y') {
1763
+ const wasFcmRouter = state.installEndpointsProviderKey === 'fcm_router'
1663
1764
  resetInstallEndpointsOverlay()
1765
+ if (wasFcmRouter) {
1766
+ state.routerDashboardOpen = true
1767
+ }
1664
1768
  }
1665
1769
  return
1666
1770
  }
@@ -2558,10 +2662,26 @@ export function createKeyHandler(ctx) {
2558
2662
 
2559
2663
  // 📖 Profile system removed - API keys now persist permanently across all sessions
2560
2664
 
2561
- // 📖 Shift+R intentionally stays unadvertised in the main UI, but remains
2562
- // 📖 available as a tester entry point for the Router Dashboard.
2665
+ // 📖 Shift+R: Open Router Dashboard AND launch OpenCode with the selected model.
2666
+ // 📖 If the dashboard is already open, just bring it to front.
2563
2667
  if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
2668
+ if (state.routerDashboardOpen) {
2669
+ state.routerDashboardScrollOffset = 0
2670
+ return
2671
+ }
2564
2672
  openRouterDashboardOverlay(state)
2673
+ // 📖 If a model is selected in the main table, launch OpenCode with it after opening dashboard
2674
+ const selected = state.visibleSorted?.[state.cursor]
2675
+ if (selected && selected.providerKey && selected.modelId) {
2676
+ const launchModel = {
2677
+ modelId: selected.modelId,
2678
+ label: selected.label,
2679
+ tier: selected.tier,
2680
+ providerKey: selected.providerKey,
2681
+ }
2682
+ // 📖 Launch asynchronously — don't await, dashboard renders while OpenCode starts
2683
+ void startOpenCode(launchModel, state.config)
2684
+ }
2565
2685
  return
2566
2686
  }
2567
2687
 
package/src/overlays.js CHANGED
@@ -343,7 +343,7 @@ export function createOverlayRenderers(state, deps) {
343
343
  const lines = []
344
344
  const cursorLineByRow = {}
345
345
  const providerChoices = getConfiguredInstallableProviders(state.config)
346
- const toolChoices = getInstallTargetModes()
346
+ const toolChoices = getInstallTargetModes().filter(t => !(state.installEndpointsProviderKey === 'fcm_router' && t === 'fcm_router'))
347
347
  const totalSteps = 4
348
348
  const scopeChoices = [
349
349
  {
@@ -357,9 +357,11 @@ export function createOverlayRenderers(state, deps) {
357
357
  hint: 'Choose a smaller curated subset for a cleaner model picker.',
358
358
  },
359
359
  ]
360
- const selectedProviderLabel = state.installEndpointsProviderKey
361
- ? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
362
- : '—'
360
+ const selectedProviderLabel = state.installEndpointsProviderKey === 'fcm_router'
361
+ ? 'Smart Router Daemon'
362
+ : state.installEndpointsProviderKey
363
+ ? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
364
+ : '—'
363
365
 
364
366
  // 📖 Resolve tool label from metadata instead of hard-coded switch
365
367
  const selectedToolLabel = state.installEndpointsToolMode
@@ -503,9 +503,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
503
503
  ? themeColors.metricWarn(ctxRaw.padEnd(W_CTX))
504
504
  : numK <= 128
505
505
  ? chalk.rgb(200, 180, 50).bold(ctxRaw.padEnd(W_CTX))
506
- : numK <= 200
506
+ : numK <= 256
507
507
  ? chalk.rgb(100, 200, 80).bold(ctxRaw.padEnd(W_CTX))
508
- : themeColors.metricGood(ctxRaw.padEnd(W_CTX))
508
+ : numK <= 400
509
+ ? chalk.rgb(0, 255, 200).bold(ctxRaw.padEnd(W_CTX))
510
+ : chalk.rgb(0, 255, 255).bold.underline(ctxRaw.padEnd(W_CTX))
509
511
  } else {
510
512
  ctxCell = themeColors.dim(ctxRaw.padEnd(W_CTX))
511
513
  }
@@ -823,7 +825,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
823
825
  const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
824
826
  lines.push(
825
827
  ' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
826
- chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Support me by following me on X ! @vavanessadev\x1b]8;;\x1b\\')
828
+ chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Follow @vavanessadev on X for updates and support\x1b]8;;\x1b\\')
827
829
  )
828
830
 
829
831
  if (versionStatus.isOutdated) {
@@ -637,7 +637,8 @@ class RouterRuntime {
637
637
  reloadConfigFromDisk() {
638
638
  try {
639
639
  const nextConfig = loadConfig()
640
- if (!nextConfig.router) nextConfig.router = this.routerConfig()
640
+ // 📖 Always rebuild the router set from favorites so UI toggles apply dynamically
641
+ ensureRouterConfigForDaemon(nextConfig, true)
641
642
  this.config = nextConfig
642
643
  this.refreshRouteState()
643
644
  this.scheduleProbeLoop()
@@ -649,12 +650,10 @@ class RouterRuntime {
649
650
  }
650
651
 
651
652
  getApiKeyForProvider(providerKey) {
652
- // 📖 Router background startup should work without inherited shell env, so
653
- // 📖 config keys are primary. Env is only a fallback for headless sessions.
654
653
  const configured = this.config?.apiKeys?.[providerKey]
655
654
  if (Array.isArray(configured)) return configured.find(Boolean) || null
656
655
  if (typeof configured === 'string' && configured.trim()) return configured.trim()
657
- return getApiKey({ apiKeys: {}, providers: {} }, providerKey)
656
+ return null
658
657
  }
659
658
 
660
659
  getSet(setName = null) {
@@ -1052,15 +1051,18 @@ class RouterRuntime {
1052
1051
  let errorCode = 'all_models_unavailable'
1053
1052
  let errorType = 'service_unavailable'
1054
1053
  if (health.length > 0) {
1055
- if (health.every((h) => h.state === 'AUTH_ERROR')) {
1054
+ const allAuthError = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR')
1055
+ const allAuthOrQuota = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR' || quotaExhausted.includes(h.key))
1056
+ const allStaleOrUnsupported = health.every((h) => h.state === 'STALE' || h.state === 'UNSUPPORTED')
1057
+ if (allAuthError) {
1056
1058
  statusCode = 401
1057
1059
  errorCode = 'invalid_api_key'
1058
1060
  errorType = 'invalid_request_error'
1059
- } else if (health.every((h) => h.state === 'AUTH_ERROR' || quotaExhausted.includes(h.key))) {
1061
+ } else if (allAuthOrQuota) {
1060
1062
  statusCode = 429
1061
1063
  errorCode = 'insufficient_quota'
1062
1064
  errorType = 'insufficient_quota'
1063
- } else if (health.every((h) => h.state === 'STALE' || h.state === 'UNSUPPORTED')) {
1065
+ } else if (allStaleOrUnsupported) {
1064
1066
  statusCode = 400
1065
1067
  errorCode = 'invalid_model'
1066
1068
  errorType = 'invalid_request_error'
@@ -1808,7 +1810,7 @@ export function createRouterRuntimeForTest({ config, port = 0, logger = null, to
1808
1810
  })
1809
1811
  }
1810
1812
 
1811
- function ensureRouterConfigForDaemon(config) {
1813
+ function ensureRouterConfigForDaemon(config, skipSave = false) {
1812
1814
  // 📖 Always rebuild from favorites or defaults — no more manual set management
1813
1815
  const favSet = buildRouterSetFromFavorites(config)
1814
1816
  const activeSet = favSet || buildDefaultRouterSet(config)
@@ -1819,7 +1821,7 @@ function ensureRouterConfigForDaemon(config) {
1819
1821
  activeSet: activeSet.name,
1820
1822
  sets: { [activeSet.name]: activeSet },
1821
1823
  })
1822
- saveConfig(config)
1824
+ if (!skipSave) saveConfig(config)
1823
1825
  return config.router
1824
1826
  }
1825
1827