free-coding-models 0.5.0 → 0.5.1
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 +9 -1
- package/bin/free-coding-models.js +10 -0
- package/changelog/v0.5.1.md +24 -0
- package/package.json +7 -2
- package/src/core/router-daemon.js +166 -1
- package/src/core/utils.js +2 -0
- package/src/tui/cli-help.js +2 -0
- package/src/tui/render-table.js +1 -1
- package/web/README.md +8 -5
- package/web/dist/assets/index-ByGf4Kq-.js +14 -0
- package/web/dist/assets/index-Ds7wmHBv.css +1 -0
- package/web/dist/index.html +3 -6
- package/web/index.html +1 -4
- package/web/package.json +11 -0
- package/web/server.js +606 -211
- package/web/src/App.jsx +54 -12
- package/web/src/components/analytics/AnalyticsView.jsx +10 -4
- package/web/src/components/atoms/AILatencyCell.jsx +38 -0
- package/web/src/components/atoms/AILatencyCell.module.css +43 -0
- package/web/src/components/atoms/HealthCell.jsx +53 -0
- package/web/src/components/atoms/HealthCell.module.css +15 -0
- package/web/src/components/atoms/LastPingCell.jsx +35 -0
- package/web/src/components/atoms/LastPingCell.module.css +35 -0
- package/web/src/components/atoms/MoodCell.jsx +25 -0
- package/web/src/components/atoms/MoodCell.module.css +6 -0
- package/web/src/components/atoms/RankCell.jsx +9 -0
- package/web/src/components/atoms/RankCell.module.css +9 -0
- package/web/src/components/atoms/TPSCell.jsx +36 -0
- package/web/src/components/atoms/TPSCell.module.css +38 -0
- package/web/src/components/atoms/VerdictBadge.jsx +30 -7
- package/web/src/components/atoms/VerdictBadge.module.css +24 -15
- package/web/src/components/dashboard/ExportModal.jsx +9 -4
- package/web/src/components/dashboard/FilterBar.jsx +112 -10
- package/web/src/components/dashboard/FilterBar.module.css +86 -1
- package/web/src/components/dashboard/ModelTable.jsx +293 -52
- package/web/src/components/dashboard/ModelTable.module.css +131 -33
- package/web/src/components/dashboard/StatsBar.jsx +7 -5
- package/web/src/components/layout/Footer.jsx +1 -1
- package/web/src/components/layout/Header.jsx +43 -9
- package/web/src/components/layout/Header.module.css +38 -4
- package/web/src/components/layout/Sidebar.jsx +19 -11
- package/web/src/components/layout/Sidebar.module.css +15 -5
- package/web/src/components/settings/SettingsView.jsx +24 -6
- package/web/src/components/settings/SettingsView.module.css +0 -1
- package/web/src/global.css +70 -73
- package/web/src/hooks/useFilter.js +117 -25
- package/web/src/hooks/useSSE.js +33 -9
- package/web/src/hooks/useSocket.js +200 -0
- package/web/vite.config.js +41 -0
- package/web/dist/assets/index-CGN-0_A0.css +0 -1
- package/web/dist/assets/index-CvMUM9Jr.js +0 -11
package/README.md
CHANGED
|
@@ -145,7 +145,15 @@ docker run -p 19280:19280 ghcr.io/vava-nessa/free-coding-models:latest
|
|
|
145
145
|
docker run -p 19280:19280 -e OPENROUTER_API_KEY=your_key ghcr.io/vava-nessa/free-coding-models:latest
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
Access the web dashboard at `http://localhost:19280/` and configure your coding tool to use `http://localhost:19280/v1` with model `fcm`.
|
|
148
|
+
Access the daemon web dashboard at `http://localhost:19280/` and configure your coding tool to use `http://localhost:19280/v1` with model `fcm`.
|
|
149
|
+
|
|
150
|
+
For the full TUI-style catalog dashboard from an npm install, run:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
free-coding-models web
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This starts the realtime Web Dashboard locally, opens it in your browser, and uses `http://localhost:3333/` by default. Override the port with `FCM_WEB_PORT=3334 free-coding-models web`.
|
|
149
157
|
|
|
150
158
|
### Available Image Tags
|
|
151
159
|
|
|
@@ -53,6 +53,16 @@ async function main() {
|
|
|
53
53
|
process.exit(0);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// 📖 Standalone web dashboard: same full-catalog ping UI as the TUI, served
|
|
57
|
+
// 📖 locally with Socket.IO/SSE/REST realtime updates.
|
|
58
|
+
if (cliArgs.webMode) {
|
|
59
|
+
const { startWebServer } = await import('../web/server.js');
|
|
60
|
+
const parsedPort = Number.parseInt(process.env.FCM_WEB_PORT || process.env.FCM_PORT || '3333', 10);
|
|
61
|
+
const port = Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : 3333;
|
|
62
|
+
await startWebServer(port, { open: true, startPingLoop: true });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
// 📖 Router daemon lifecycle flags run before the TUI so automation and
|
|
57
67
|
// 📖 editor integrations can manage the local OpenAI-compatible endpoint.
|
|
58
68
|
if (cliArgs.daemonMode || cliArgs.daemonBackgroundMode || cliArgs.daemonStopMode || cliArgs.daemonStatusMode) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog v0.5.1 - 2026-05-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Added `free-coding-models web` as the npm-friendly way to launch the full TUI-style realtime Web Dashboard from a global install.
|
|
5
|
+
- Added Socket.IO as the primary dashboard realtime transport, with SSE and REST polling fallbacks so the UI keeps updating even when one transport is unavailable.
|
|
6
|
+
- Added `/api/state` support for dashboard clients and daemon dashboard clients, including ping mode, countdown, per-model ping state, and benchmark progress metadata.
|
|
7
|
+
- Added tests that lock every visible web table column as sortable, including display-only columns such as `❔`, Last Ping, AI Latency, TPS, and Trend.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Reworked the standalone web server to mirror the TUI ping cadence more closely: startup speed mode, normal cadence, idle slow mode, per-model `isPinging`, and frequent incremental updates.
|
|
11
|
+
- Changed the Web Dashboard AI Latency global benchmark to benchmark only the models currently visible after filters and search, instead of always benchmarking the full catalog.
|
|
12
|
+
- Made every dashboard table column cycle through ascending, descending, and reset sorting, with missing values consistently pushed to the bottom.
|
|
13
|
+
- Removed the top stats card row from the dashboard for a cleaner, table-first layout.
|
|
14
|
+
- Removed the `ms` suffix from Last Ping and Avg cells in the dashboard table to make dense latency columns easier to scan.
|
|
15
|
+
- Improved dashboard table borders and added visible column separators, including stronger light-theme borders.
|
|
16
|
+
- Updated npm and web documentation to distinguish the full catalog dashboard (`free-coding-models web`, default `localhost:3333`) from the router daemon dashboard (`free-coding-models --daemon`, default `localhost:19280`).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fixed `free-coding-models web` being parsed as an API key by treating `web` as a real subcommand.
|
|
20
|
+
- Fixed dashboard benchmark spinners so only the actively benchmarked row shows running state instead of making unrelated rows spin during global scans.
|
|
21
|
+
- Fixed benchmark result keys by using provider/model identifiers, avoiding collisions and invisible results when providers share model ids.
|
|
22
|
+
- Fixed the Tier “All” filter mismatch in the Web Dashboard.
|
|
23
|
+
- Fixed light-theme button contrast where accent buttons could render unreadable black-on-black text.
|
|
24
|
+
- Fixed the web development readiness check so it waits for the correct local server response.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"start": "node bin/free-coding-models.js",
|
|
54
54
|
"test": "node --test test/test.js",
|
|
55
55
|
"prepack": "npm run build:web",
|
|
56
|
+
"dev": "node scripts/dev-web.mjs",
|
|
56
57
|
"dev:web": "node scripts/dev-web.mjs",
|
|
57
58
|
"build:web": "vite build",
|
|
58
59
|
"preview:web": "vite preview",
|
|
@@ -60,7 +61,11 @@
|
|
|
60
61
|
"test:fcm:mock": "node scripts/testfcm-runner.mjs --tool crush --tool-bin-dir test/fixtures/mock-bin"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
|
-
"
|
|
64
|
+
"@tabler/icons-react": "^3.44.0",
|
|
65
|
+
"@tanstack/react-table": "^8.21.3",
|
|
66
|
+
"chalk": "^5.6.2",
|
|
67
|
+
"socket.io": "^4.8.3",
|
|
68
|
+
"socket.io-client": "^4.8.3"
|
|
64
69
|
},
|
|
65
70
|
"packageManager": "pnpm@10.33.2",
|
|
66
71
|
"engines": {
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
saveConfig,
|
|
49
49
|
} from './config.js'
|
|
50
50
|
import { buildChatCompletionPingBody, resolveCloudflareUrl, shouldUseDisabledThinkingForProvider } from './ping.js'
|
|
51
|
+
import { benchmarkModel, BENCHMARK_TIMEOUT_MS } from './benchmark.js'
|
|
51
52
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
52
53
|
|
|
53
54
|
export const ROUTER_DEFAULT_PORT = 19280
|
|
@@ -343,12 +344,32 @@ function getWebModelsPayload(runtime) {
|
|
|
343
344
|
pingCount: pings.length,
|
|
344
345
|
hasApiKey,
|
|
345
346
|
inRouterSet: inSetIndex.has(`${providerKey}::${modelId}`),
|
|
347
|
+
benchmarkKey: key,
|
|
348
|
+
isBenchmarking: runtime.webBenchmarkRunning?.has(key) || false,
|
|
349
|
+
benchmark: runtime.webBenchmarkResults?.get(key) || null,
|
|
346
350
|
})
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
return payload
|
|
350
354
|
}
|
|
351
355
|
|
|
356
|
+
function getWebStatePayload(runtime) {
|
|
357
|
+
const router = runtime.routerConfig()
|
|
358
|
+
const probeInterval = router.probeIntervals?.[router.probeMode] || DEFAULT_ROUTER_SETTINGS.probeIntervals.balanced
|
|
359
|
+
return {
|
|
360
|
+
pingMode: router.probeMode === 'aggressive' ? 'speed' : router.probeMode === 'eco' ? 'slow' : 'normal',
|
|
361
|
+
pingModeSource: 'daemon-probe-mode',
|
|
362
|
+
pingInterval: probeInterval,
|
|
363
|
+
nextPingAt: runtime.lastProbeAt ? runtime.lastProbeAt + probeInterval : null,
|
|
364
|
+
pendingPings: runtime.probeTimeouts?.size || 0,
|
|
365
|
+
isPinging: (runtime.probeTimeouts?.size || 0) > 0,
|
|
366
|
+
globalBenchmarkRunning: runtime.webGlobalBenchmarkRunning || false,
|
|
367
|
+
globalBenchmarkTotal: runtime.webGlobalBenchmarkTotal || 0,
|
|
368
|
+
globalBenchmarkCompleted: runtime.webGlobalBenchmarkCompleted || 0,
|
|
369
|
+
models: getWebModelsPayload(runtime),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
352
373
|
function getWebConfigPayload(runtime) {
|
|
353
374
|
const providers = {}
|
|
354
375
|
for (const [key, src] of Object.entries(sources)) {
|
|
@@ -786,6 +807,11 @@ class RouterRuntime {
|
|
|
786
807
|
this.quotaExhausted = new Set()
|
|
787
808
|
this.quotaDetails = new Map()
|
|
788
809
|
this.staleNotifications = new Set()
|
|
810
|
+
this.webBenchmarkRunning = new Set()
|
|
811
|
+
this.webBenchmarkResults = new Map()
|
|
812
|
+
this.webGlobalBenchmarkRunning = false
|
|
813
|
+
this.webGlobalBenchmarkTotal = 0
|
|
814
|
+
this.webGlobalBenchmarkCompleted = 0
|
|
789
815
|
this.refreshRouteState()
|
|
790
816
|
}
|
|
791
817
|
|
|
@@ -1131,6 +1157,99 @@ class RouterRuntime {
|
|
|
1131
1157
|
}
|
|
1132
1158
|
}
|
|
1133
1159
|
|
|
1160
|
+
broadcastWebState() {
|
|
1161
|
+
this.broadcast('models', getWebStatePayload(this))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async runWebBenchmark(providerKey, modelId) {
|
|
1165
|
+
const key = modelKey(providerKey, modelId)
|
|
1166
|
+
if (this.webBenchmarkRunning.has(key)) return { skipped: true }
|
|
1167
|
+
const source = sources[providerKey]
|
|
1168
|
+
if (!source?.url) {
|
|
1169
|
+
return { ok: false, code: 'UNSUPPORTED', totalMs: 0, error: 'Provider has no benchmark URL', retries: 0 }
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
this.webBenchmarkRunning.add(key)
|
|
1173
|
+
this.broadcastWebState()
|
|
1174
|
+
try {
|
|
1175
|
+
const result = await benchmarkModel({
|
|
1176
|
+
apiKey: this.getApiKeyForProvider(providerKey) ?? null,
|
|
1177
|
+
modelId,
|
|
1178
|
+
providerKey,
|
|
1179
|
+
url: source.url,
|
|
1180
|
+
timeoutMs: BENCHMARK_TIMEOUT_MS,
|
|
1181
|
+
})
|
|
1182
|
+
this.webBenchmarkResults.set(key, result)
|
|
1183
|
+
return result
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
const fallback = { ok: false, code: 'ERR', totalMs: 0, error: err?.message || 'Benchmark failed', retries: 0 }
|
|
1186
|
+
this.webBenchmarkResults.set(key, fallback)
|
|
1187
|
+
return fallback
|
|
1188
|
+
} finally {
|
|
1189
|
+
this.webBenchmarkRunning.delete(key)
|
|
1190
|
+
this.broadcastWebState()
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async runWebGlobalBenchmark(models) {
|
|
1195
|
+
if (this.webGlobalBenchmarkRunning) return { started: false, error: 'Global benchmark already running' }
|
|
1196
|
+
const knownModels = []
|
|
1197
|
+
const seen = new Set()
|
|
1198
|
+
for (const item of Array.isArray(models) ? models : []) {
|
|
1199
|
+
const providerKey = typeof item?.providerKey === 'string' ? item.providerKey : ''
|
|
1200
|
+
const modelId = typeof item?.modelId === 'string' ? item.modelId : ''
|
|
1201
|
+
const key = modelKey(providerKey, modelId)
|
|
1202
|
+
if (!this.modelCatalog.has(key) || seen.has(key)) continue
|
|
1203
|
+
seen.add(key)
|
|
1204
|
+
knownModels.push({ providerKey, modelId, key })
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const fallbackModels = knownModels.length > 0
|
|
1208
|
+
? knownModels
|
|
1209
|
+
: [...this.modelCatalog.values()].filter((m) => sources[m.providerKey]?.url && !sources[m.providerKey]?.cliOnly)
|
|
1210
|
+
|
|
1211
|
+
this.webGlobalBenchmarkRunning = true
|
|
1212
|
+
this.webGlobalBenchmarkTotal = fallbackModels.length
|
|
1213
|
+
this.webGlobalBenchmarkCompleted = 0
|
|
1214
|
+
this.broadcastWebState()
|
|
1215
|
+
|
|
1216
|
+
const healthPriority = { up: 0, pending: 1, timeout: 2, noauth: 3, auth_error: 4, down: 5 }
|
|
1217
|
+
const sorted = [...fallbackModels].sort((a, b) => {
|
|
1218
|
+
const aw = this.probeWindows.get(modelKey(a.providerKey, a.modelId)) || []
|
|
1219
|
+
const bw = this.probeWindows.get(modelKey(b.providerKey, b.modelId)) || []
|
|
1220
|
+
const aLast = aw.at(-1)
|
|
1221
|
+
const bLast = bw.at(-1)
|
|
1222
|
+
const aState = aLast?.ok ? 'up' : aw.length ? 'down' : 'pending'
|
|
1223
|
+
const bState = bLast?.ok ? 'up' : bw.length ? 'down' : 'pending'
|
|
1224
|
+
const hpA = healthPriority[aState] ?? 6
|
|
1225
|
+
const hpB = healthPriority[bState] ?? 6
|
|
1226
|
+
if (hpA !== hpB) return hpA - hpB
|
|
1227
|
+
return (aLast?.latencyMs ?? 99999) - (bLast?.latencyMs ?? 99999)
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
const workers = new Array(Math.min(5, sorted.length)).fill(null).map(async () => {
|
|
1231
|
+
while (sorted.length > 0) {
|
|
1232
|
+
const next = sorted.shift()
|
|
1233
|
+
if (!next) break
|
|
1234
|
+
try {
|
|
1235
|
+
await this.runWebBenchmark(next.providerKey, next.modelId)
|
|
1236
|
+
} finally {
|
|
1237
|
+
this.webGlobalBenchmarkCompleted += 1
|
|
1238
|
+
this.broadcastWebState()
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
void Promise.all(workers).finally(() => {
|
|
1244
|
+
this.webGlobalBenchmarkRunning = false
|
|
1245
|
+
this.webGlobalBenchmarkTotal = 0
|
|
1246
|
+
this.webGlobalBenchmarkCompleted = 0
|
|
1247
|
+
this.broadcastWebState()
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
return { started: true, total: fallbackModels.length }
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1134
1253
|
statusPayload() {
|
|
1135
1254
|
const router = this.routerConfig()
|
|
1136
1255
|
const activeSet = this.getSet(router.activeSet)
|
|
@@ -1892,6 +2011,10 @@ class RouterRuntime {
|
|
|
1892
2011
|
sendJson(res, 200, getWebModelsPayload(this), { 'x-request-id': requestId })
|
|
1893
2012
|
return
|
|
1894
2013
|
}
|
|
2014
|
+
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
2015
|
+
sendJson(res, 200, getWebStatePayload(this), { 'x-request-id': requestId })
|
|
2016
|
+
return
|
|
2017
|
+
}
|
|
1895
2018
|
if (req.method === 'GET' && url.pathname === '/api/config') {
|
|
1896
2019
|
sendJson(res, 200, getWebConfigPayload(this), { 'x-request-id': requestId })
|
|
1897
2020
|
return
|
|
@@ -1907,11 +2030,53 @@ class RouterRuntime {
|
|
|
1907
2030
|
'Connection': 'keep-alive',
|
|
1908
2031
|
'x-request-id': requestId,
|
|
1909
2032
|
})
|
|
1910
|
-
res.write(`data: ${JSON.stringify(
|
|
2033
|
+
res.write(`data: ${JSON.stringify(getWebStatePayload(this))}\n\n`)
|
|
1911
2034
|
this.sseClients.add(res)
|
|
1912
2035
|
req.on('close', () => this.sseClients.delete(res))
|
|
1913
2036
|
return
|
|
1914
2037
|
}
|
|
2038
|
+
if (req.method === 'POST' && url.pathname === '/api/activity') {
|
|
2039
|
+
sendJson(res, 200, { ok: true }, { 'x-request-id': requestId })
|
|
2040
|
+
return
|
|
2041
|
+
}
|
|
2042
|
+
if (req.method === 'POST' && url.pathname === '/api/benchmark') {
|
|
2043
|
+
const body = await readJsonBody(req)
|
|
2044
|
+
const providerKey = typeof body.providerKey === 'string' ? body.providerKey : ''
|
|
2045
|
+
const modelId = typeof body.modelId === 'string' ? body.modelId : ''
|
|
2046
|
+
if (!this.modelCatalog.has(modelKey(providerKey, modelId))) {
|
|
2047
|
+
sendJson(res, 404, { error: 'Model not found' }, { 'x-request-id': requestId })
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
if (this.webBenchmarkRunning.has(modelKey(providerKey, modelId))) {
|
|
2051
|
+
sendJson(res, 409, { error: 'Benchmark already in progress for this model' }, { 'x-request-id': requestId })
|
|
2052
|
+
return
|
|
2053
|
+
}
|
|
2054
|
+
const result = await this.runWebBenchmark(providerKey, modelId)
|
|
2055
|
+
sendJson(res, 200, result, { 'x-request-id': requestId })
|
|
2056
|
+
return
|
|
2057
|
+
}
|
|
2058
|
+
if (url.pathname === '/api/global-benchmark') {
|
|
2059
|
+
if (req.method === 'GET') {
|
|
2060
|
+
sendJson(res, 200, {
|
|
2061
|
+
running: this.webGlobalBenchmarkRunning,
|
|
2062
|
+
total: this.webGlobalBenchmarkTotal,
|
|
2063
|
+
completed: this.webGlobalBenchmarkCompleted,
|
|
2064
|
+
}, { 'x-request-id': requestId })
|
|
2065
|
+
return
|
|
2066
|
+
}
|
|
2067
|
+
if (req.method !== 'POST') {
|
|
2068
|
+
sendError(res, 405, 'Method not allowed', 'invalid_request_error', 'method_not_allowed', requestId)
|
|
2069
|
+
return
|
|
2070
|
+
}
|
|
2071
|
+
if (this.webGlobalBenchmarkRunning) {
|
|
2072
|
+
sendJson(res, 409, { error: 'Global benchmark already running' }, { 'x-request-id': requestId })
|
|
2073
|
+
return
|
|
2074
|
+
}
|
|
2075
|
+
const body = await readJsonBody(req)
|
|
2076
|
+
const result = await this.runWebGlobalBenchmark(body.models)
|
|
2077
|
+
sendJson(res, result.started ? 202 : 409, result, { 'x-request-id': requestId })
|
|
2078
|
+
return
|
|
2079
|
+
}
|
|
1915
2080
|
if (req.method === 'GET' && url.pathname.startsWith('/api/key/')) {
|
|
1916
2081
|
// 📖 Reveals raw API keys — same-origin only to prevent malicious sites
|
|
1917
2082
|
// 📖 from exfiltrating provider credentials via XHR/fetch.
|
package/src/core/utils.js
CHANGED
|
@@ -471,6 +471,8 @@ export function parseArgs(argv) {
|
|
|
471
471
|
flags.push(arg.toLowerCase())
|
|
472
472
|
} else if (skipIndices.has(i)) {
|
|
473
473
|
// 📖 Skip — this is a value for --tier, not an API key
|
|
474
|
+
} else if (i === 0 && arg.toLowerCase() === 'web') {
|
|
475
|
+
// 📖 `free-coding-models web` is a subcommand, not a provider API key.
|
|
474
476
|
} else if (!apiKey) {
|
|
475
477
|
apiKey = arg
|
|
476
478
|
}
|
package/src/tui/cli-help.js
CHANGED
|
@@ -36,6 +36,7 @@ const ANALYSIS_FLAGS = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
const CONFIG_FLAGS = [
|
|
39
|
+
{ flag: 'web | --web | --gui', description: 'Start the full-catalog realtime Web Dashboard' },
|
|
39
40
|
{ flag: '--daemon', description: 'Start the FCM Router daemon + web dashboard (same port)' },
|
|
40
41
|
{ flag: '--daemon-bg', description: 'Start the FCM Router daemon in the background' },
|
|
41
42
|
{ flag: '--daemon-status', description: 'Print FCM Router daemon status JSON' },
|
|
@@ -47,6 +48,7 @@ const CONFIG_FLAGS = [
|
|
|
47
48
|
|
|
48
49
|
const EXAMPLES = [
|
|
49
50
|
'free-coding-models --help',
|
|
51
|
+
'free-coding-models web',
|
|
50
52
|
'free-coding-models --daemon',
|
|
51
53
|
'free-coding-models --daemon-bg',
|
|
52
54
|
'free-coding-models --daemon-status',
|
package/src/tui/render-table.js
CHANGED
|
@@ -412,7 +412,7 @@ export function renderTable({
|
|
|
412
412
|
})
|
|
413
413
|
|
|
414
414
|
const lines = [
|
|
415
|
-
` ${
|
|
415
|
+
` ${chalk.rgb(118, 185, 0).bgRgb(0, 0, 0).bold(' > ')}${chalk.rgb(118, 185, 0).bgRgb(0, 0, 0).bold('free')}${chalk.rgb(255, 255, 255).bgRgb(0, 0, 0).bold('-coding-models')}${chalk.rgb(118, 185, 0).bgRgb(0, 0, 0).bold('_ ')} ${themeColors.dim(`v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${chalk.reset('')} ` +
|
|
416
416
|
themeColors.dim('📦 ') + themeColors.accentBold(`${completedPings}/${totalVisible}`) + themeColors.dim(' ') +
|
|
417
417
|
themeColors.success(`✅ ${up}`) + themeColors.dim(' up ') +
|
|
418
418
|
themeColors.warning(`⏳ ${timeout}`) + themeColors.dim(' timeout ') +
|
package/web/README.md
CHANGED
|
@@ -14,11 +14,14 @@ To maintain maximum code sharing, **95%+ of all components and logic are kept co
|
|
|
14
14
|
|
|
15
15
|
## ⚡ API & Event Integration
|
|
16
16
|
|
|
17
|
-
The React app
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
The React app uses a realtime-first connection strategy against the local engine:
|
|
18
|
+
* **Socket.IO** is preferred in dev/web-server mode for instant per-model ping and benchmark updates.
|
|
19
|
+
* **`GET /api/events` / `EventSource`** is the streaming fallback used by daemon/Docker surfaces.
|
|
20
|
+
* **`GET /api/state`** returns the wrapped live dashboard state for REST fallback polling.
|
|
21
|
+
* **`GET /api/models`** remains the legacy flat model catalog endpoint for simple clients.
|
|
22
|
+
* **`GET /api/config`** retrieves active provider toggles (keys are masked).
|
|
23
|
+
* **`POST /api/settings`** updates API keys and provider preferences.
|
|
24
|
+
* **`POST /api/global-benchmark`** benchmarks only the models currently visible in the web table, so filters/search control the benchmark scope.
|
|
22
25
|
|
|
23
26
|
---
|
|
24
27
|
|