free-coding-models 0.5.15 β 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog/v0.5.16.md +15 -0
- package/changelog/v0.5.17.md +13 -0
- package/package.json +3 -3
- package/src/core/config.js +1 -0
- package/src/core/router-daemon.js +61 -11
- package/web/dist/assets/index-B4ioiEvP.js +41 -0
- package/web/dist/assets/index-avN2GM3H.css +1 -0
- package/web/dist/index.html +6 -2
- package/web/index.html +4 -0
- package/web/server.js +166 -1
- package/web/src/App.jsx +31 -14
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +464 -0
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +347 -0
- package/web/src/components/dashboard/ModelTable.jsx +49 -29
- package/web/src/components/dashboard/ModelTable.module.css +12 -0
- package/web/dist/assets/index-BRqzsHVw.css +0 -1
- package/web/dist/assets/index-CUYQh5_t.js +0 -39
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog v0.5.16 - 2026-06-04
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **π Expandable detail rows in the web dashboard** β clicking any model row now expands an inline 3βcolumn panel below it (accordion style, one row at a time), replacing the old side panel.
|
|
5
|
+
- **π¬ Inline Mini Playground** β middle column of the expanded row lets you chat oneβonβone with the selected model via SSE streaming. Type a message, hit Enter, and watch the response appear in real time.
|
|
6
|
+
- **β‘ Inline AI Latency benchmark** β right column adds a "Test AI Latency" button that runs a live streaming benchmark against the selected model. Token count, TPS, latency, and generated text are shown as they stream in, driven by a new `/api/benchmark-stream` SSE endpoint on the server.
|
|
7
|
+
- **π Model Info column** β left column of the expanded row shows tier, SWEβbench score, context window, provider, status, avg ping, stability, verdict, and uptime at a glance, plus favorite toggle and launch buttons.
|
|
8
|
+
- **π PostHog analytics** β basic event tracking for `app_web_start` (app mount) and `app_router_start` (router open). PostHog snippet injected in `index.html`.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **DetailPanel removed** from the web dashboard β replaced entirely by the inline expandable rows. The old sideβpanel had a CSS bug (never slid in); expand rows are always visible when triggered.
|
|
12
|
+
- **ModelTable** now accepts `onToast`, `onSetToolMode`, `onCycleToolMode`, and `onOpenFallback` props for use in the expanded row.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- DetailPanel slideβin animation (was broken with `translateX(100%)` and no openβstate class) β moot since it was removed, but it was patched during exploration.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog v0.5.17 - 2026-06-08
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **Docker provider key mapping** β replaced broken `sed` logic with a proper Node.js init script (`scripts/docker-init.mjs`). The script imports `ENV_VARS` from `config.js` (single source of truth) and generates clean entrypoint files. `docker-entrypoint.sh` simplified to a 1βline Node call. Added `/api/key/:provider/test` POST endpoint to the daemon (was 404), and fixed the key test probe to use the first valid model from the provider instead of an empty string.
|
|
5
|
+
- **Docker compose** β cleaned 9 stale providers, added `GEMINI_API_KEY` and `OPENCODE_ZEN_API_KEY` support.
|
|
6
|
+
|
|
7
|
+
### Updated
|
|
8
|
+
- **kandown** bumped from 0.4.0 β 0.8.0 (minor version bump, improved task kanban).
|
|
9
|
+
- **vite-plus** bumped to 0.1.24 (dependency fix).
|
|
10
|
+
- **docker/setup-qemu-action** bumped from 3 β 4 in CI (major bump, multiβarch builds).
|
|
11
|
+
|
|
12
|
+
### Added (test coverage)
|
|
13
|
+
- Added tests for Docker init script key mapping and `/api/key/:provider/test` endpoint (see `test/test.js`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@tabler/icons-react": "^3.44.0",
|
|
68
68
|
"@tanstack/react-table": "^8.21.3",
|
|
69
69
|
"chalk": "^5.6.2",
|
|
70
|
-
"kandown": "^0.
|
|
70
|
+
"kandown": "^0.8.0",
|
|
71
71
|
"socket.io": "^4.8.3",
|
|
72
72
|
"socket.io-client": "^4.8.3"
|
|
73
73
|
},
|
|
@@ -80,6 +80,6 @@
|
|
|
80
80
|
"react": "^19.2.7",
|
|
81
81
|
"react-dom": "^19.2.7",
|
|
82
82
|
"vite": "^8.0.16",
|
|
83
|
-
"vite-plus": "^0.1.
|
|
83
|
+
"vite-plus": "^0.1.24"
|
|
84
84
|
}
|
|
85
85
|
}
|
package/src/core/config.js
CHANGED
|
@@ -108,6 +108,7 @@ import { syncShellEnv } from './shell-env.js'
|
|
|
108
108
|
|
|
109
109
|
// π New JSON config path β stores all providers' API keys + enabled state
|
|
110
110
|
export const CONFIG_PATH = join(homedir(), '.free-coding-models.json')
|
|
111
|
+
export { ENV_VARS }
|
|
111
112
|
|
|
112
113
|
// π Runtime data directory β backups and local snapshots live here.
|
|
113
114
|
export const DAEMON_DATA_DIR = join(homedir(), '.free-coding-models')
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
normalizeRouterConfig,
|
|
48
48
|
saveConfig,
|
|
49
49
|
} from './config.js'
|
|
50
|
-
import { buildChatCompletionPingBody, resolveCloudflareUrl, shouldUseDisabledThinkingForProvider } from './ping.js'
|
|
50
|
+
import { buildChatCompletionPingBody, ping, resolveCloudflareUrl, shouldUseDisabledThinkingForProvider } from './ping.js'
|
|
51
51
|
import { benchmarkModel, BENCHMARK_TIMEOUT_MS } from './benchmark.js'
|
|
52
52
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
53
53
|
|
|
@@ -73,6 +73,7 @@ export function getRouterPortRange() {
|
|
|
73
73
|
|
|
74
74
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
75
75
|
const CLI_ENTRY_PATH = join(__dirname, '..', '..', 'bin', 'free-coding-models.js')
|
|
76
|
+
const LOCAL_VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')).version
|
|
76
77
|
const MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
77
78
|
const MAX_REQUEST_LOG = 200
|
|
78
79
|
const MAX_SSE_CLIENTS = 10
|
|
@@ -2537,6 +2538,22 @@ class RouterRuntime {
|
|
|
2537
2538
|
sendJson(res, 200, getWebModelsPayload(this), { 'x-request-id': requestId })
|
|
2538
2539
|
return
|
|
2539
2540
|
}
|
|
2541
|
+
// π Stub endpoints for the web dashboard's hooks (useToolMode, useFavorites,
|
|
2542
|
+
// π useUpdateChecker). These were 404 before β minimal shapes that match
|
|
2543
|
+
// π what the dashboard hooks expect. See PR #108 for context.
|
|
2544
|
+
if (req.method === 'GET' && (url.pathname === '/api/tool-mode')) {
|
|
2545
|
+
sendJson(res, 200, { mode: 'opencode', tools: ['opencode', 'openclaw', 'opencode-desktop', 'opencode-web'] }, { 'x-request-id': requestId })
|
|
2546
|
+
return
|
|
2547
|
+
}
|
|
2548
|
+
if (req.method === 'GET' && (url.pathname === '/api/favorites')) {
|
|
2549
|
+
const cfg = this.config || {}
|
|
2550
|
+
sendJson(res, 200, { favorites: cfg.favorites || [], pinnedAndSticky: Boolean(cfg.settings?.favoritesPinnedAndSticky) }, { 'x-request-id': requestId })
|
|
2551
|
+
return
|
|
2552
|
+
}
|
|
2553
|
+
if (req.method === 'GET' && (url.pathname === '/api/version')) {
|
|
2554
|
+
sendJson(res, 200, { local: LOCAL_VERSION, latest: null, lastReleaseDate: null, error: null }, { 'x-request-id': requestId })
|
|
2555
|
+
return
|
|
2556
|
+
}
|
|
2540
2557
|
// π /api/router/catalog β lightweight catalog of routeable models for
|
|
2541
2558
|
// π the Web Router Dashboard's "Add model" picker. Returns one row
|
|
2542
2559
|
// π per (provider, model) with `key`, label, tier, ctx. We filter to
|
|
@@ -2673,21 +2690,50 @@ class RouterRuntime {
|
|
|
2673
2690
|
sendJson(res, result.started ? 202 : 409, result, { 'x-request-id': requestId })
|
|
2674
2691
|
return
|
|
2675
2692
|
}
|
|
2676
|
-
if (
|
|
2677
|
-
// π Reveals raw API keys β same-origin only to prevent malicious sites
|
|
2678
|
-
// π from exfiltrating provider credentials via XHR/fetch.
|
|
2693
|
+
if (url.pathname.startsWith('/api/key/')) {
|
|
2679
2694
|
if (!isSameOriginOrLocal(req)) {
|
|
2680
2695
|
sendError(res, 403, 'Forbidden cross-origin request', 'invalid_request_error', 'forbidden_origin', requestId)
|
|
2681
2696
|
return
|
|
2682
2697
|
}
|
|
2683
|
-
const
|
|
2684
|
-
if (
|
|
2685
|
-
|
|
2698
|
+
const testMatch = url.pathname.match(/^\/api\/key\/([^/]+)\/test$/)
|
|
2699
|
+
if (testMatch && req.method === 'POST') {
|
|
2700
|
+
const providerKey = decodeURIComponent(testMatch[1])
|
|
2701
|
+
if (!sources[providerKey]) {
|
|
2702
|
+
sendError(res, 404, 'Unknown provider', 'invalid_request_error', 'unknown_provider', requestId)
|
|
2703
|
+
return
|
|
2704
|
+
}
|
|
2705
|
+
const apiKey = this.getApiKeyForProvider(providerKey)
|
|
2706
|
+
if (!apiKey) {
|
|
2707
|
+
sendJson(res, 200, { outcome: 'missing_key', detail: `${providerKey} has no saved API key.` }, { 'x-request-id': requestId })
|
|
2708
|
+
return
|
|
2709
|
+
}
|
|
2710
|
+
const providerModels = sources[providerKey]?.models || []
|
|
2711
|
+
const modelId = providerModels[0]?.[0] || ''
|
|
2712
|
+
try {
|
|
2713
|
+
const result = await ping(apiKey, modelId, providerKey, sources[providerKey].url)
|
|
2714
|
+
const code = result?.code
|
|
2715
|
+
if (code === '200') {
|
|
2716
|
+
sendJson(res, 200, { outcome: 'ok', code: 200 }, { 'x-request-id': requestId })
|
|
2717
|
+
} else if (code === '401' || code === '403') {
|
|
2718
|
+
sendJson(res, 200, { outcome: 'auth_error', code: Number(code) || code }, { 'x-request-id': requestId })
|
|
2719
|
+
} else {
|
|
2720
|
+
sendJson(res, 200, { outcome: 'fail', code: code ?? 'ERR', detail: 'Probe did not return a 2xx' }, { 'x-request-id': requestId })
|
|
2721
|
+
}
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
sendJson(res, 200, { outcome: 'fail', detail: err.message || 'Probe failed' }, { 'x-request-id': requestId })
|
|
2724
|
+
}
|
|
2725
|
+
return
|
|
2726
|
+
}
|
|
2727
|
+
if (req.method === 'GET') {
|
|
2728
|
+
const providerKey = decodeURIComponent(url.pathname.slice('/api/key/'.length))
|
|
2729
|
+
if (!providerKey || !sources[providerKey]) {
|
|
2730
|
+
sendError(res, 404, 'Unknown provider', 'invalid_request_error', 'unknown_provider', requestId)
|
|
2731
|
+
return
|
|
2732
|
+
}
|
|
2733
|
+
const rawKey = this.getApiKeyForProvider(providerKey)
|
|
2734
|
+
sendJson(res, 200, { key: rawKey || null }, { 'x-request-id': requestId })
|
|
2686
2735
|
return
|
|
2687
2736
|
}
|
|
2688
|
-
const rawKey = this.getApiKeyForProvider(providerKey)
|
|
2689
|
-
sendJson(res, 200, { key: rawKey || null }, { 'x-request-id': requestId })
|
|
2690
|
-
return
|
|
2691
2737
|
}
|
|
2692
2738
|
if (req.method === 'POST' && url.pathname === '/api/settings') {
|
|
2693
2739
|
// π Writes API keys + provider toggles β same-origin only to block
|
|
@@ -2881,7 +2927,7 @@ const PREFERRED_DEFAULT_MODELS = [
|
|
|
2881
2927
|
* @param {object} [options] { probeFn: async (entry) => ({ ok, latencyMs, code }) }
|
|
2882
2928
|
* @returns {{ name: string, models: Array, created: string }}
|
|
2883
2929
|
*/
|
|
2884
|
-
export async function buildDefaultRouterSet(config = {}, maxModels
|
|
2930
|
+
export async function buildDefaultRouterSet(config = {}, maxModels, options = {}) {
|
|
2885
2931
|
const probeFn = typeof options.probeFn === 'function' ? options.probeFn : null
|
|
2886
2932
|
const probeTimeoutMs = typeof options.probeTimeoutMs === 'number' ? options.probeTimeoutMs : 1500
|
|
2887
2933
|
const probeBudget = typeof options.probeBudget === 'number' ? options.probeBudget : 24
|
|
@@ -2890,6 +2936,10 @@ export async function buildDefaultRouterSet(config = {}, maxModels = 5, options
|
|
|
2890
2936
|
.filter(([, value]) => (Array.isArray(value) ? value.length > 0 : typeof value === 'string' && value.trim()))
|
|
2891
2937
|
.map(([provider]) => provider))
|
|
2892
2938
|
|
|
2939
|
+
// π Scale default set size with configured providers so users with many
|
|
2940
|
+
// π keys get a richer default router set (PR #108 idea, kept from the
|
|
2941
|
+
// π previous sync version).
|
|
2942
|
+
if (maxModels === undefined) maxModels = Math.max(5, keyedProviders.size * 2)
|
|
2893
2943
|
const entries = []
|
|
2894
2944
|
for (const [providerKey, source] of Object.entries(sources)) {
|
|
2895
2945
|
if (!isRouteableProvider(providerKey)) continue
|