free-coding-models 0.5.16 → 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.
|
@@ -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
|