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/README.md +6 -0
- package/bin/free-coding-models.js +33 -2
- package/changelog/v0.5.89.md +20 -0
- package/package.json +2 -2
- package/src/core/router-daemon.js +1129 -500
- package/src/core/router-v2/anthropic-compat.js +473 -0
- package/src/core/router-v2/bench.js +171 -0
- package/src/core/router-v2/breaker-store.js +265 -0
- package/src/core/router-v2/constants.js +108 -0
- package/src/core/router-v2/decision-trace.js +134 -0
- package/src/core/router-v2/failure-classifier.js +231 -0
- package/src/core/router-v2/request-history.js +137 -0
- package/src/core/router-v2/response-gate.js +175 -0
- package/src/core/router-v2/tui-dashboard.js +632 -0
- package/src/core/schema-normalizer.js +23 -6
- package/src/core/utils.js +12 -0
- package/src/tui/app.js +7 -2
- package/src/tui/cli-help.js +4 -0
- package/src/tui/key-handler.js +117 -2
- package/src/tui/overlays.js +10 -0
- package/src/tui/tui-state.js +22 -0
- package/web/dist/assets/index-C5hgQLYN.js +48 -0
- package/web/dist/assets/index-CCaxIOti.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +82 -0
- package/web/dist/assets/index-CAzFIt8P.css +0 -1
- package/web/dist/assets/index-DFg1h0Nd.js +0 -44
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
|
}
|
package/src/tui/cli-help.js
CHANGED
|
@@ -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' },
|
package/src/tui/key-handler.js
CHANGED
|
@@ -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
|
|
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) {
|
package/src/tui/overlays.js
CHANGED
|
@@ -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,
|
package/src/tui/tui-state.js
CHANGED
|
@@ -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
|
|