free-coding-models 0.5.88 → 0.5.89

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/src/core/utils.js CHANGED
@@ -608,6 +608,13 @@ export function parseArgs(argv) {
608
608
  const daemonBackgroundMode = flags.includes('--daemon-bg')
609
609
  const daemonStopMode = flags.includes('--daemon-stop')
610
610
  const daemonStatusMode = flags.includes('--daemon-status')
611
+ // 📖 Router v2 (beta) lifecycle flags - run v2 alongside v1 on its own
612
+ // port (19380) with persisted breakers + decision traces. See
613
+ // src/core/router-v2/daemon.js and docs/router-v2.md.
614
+ const routerV2Mode = flags.includes('--router-v2')
615
+ const routerV2BackgroundMode = flags.includes('--router-v2-bg')
616
+ const routerV2StopMode = flags.includes('--router-v2-stop')
617
+ const routerV2StatusMode = flags.includes('--router-v2-status')
611
618
 
612
619
  // 📖 --fix-permissions / --yes / -y - auto-answer "yes" to the config-permission
613
620
  // 📖 security prompt (chmod 600, best-effort on Windows) so scripts, CI and
@@ -706,6 +713,11 @@ export function parseArgs(argv) {
706
713
  checkDriftMode,
707
714
  driftThreshold,
708
715
  daemonStatusMode,
716
+ // 📖 Router v2 (beta) lifecycle flags - see src/core/router-v2/daemon.js
717
+ routerV2Mode,
718
+ routerV2BackgroundMode,
719
+ routerV2StopMode,
720
+ routerV2StatusMode,
709
721
  // 📖 Profile system removed - API keys now persist permanently across all sessions
710
722
  recommendMode,
711
723
  devMode,
package/src/tui/app.js CHANGED
@@ -123,6 +123,7 @@ import { createOverlayRenderers } from './overlays.js'
123
123
  import { createKeyHandler, createMouseEventHandler } from './key-handler.js'
124
124
  import { createMouseHandler, containsMouseSequence } from './mouse.js'
125
125
  import { stopRouterDashboardClient } from '../core/router-dashboard.js'
126
+ import { stopRouterV2DashboardClient } from '../core/router-v2/tui-dashboard.js'
126
127
  import { getToolModeOrder, getToolMeta } from '../core/tool-metadata.js'
127
128
  import { startExternalTool } from '../core/tool-launchers.js'
128
129
  import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../core/tool-bootstrap.js'
@@ -758,6 +759,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
758
759
  clearTimeout(state.pingIntervalObj)
759
760
  clearInterval(state.versionRecheckTimer)
760
761
  stopRouterDashboardClient(state)
762
+ stopRouterV2DashboardClient(state)
761
763
  process.stdout.write(ALT_LEAVE)
762
764
  if (process.stdout.isTTY) {
763
765
  process.stdout.flush && process.stdout.flush()
@@ -782,6 +784,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
782
784
  clearTimeout(state.pingIntervalObj)
783
785
  clearInterval(state.versionRecheckTimer)
784
786
  stopRouterDashboardClient(state)
787
+ stopRouterV2DashboardClient(state)
785
788
  // 📖 Remove the actual registered wrappers (see keypressWrapper/mouseDataWrapper).
786
789
  if (keypressWrapper) process.stdin.removeListener('keypress', keypressWrapper)
787
790
  if (mouseDataWrapper) process.stdin.removeListener('data', mouseDataWrapper)
@@ -1118,6 +1121,8 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
1118
1121
  ? overlays.renderInstalledModels()
1119
1122
  : state.routerDashboardOpen
1120
1123
  ? overlays.renderRouterDashboard()
1124
+ : state.routerV2DashboardOpen
1125
+ ? overlays.renderRouterV2Dashboard()
1121
1126
  : state.playgroundOpen
1122
1127
  ? overlays.renderPlayground()
1123
1128
  : state.tokenUsageOpen
@@ -1261,9 +1266,9 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
1261
1266
  }
1262
1267
 
1263
1268
  state.results.forEach(r => {
1264
- // 📖 When router dashboard is open, ONLY ping favorites every second
1269
+ // 📖 When a router dashboard is open, ONLY ping favorites every second
1265
1270
  // 📖 to prevent massive rate limiting across the entire 90+ model catalog.
1266
- if (state.routerDashboardOpen) {
1271
+ if (state.routerDashboardOpen || state.routerV2DashboardOpen) {
1267
1272
  const favKey = `${r.providerKey}/${r.modelId}`
1268
1273
  if (!state.config.favorites.includes(favKey)) return
1269
1274
  }
@@ -47,6 +47,10 @@ const CONFIG_FLAGS = [
47
47
  { flag: '--daemon-bg', description: 'Start the FCM Router daemon in the background' },
48
48
  { flag: '--daemon-status', description: 'Print FCM Router daemon status JSON' },
49
49
  { flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
50
+ { flag: '--router-v2', description: 'Start the Router v2 daemon (BETA) on port 19380: content-validated failover + Anthropic /v1/messages' },
51
+ { flag: '--router-v2-bg', description: 'Start Router v2 (BETA) in the background' },
52
+ { flag: '--router-v2-status', description: 'Print Router v2 (BETA) daemon status JSON' },
53
+ { flag: '--router-v2-stop', description: 'Gracefully stop the Router v2 (BETA) daemon' },
50
54
  { flag: '--sync-set [name]', description: 'Auto-discover and live-probe models into a router set' },
51
55
  { flag: '--config-dir <dir>', description: 'Store config.json + backups/ in <dir> (e.g. --config-dir ~/.config/free-coding-models for the XDG layout)' },
52
56
  { flag: '--fix-permissions, --yes, -y', description: 'Auto-fix insecure config file permissions (chmod 600) without prompting; safe for scripts and CI' },
@@ -46,6 +46,15 @@ import {
46
46
  setDashboardNotice,
47
47
  toggleRouterDashboardProbePause,
48
48
  } from '../core/router-dashboard.js'
49
+ import {
50
+ closeRouterV2DashboardOverlay,
51
+ cycleRouterV2ProbeMode,
52
+ openRouterV2DashboardOverlay,
53
+ renderRouterV2Dashboard,
54
+ setRouterV2Notice,
55
+ testAllVisibleViaRouterV2,
56
+ testModelViaRouterV2,
57
+ } from '../core/router-v2/tui-dashboard.js'
49
58
  import {
50
59
  closePlaygroundOverlay,
51
60
  openPlaygroundOverlay,
@@ -82,8 +91,8 @@ function spawnDaemonCommand(state, args) {
82
91
  })
83
92
  child.unref()
84
93
 
85
- // 📖 Only capture stdout for start commands stop doesn't need error surfacing.
86
- if (!args.includes('--daemon-bg')) return
94
+ // 📖 Only capture stdout for start commands - stop doesn't need error surfacing.
95
+ if (!args.includes('--daemon-bg') && !args.includes('--router-v2-bg')) return
87
96
 
88
97
  let stdout = ''
89
98
  let stderr = ''
@@ -1502,6 +1511,7 @@ export function createKeyHandler(ctx) {
1502
1511
 
1503
1512
  case 'open-recommend': return openRecommendOverlay()
1504
1513
  case 'open-router-dashboard': return openRouterDashboardOverlay(state)
1514
+ case 'open-router-v2-dashboard': return openRouterV2DashboardOverlay(state)
1505
1515
  case 'open-playground': return openPlaygroundOverlay(state)
1506
1516
  case 'open-token-usage': return openTokenUsageOverlay()
1507
1517
  case 'open-runtime-report': return openRuntimeReportOverlay()
@@ -1774,6 +1784,82 @@ export function createKeyHandler(ctx) {
1774
1784
  return
1775
1785
  }
1776
1786
 
1787
+ // 📖 Router v2 (BETA) dashboard: ↑↓ navigate the fallback chain,
1788
+ // T tests the model under the cursor through the router (pinned request
1789
+ // through the FULL chain), S toggles the daemon, I cycles probe speed,
1790
+ // C clears the persisted history, Esc goes back.
1791
+ if (state.routerV2DashboardOpen) {
1792
+ const routingOrder = Array.isArray(state.routerV2Stats?.routingOrder) ? state.routerV2Stats.routingOrder : []
1793
+ const maxCursor = Math.max(0, routingOrder.length)
1794
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
1795
+
1796
+ if (key.name === 'escape') {
1797
+ closeRouterV2DashboardOverlay(state)
1798
+ return
1799
+ }
1800
+
1801
+ if (key.name === 's') {
1802
+ const isRunning = state.routerV2Status === 'ready' || state.routerV2Status === 'partial'
1803
+ state.routerV2Status = 'loading'
1804
+ spawnDaemonCommand(state, [isRunning ? '--daemon-stop' : '--daemon-bg'])
1805
+ return
1806
+ }
1807
+
1808
+ if (key.name === 'return' || key.name === 'enter') {
1809
+ if ((state.routerV2CursorIndex ?? 0) === maxCursor) {
1810
+ const isRunning = state.routerV2Status === 'ready' || state.routerV2Status === 'partial'
1811
+ state.routerV2Status = 'loading'
1812
+ spawnDaemonCommand(state, [isRunning ? '--daemon-stop' : '--daemon-bg'])
1813
+ }
1814
+ return
1815
+ }
1816
+
1817
+ if (key.name === 't') {
1818
+ const entry = routingOrder[state.routerV2CursorIndex ?? 0]
1819
+ if (entry?.key) void testModelViaRouterV2(state, entry.key)
1820
+ return
1821
+ }
1822
+
1823
+ if (key.name === 'up' || key.name === 'k') {
1824
+ state.routerV2CursorIndex = Math.max(0, (state.routerV2CursorIndex ?? 0) - 1)
1825
+ return
1826
+ }
1827
+ if (key.name === 'down' || key.name === 'j') {
1828
+ state.routerV2CursorIndex = Math.min(maxCursor, (state.routerV2CursorIndex ?? 0) + 1)
1829
+ return
1830
+ }
1831
+ if (key.name === 'pageup') {
1832
+ state.routerV2ScrollOffset = Math.max(0, (state.routerV2ScrollOffset || 0) - pageStep)
1833
+ return
1834
+ }
1835
+ if (key.name === 'pagedown') {
1836
+ state.routerV2ScrollOffset = (state.routerV2ScrollOffset || 0) + pageStep
1837
+ return
1838
+ }
1839
+ if (key.name === 'home') {
1840
+ state.routerV2CursorIndex = 0
1841
+ state.routerV2ScrollOffset = 0
1842
+ return
1843
+ }
1844
+ if (key.name === 'i') {
1845
+ try { await cycleRouterV2ProbeMode(state) } catch {}
1846
+ return
1847
+ }
1848
+ if (key.name === 'c') {
1849
+ void (async () => {
1850
+ try {
1851
+ if (state.routerV2BaseUrl) {
1852
+ await fetch(`${state.routerV2BaseUrl}/api/router-v2/history`, { method: 'DELETE' })
1853
+ state.routerV2History = null
1854
+ setRouterV2Notice(state, 'success', 'Request history cleared.')
1855
+ }
1856
+ } catch {}
1857
+ })()
1858
+ return
1859
+ }
1860
+ return
1861
+ }
1862
+
1777
1863
  // 📖 Playground overlay: handles Enter, Esc, Backspace, Ctrl+S, Ctrl+L,
1778
1864
  // 📖 PageUp/PageDown, and printable input itself. The dedicated handler
1779
1865
  // 📖 in core/playground.js owns the draft and submission state.
@@ -2926,6 +3012,16 @@ export function createKeyHandler(ctx) {
2926
3012
  return
2927
3013
  }
2928
3014
 
3015
+ // 📖 Shift+V: Open / close the Router v2 (BETA) dashboard.
3016
+ if (key.name === 'v' && key.shift && !key.ctrl && !key.meta) {
3017
+ if (state.routerV2DashboardOpen) {
3018
+ closeRouterV2DashboardOverlay(state)
3019
+ return
3020
+ }
3021
+ openRouterV2DashboardOverlay(state)
3022
+ return
3023
+ }
3024
+
2929
3025
  // 📖 Shift+T: open the Token Usage screen.
2930
3026
  if (key.name === 't' && key.shift && !key.ctrl && !key.meta) {
2931
3027
  openTokenUsageOverlay()
@@ -3070,6 +3166,25 @@ export function createKeyHandler(ctx) {
3070
3166
  return
3071
3167
  }
3072
3168
 
3169
+ // 📖 Ctrl+T: test the selected model THROUGH the Router v2 daemon (beta).
3170
+ // 📖 Unlike Ctrl+A (direct provider call), this exercises the full routing
3171
+ // chain: schema normalization, pre-prompt, content gate and the breaker
3172
+ // update. The pinned-model request runs with failover disabled.
3173
+ if (key.ctrl && key.name === 't' && !key.shift) {
3174
+ const selected = state.visibleSorted?.[state.cursor]
3175
+ if (selected?.providerKey && selected?.modelId) {
3176
+ void testModelViaRouterV2(state, `${selected.providerKey}/${selected.modelId}`)
3177
+ }
3178
+ return
3179
+ }
3180
+
3181
+ // 📖 Ctrl+Shift+T: test every visible configured model through Router v2,
3182
+ // results streaming into the Shift+V overlay as they land.
3183
+ if (key.ctrl && key.shift && key.name === 't') {
3184
+ void testAllVisibleViaRouterV2(state)
3185
+ return
3186
+ }
3187
+
3073
3188
  if (key.shift && key.name === 'up') {
3074
3189
  const selected = state.visibleSorted?.[state.cursor]
3075
3190
  if (selected?.isFavorite) {
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { buildCliHelpLines } from './cli-help.js'
23
23
  import { renderRouterDashboard as renderRouterDashboardOverlay } from '../core/router-dashboard.js'
24
+ import { renderRouterV2Dashboard as renderRouterV2DashboardOverlay } from '../core/router-v2/tui-dashboard.js'
24
25
  import { renderPlayground as renderPlaygroundOverlay } from '../core/playground.js'
25
26
  import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
26
27
  import { getProviderBillingNote, getProviderLabelWithBilling } from '../core/provider-metadata.js'
@@ -1016,6 +1017,8 @@ export function createOverlayRenderers(state, deps) {
1016
1017
  lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
1017
1018
  lines.push(` ${key('Ctrl+A')} AI Speed Test ${hint('(benchmark selected model → time + TPS)')}`)
1018
1019
  lines.push(` ${key('Ctrl+U')} Global AI Speed Test ${hint('(benchmark all models; Settings can auto-run it on startup)')}`)
1020
+ lines.push(` ${key('Ctrl+T')} Test selected model via Router v2 ${hint('(real request through the daemon: gate + failover included)')}`)
1021
+ lines.push(` ${key('Ctrl+Shift+T')} Test all visible models via Router v2 ${hint('(results stream into the Shift+V dashboard)')}`)
1019
1022
  lines.push(` ${key('Shift+P')} Re-probe failed rows ${hint('(retry only rows showing auth fail / 429 / 404 / timeout - not the whole list)')}`)
1020
1023
  lines.push(` ${key('Ctrl+Shift+P')} Probe All Models ${hint('(test all configured models; auto-hide broken 404/410)')}`)
1021
1024
  lines.push(` ${key('Space')} Expand selected row ${hint('(2-line detail: provider, endpoint, full model id; Space again or move to collapse)')}`)
@@ -1413,6 +1416,12 @@ export function createOverlayRenderers(state, deps) {
1413
1416
  return renderRouterDashboardOverlay(state, { LOCAL_VERSION })
1414
1417
  }
1415
1418
 
1419
+ // 📖 Router v2 (BETA) overlay (Shift+V): hardened failover daemon view with
1420
+ // request chains, DEGRADED states and pinned-model tests through the router.
1421
+ function renderRouterV2Dashboard() {
1422
+ return renderRouterV2DashboardOverlay(state, { LOCAL_VERSION })
1423
+ }
1424
+
1416
1425
  // ─── Playground overlay ───────────────────────────────────────────────────────
1417
1426
  // 📖 Renders the in-TUI chat playground when the user presses `;` or opens
1418
1427
  // 📖 it from the command palette. All chat traffic flows to the local
@@ -1648,6 +1657,7 @@ export function createOverlayRenderers(state, deps) {
1648
1657
  renderChangelog,
1649
1658
  renderInstalledModels,
1650
1659
  renderRouterDashboard,
1660
+ renderRouterV2Dashboard,
1651
1661
  renderPlayground,
1652
1662
  renderIncompatibleFallback,
1653
1663
  renderTokenUsage,
@@ -292,6 +292,28 @@ export function createTuiState({
292
292
  routerDashboardEverOpened: false,
293
293
  routerDashboardCursorIndex: 0,
294
294
 
295
+ // 📖 Router v2 (BETA) dashboard overlay (Shift+V). Talks to the parallel
296
+ // v2 daemon on port 19380: same defensive polling shape as v1 plus
297
+ // pinned-model test results and the persisted request-chain history.
298
+ routerV2DashboardOpen: false,
299
+ routerV2Status: 'idle',
300
+ routerV2BaseUrl: null,
301
+ routerV2Port: null,
302
+ routerV2Health: null,
303
+ routerV2Stats: null,
304
+ routerV2History: null,
305
+ routerV2Error: null,
306
+ routerV2ScrollOffset: 0,
307
+ routerV2CursorIndex: 0,
308
+ routerV2PollTimer: null,
309
+ routerV2EventAbort: null,
310
+ routerV2Notice: null,
311
+ routerV2NoticeTimer: null,
312
+ routerV2TestResults: new Map(),
313
+ routerV2TestRunning: new Set(),
314
+ routerV2BatchTest: null,
315
+ routerV2LastEventAt: null,
316
+
295
317
  // 📖 Custom text filter (Ctrl+P → type text → Enter). Ephemeral — not saved to config.
296
318
  customTextFilter: null,
297
319