free-coding-models 0.4.3 → 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 +19 -9
- package/changelog/v0.5.0.md +15 -0
- package/changelog/v0.5.1.md +24 -0
- package/package.json +7 -2
- package/src/{analysis.js → core/analysis.js} +5 -5
- package/src/{constants.js → core/constants.js} +1 -1
- package/src/{endpoint-installer.js → core/endpoint-installer.js} +1 -1
- package/src/{installed-models-manager.js → core/installed-models-manager.js} +1 -1
- package/src/{kilo.js → core/kilo.js} +1 -2
- package/src/{openclaw.js → core/openclaw.js} +1 -1
- package/src/{opencode.js → core/opencode.js} +2 -1
- package/src/{ping-loop.js → core/ping-loop.js} +1 -1
- package/src/{router-daemon.js → core/router-daemon.js} +169 -4
- package/src/{router-dashboard.js → core/router-dashboard.js} +2 -2
- package/src/{setup.js → core/setup.js} +1 -1
- package/src/{sync-set.js → core/sync-set.js} +1 -1
- package/src/{telemetry.js → core/telemetry.js} +1 -1
- package/src/{tool-launchers.js → core/tool-launchers.js} +2 -2
- package/src/{updater.js → core/updater.js} +1 -1
- package/src/{utils.js → core/utils.js} +2 -0
- package/src/{app.js → tui/app.js} +38 -38
- package/src/{cli-help.js → tui/cli-help.js} +3 -1
- package/src/{command-palette.js → tui/command-palette.js} +2 -2
- package/src/{key-handler.js → tui/key-handler.js} +11 -11
- package/src/{overlays.js → tui/overlays.js} +2 -2
- package/src/{render-helpers.js → tui/render-helpers.js} +2 -2
- package/src/{render-table.js → tui/render-table.js} +9 -9
- package/src/{tui-filters.js → tui/tui-filters.js} +3 -3
- package/src/{tui-state.js → tui/tui-state.js} +1 -1
- package/web/README.md +46 -0
- 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 +609 -214
- 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/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +0 -1
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +0 -1
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +0 -1
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +0 -1
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +0 -1
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +0 -1
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +0 -1
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +0 -1
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +0 -1
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +0 -1
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +0 -1
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +0 -1
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +0 -1
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +0 -1
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +0 -1
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +0 -1
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +0 -1
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +0 -1
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +0 -1
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +0 -1
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +0 -1
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +0 -1
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +0 -1
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +0 -1
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +0 -1
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +0 -1
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +0 -1
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +0 -1
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +0 -1
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +0 -1
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +0 -1
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +0 -1
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +0 -1
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +0 -1
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +0 -1
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +0 -1
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +0 -1
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +0 -1
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +0 -1
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +0 -1
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +0 -1
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +0 -1
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +0 -1
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +0 -1
- package/web/dist/assets/index-CGN-0_A0.css +0 -1
- package/web/dist/assets/index-Czwis3ab.js +0 -11
- /package/src/{benchmark.js → core/benchmark.js} +0 -0
- /package/src/{cache.js → core/cache.js} +0 -0
- /package/src/{changelog-loader.js → core/changelog-loader.js} +0 -0
- /package/src/{config.js → core/config.js} +0 -0
- /package/src/{favorites.js → core/favorites.js} +0 -0
- /package/src/{kilo-config.js → core/kilo-config.js} +0 -0
- /package/src/{legacy-proxy-cleanup.js → core/legacy-proxy-cleanup.js} +0 -0
- /package/src/{model-merger.js → core/model-merger.js} +0 -0
- /package/src/{opencode-config.js → core/opencode-config.js} +0 -0
- /package/src/{ping.js → core/ping.js} +0 -0
- /package/src/{product-flags.js → core/product-flags.js} +0 -0
- /package/src/{provider-metadata.js → core/provider-metadata.js} +0 -0
- /package/src/{provider-quota-fetchers.js → core/provider-quota-fetchers.js} +0 -0
- /package/src/{quota-capabilities.js → core/quota-capabilities.js} +0 -0
- /package/src/{security.js → core/security.js} +0 -0
- /package/src/{shell-env.js → core/shell-env.js} +0 -0
- /package/src/{testfcm.js → core/testfcm.js} +0 -0
- /package/src/{token-usage-reader.js → core/token-usage-reader.js} +0 -0
- /package/src/{tool-bootstrap.js → core/tool-bootstrap.js} +0 -0
- /package/src/{tool-metadata.js → core/tool-metadata.js} +0 -0
- /package/src/{usage-reader.js → core/usage-reader.js} +0 -0
- /package/src/{mouse.js → tui/mouse.js} +0 -0
- /package/src/{theme.js → tui/theme.js} +0 -0
- /package/src/{tier-colors.js → tui/tier-colors.js} +0 -0
- /package/src/{ui-config.js → tui/ui-config.js} +0 -0
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
|
|
|
@@ -10,13 +10,13 @@ if (process.argv.includes('--dev')) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
|
-
import { parseArgs, TIER_LETTER_MAP } from '../src/utils.js';
|
|
14
|
-
import { loadConfig } from '../src/config.js';
|
|
15
|
-
import { ensureTelemetryConfig } from '../src/telemetry.js';
|
|
16
|
-
import { ensureFavoritesConfig } from '../src/favorites.js';
|
|
17
|
-
import { buildCliHelpText } from '../src/cli-help.js';
|
|
18
|
-
import { ALT_LEAVE } from '../src/constants.js';
|
|
19
|
-
import { runApp } from '../src/app.js';
|
|
13
|
+
import { parseArgs, TIER_LETTER_MAP } from '../src/core/utils.js';
|
|
14
|
+
import { loadConfig } from '../src/core/config.js';
|
|
15
|
+
import { ensureTelemetryConfig } from '../src/core/telemetry.js';
|
|
16
|
+
import { ensureFavoritesConfig } from '../src/core/favorites.js';
|
|
17
|
+
import { buildCliHelpText } from '../src/tui/cli-help.js';
|
|
18
|
+
import { ALT_LEAVE } from '../src/core/constants.js';
|
|
19
|
+
import { runApp } from '../src/tui/app.js';
|
|
20
20
|
|
|
21
21
|
// Global error handlers to ensure terminal is restored if something crashes catastrophically
|
|
22
22
|
process.on('uncaughtException', (err) => {
|
|
@@ -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) {
|
|
@@ -61,7 +71,7 @@ async function main() {
|
|
|
61
71
|
runRouterDaemon,
|
|
62
72
|
startRouterDaemonBackground,
|
|
63
73
|
stopRouterDaemon,
|
|
64
|
-
} = await import('../src/router-daemon.js');
|
|
74
|
+
} = await import('../src/core/router-daemon.js');
|
|
65
75
|
|
|
66
76
|
if (cliArgs.daemonMode) {
|
|
67
77
|
await runRouterDaemon();
|
|
@@ -80,7 +90,7 @@ async function main() {
|
|
|
80
90
|
|
|
81
91
|
// 📖 --sync-set [name] — auto-discover, probe, and populate a router set
|
|
82
92
|
if (cliArgs.syncSetMode) {
|
|
83
|
-
const { syncSet } = await import('../src/sync-set.js');
|
|
93
|
+
const { syncSet } = await import('../src/core/sync-set.js');
|
|
84
94
|
const result = await syncSet({ name: cliArgs.syncSetName || 'auto' });
|
|
85
95
|
console.log(JSON.stringify(result, null, 2));
|
|
86
96
|
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog v0.5.0 - 2026-05-31
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **Final Desktop PRD**: Completed `desktop/prd-desktop.md` defining Tauri v2 shell, Bun/Node sidecar integration, zombie process prevention (parent-death binding), and loopback-only security models.
|
|
5
|
+
- **Sub-Project Documentation**: Added `/web/README.md` and `/desktop/README.md` to guide modular frontend and desktop sidecar development.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Architectural Segregation (Core vs. TUI)**: Partitioned the codebase into isolated layers:
|
|
9
|
+
- `src/core/` for 100% shared business logic (routing, scoring, pings, daemon, quota, and config management) shared 1:1 with CLI, Docker, and Tauri sidecar.
|
|
10
|
+
- `src/tui/` for terminal-only layouts, keystroke handlers, ANSI renderers, and interactive loops.
|
|
11
|
+
- **Relative Import Pathways**: Restructured and re-routed relative imports across all entry points, TUI assets, and core logic files.
|
|
12
|
+
- **Port Consistency**: Standardized loopback proxy and daemon endpoints on port `19280` across all CLI, Docker, and Desktop configurations.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Clean workspace**: Fully purged `.kandown` task manager logs, `.claude-mcp.json`, and stale planning/analysis documents to ensure a zero-noise, lightweight npm package profile.
|
|
@@ -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.
|
|
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": {
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
* @see {@link ../src/ping.js} ping implementation
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import { MODELS, sources } from '
|
|
39
|
-
import { findBestModel, filterByTier, formatCtxWindow, labelFromId, TIER_LETTER_MAP } from '
|
|
40
|
-
import { isProviderEnabled, getApiKey } from '
|
|
41
|
-
import { ping } from '
|
|
42
|
-
import { PROVIDER_COLOR } from '
|
|
38
|
+
import { MODELS, sources } from '../../sources.js'
|
|
39
|
+
import { findBestModel, filterByTier, formatCtxWindow, labelFromId, TIER_LETTER_MAP } from './utils.js'
|
|
40
|
+
import { isProviderEnabled, getApiKey } from './config.js'
|
|
41
|
+
import { ping } from './ping.js'
|
|
42
|
+
import { PROVIDER_COLOR } from '../tui/render-table.js'
|
|
43
43
|
import chalk from 'chalk'
|
|
44
44
|
|
|
45
45
|
// 📖 runFiableMode: Analyze models for reliability over 10 seconds, output the best one.
|
|
@@ -48,7 +48,7 @@ import chalk from 'chalk'
|
|
|
48
48
|
// 📖 \x1b[?7l disables auto-wrap so wide rows clip at the right edge instead of
|
|
49
49
|
// 📖 wrapping to the next line (which would double the row height and overflow).
|
|
50
50
|
// 📖 Mouse tracking sequences are appended/prepended so clicks and scroll work in the TUI.
|
|
51
|
-
import { MOUSE_ENABLE, MOUSE_DISABLE } from '
|
|
51
|
+
import { MOUSE_ENABLE, MOUSE_DISABLE } from '../tui/mouse.js'
|
|
52
52
|
|
|
53
53
|
export const ALT_ENTER = '\x1b[?1049h\x1b[?25l\x1b[?7l' + MOUSE_ENABLE
|
|
54
54
|
export const ALT_LEAVE = MOUSE_DISABLE + '\x1b[?7h\x1b[?1049l\x1b[?25h'
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
44
44
|
import { homedir } from 'node:os'
|
|
45
45
|
import { dirname, join } from 'node:path'
|
|
46
|
-
import { MODELS, sources } from '
|
|
46
|
+
import { MODELS, sources } from '../../sources.js'
|
|
47
47
|
import { getApiKey, saveConfig } from './config.js'
|
|
48
48
|
import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
49
49
|
import { getToolMeta } from './tool-metadata.js'
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
49
49
|
import { homedir } from 'node:os'
|
|
50
50
|
import { join, dirname } from 'node:path'
|
|
51
|
-
import { sources } from '
|
|
51
|
+
import { sources } from '../../sources.js'
|
|
52
52
|
|
|
53
53
|
const BACKUP_PATH = join(homedir(), '.free-coding-models-backups.json')
|
|
54
54
|
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk'
|
|
7
|
-
import {
|
|
7
|
+
import { sources } from '../../sources.js'
|
|
8
8
|
import { loadKiloConfig, saveKiloConfig, getKiloConfigPath } from './kilo-config.js'
|
|
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'
|
|
13
12
|
|
|
14
13
|
// 📖 Map source model IDs to Kilo built-in IDs (same as OpenCode).
|
|
15
14
|
function getKiloModelId(providerKey, modelId) {
|
|
@@ -31,7 +31,7 @@ import { homedir } from 'os'
|
|
|
31
31
|
import { dirname, join } from 'path'
|
|
32
32
|
import { installProviderEndpoints } from './endpoint-installer.js'
|
|
33
33
|
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
34
|
-
import { PROVIDER_COLOR } from '
|
|
34
|
+
import { PROVIDER_COLOR } from '../tui/render-table.js'
|
|
35
35
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
36
36
|
import { getApiKey } from './config.js'
|
|
37
37
|
import { syncShellEnv } from './shell-env.js'
|
|
@@ -28,7 +28,8 @@ import { request as httpsRequest } from 'https'
|
|
|
28
28
|
import { homedir } from 'os'
|
|
29
29
|
import { join } from 'path'
|
|
30
30
|
import { copyFileSync, existsSync } from 'fs'
|
|
31
|
-
import { PROVIDER_COLOR } from '
|
|
31
|
+
import { PROVIDER_COLOR } from '../tui/render-table.js'
|
|
32
|
+
import { sources } from '../../sources.js'
|
|
32
33
|
import { loadOpenCodeConfig, saveOpenCodeConfig } from './opencode-config.js'
|
|
33
34
|
import { getApiKey } from './config.js'
|
|
34
35
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
|
|
@@ -37,7 +37,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
37
37
|
import { appendFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
38
38
|
import { homedir } from 'node:os'
|
|
39
39
|
import { fileURLToPath } from 'node:url'
|
|
40
|
-
import { MODELS, sources } from '
|
|
40
|
+
import { MODELS, sources } from '../../sources.js'
|
|
41
41
|
import {
|
|
42
42
|
CONFIG_PATH,
|
|
43
43
|
DEFAULT_ROUTER_SETTINGS,
|
|
@@ -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
|
|
@@ -71,7 +72,7 @@ export function getRouterPortRange() {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
74
|
-
const CLI_ENTRY_PATH = join(__dirname, '..', 'bin', 'free-coding-models.js')
|
|
75
|
+
const CLI_ENTRY_PATH = join(__dirname, '..', '..', 'bin', 'free-coding-models.js')
|
|
75
76
|
const MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
76
77
|
const MAX_REQUEST_LOG = 200
|
|
77
78
|
const MAX_SSE_CLIENTS = 10
|
|
@@ -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)) {
|
|
@@ -365,7 +386,7 @@ function getWebConfigPayload(runtime) {
|
|
|
365
386
|
return { providers, totalModels: MODELS.length }
|
|
366
387
|
}
|
|
367
388
|
|
|
368
|
-
const WEB_DIST_DIR = resolvePath(__dirname, '..', 'web', 'dist')
|
|
389
|
+
const WEB_DIST_DIR = resolvePath(__dirname, '..', '..', 'web', 'dist')
|
|
369
390
|
|
|
370
391
|
function serveStaticFromDist(res, absPath) {
|
|
371
392
|
const ext = absPath.slice(absPath.lastIndexOf('.'))
|
|
@@ -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.
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
|
|
42
42
|
import chalk from 'chalk'
|
|
43
43
|
import { existsSync, readFileSync } from 'node:fs'
|
|
44
|
-
import { displayWidth, padEndDisplay, sliceOverlayLines, tintOverlayLines } from '
|
|
44
|
+
import { displayWidth, padEndDisplay, sliceOverlayLines, tintOverlayLines } from '../tui/render-helpers.js'
|
|
45
45
|
import { ROUTER_DEFAULT_PORT, ROUTER_MAX_PORT, ROUTER_PID_PATH, ROUTER_PORT_PATH, getRouterPortRange } from './router-daemon.js'
|
|
46
|
-
import { themeColors, getTierRgb } from '
|
|
46
|
+
import { themeColors, getTierRgb } from '../tui/theme.js'
|
|
47
47
|
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
48
48
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
49
49
|
import { getAvg, getVerdict } from './utils.js'
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import chalk from 'chalk'
|
|
37
37
|
import { createRequire } from 'module'
|
|
38
|
-
import { sources } from '
|
|
38
|
+
import { sources } from '../../sources.js'
|
|
39
39
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
40
40
|
import { saveConfig } from './config.js'
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ import { createRequire } from 'module'
|
|
|
51
51
|
import { saveConfig } from './config.js'
|
|
52
52
|
|
|
53
53
|
const require = createRequire(import.meta.url)
|
|
54
|
-
const pkg = require('
|
|
54
|
+
const pkg = require('../../package.json')
|
|
55
55
|
const LOCAL_VERSION = pkg.version
|
|
56
56
|
|
|
57
57
|
// 📖 PostHog capture endpoint and defaults.
|
|
@@ -41,8 +41,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from
|
|
|
41
41
|
import { homedir } from 'os'
|
|
42
42
|
import { dirname, join } from 'path'
|
|
43
43
|
import { spawn, spawnSync } from 'child_process'
|
|
44
|
-
import { sources } from '
|
|
45
|
-
import { PROVIDER_COLOR } from '
|
|
44
|
+
import { sources } from '../../sources.js'
|
|
45
|
+
import { PROVIDER_COLOR } from '../tui/render-table.js'
|
|
46
46
|
import { getApiKey } from './config.js'
|
|
47
47
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
48
48
|
import { getToolMeta, TOOL_METADATA } from './tool-metadata.js'
|
|
@@ -46,7 +46,7 @@ import { accessSync, constants } from 'fs'
|
|
|
46
46
|
|
|
47
47
|
const require = createRequire(import.meta.url)
|
|
48
48
|
const readline = require('readline')
|
|
49
|
-
const pkg = require('
|
|
49
|
+
const pkg = require('../../package.json')
|
|
50
50
|
const LOCAL_VERSION = pkg.version
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -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
|
}
|