free-coding-models 0.5.77 → 0.5.79
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.78.md +17 -0
- package/changelog/v0.5.79.md +12 -0
- package/package.json +1 -1
- package/src/core/endpoint-installer.js +4 -2
- package/src/core/router-daemon.js +34 -1
- package/src/core/schema-normalizer.js +18 -1
- package/src/core/tool-launchers.js +6 -0
- package/src/core/updater.js +24 -0
- package/src/tui/key-handler.js +1 -3
- package/src/tui/tui-state.js +11 -4
- package/web/dist/assets/{index-BjEXVUHp.js → index-Ct7RqkOX.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/src/hooks/useToolMode.js +1 -1
- package/web/src/utils/m3.js +2 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog v0.5.78 - 2026-08-23
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **Router forwards `prompt_cache_key` to NVIDIA -> HTTP 400 (fixes #160).** Clients using OpenAI JS SDK (pi agent) set `prompt_cache_key` to session UUID for prompt caching; NVIDIA rejects it with `Unsupported parameter(s): prompt_cache_key`. Added `prompt_cache_key` to `STRIP_PARAMS` in `src/core/schema-normalizer.js` and added `nvidia: normalizeNvidia` (strip + orphan tool drop) to `PROVIDER_NORMALIZERS` so `normalizeRequestBody(body, 'nvidia')` now strips the hint before forwarding. Verified with direct replay against `integrate.api.nvidia.com` and `pnpm test` 813/813. Thanks @pelvity.
|
|
5
|
+
|
|
6
|
+
- **Config `pingInterval` not respected at startup (fixes #155).** TUI always started in speed mode (2s) ignoring `settings.pingInterval` and `--ping-interval`. Now `src/tui/tui-state.js` `createTuiState()` derives `initialInterval` from `config.settings.pingInterval` (already merged from CLI in `src/tui/app.js:283`) and maps to mode via `intervalToPingMode` (30000 -> slow, 10000 -> normal, 4000 -> forced, <=3000 -> speed). Sets `pingModeSource` to `config` vs `startup` and only sets `speedModeUntil` when initial mode is speed. Users with 30s interval no longer burn quota on startup burst. Thanks @BetterToAutomateTheWorld.
|
|
7
|
+
|
|
8
|
+
- **jcode configuration option for CLI and web (fixes #145).** CLI Z cycle already had `jcode` but launch hung (stdin left in raw mode, REPL appeared frozen) and web had no jcode option at all.
|
|
9
|
+
- CLI hang: `src/tui/key-handler.js` `launchSelectedModel` now calls `stopUi({ resetRawMode: true })` (was `stopUi()` with raw mode still enabled) and `src/core/tool-launchers.js` `spawnCommand` defensively resets `stdin.setRawMode(false)` before handing TTY to child.
|
|
10
|
+
- Web: Added `jcode` to `INSTALL_TARGET_MODES` in `src/core/endpoint-installer.js` (now 15 targets, `jcode` after `pi` to match `TOOL_MODE_ORDER` order) with env-based installer (`~/.fcm-jcode-env`), mirrored in `web/src/utils/m3.js` `INSTALL_ENDPOINT_TOOL_MODES`, and fixed `web/src/hooks/useToolMode.js` cycle bug (`TOOL_MODE_ORDER` -> `INSTALL_ENDPOINT_TOOL_MODES`). Web `/api/tool-mode` now accepts `jcode` without 422 and dropdown/cycle shows it. Thanks @plahakps.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `src/core/schema-normalizer.js` JSDoc now exports `normalizeNvidia` and clarifies per-provider normalization.
|
|
14
|
+
- `web/src/utils/m3.js` and `src/core/endpoint-installer.js` tool order now matches CLI's `TOOL_MODE_ORDER` (opencode -> pi -> jcode -> opencode-desktop ...).
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
- 813/813 passing. Updated `test/test.js` endpoint install target expectation to include `jcode`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog v0.5.79 - 2026-08-23
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **Fast-coding pool malformed tool_calls hang (fixes #124).** Some providers returned `finish_reason: tool_calls` without a `tool_calls` array, causing pi and other OpenAI clients to wait forever. Router now normalizes such responses to `finish_reason: stop` in `src/core/router-daemon.js` (`normalizeToolCallsResponse`) before forwarding, making the response spec-compliant even when upstream is quirky. Thanks @stgreenb.
|
|
5
|
+
|
|
6
|
+
- **Docker mandatory update EACCES crash (fixes #109).** Inside Docker the app lives at `/app` and runs as non-root `fcm`, so `npm i -g` always hit EACCES on `/usr/local/lib/node_modules` and showed "Mandatory update failed" then exited. Added `isRunningInDocker()` in `src/core/updater.js` (checks `/.dockerenv` and `/app` prefix) to skip mandatory startup updates in containers; rebuild the image to update. Thanks @karneaud.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- Closed older user feature requests with concise responses: #51 Nix module (PR welcome), #30 oh-my-opencode (wrapper externe, pas de duplication), #27 Claude Code integration (now via router `fcm`). Only vava's own issues (75, 21, 18) remain open.
|
|
10
|
+
|
|
11
|
+
### Tests
|
|
12
|
+
- 813/813 passing. Router now re-stringifies normalized JSON only when needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.79",
|
|
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,8 +53,8 @@ import { ensureDir, readJson as sharedReadJson } from './shared-helpers.js'
|
|
|
53
53
|
// 📖 zai and opencode-zen ARE OpenAI-compatible and CAN be installed into any tool.
|
|
54
54
|
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate'])
|
|
55
55
|
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
56
|
-
// 📖
|
|
57
|
-
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'crush', 'goose', '
|
|
56
|
+
// 📖 jcode is CLI-only and uses an env-file helper (same as openhands) — selectable in both CLI and Web.
|
|
57
|
+
const INSTALL_TARGET_MODES = ['opencode', 'pi', 'jcode', 'opencode-desktop', 'opencode-web', 'openclaw', 'crush', 'goose', 'aider', 'qwen', 'openhands', 'amp', 'forgecode', 'fcm_router', 'zcode']
|
|
58
58
|
|
|
59
59
|
function getDefaultPaths() {
|
|
60
60
|
const home = homedir()
|
|
@@ -839,6 +839,8 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
839
839
|
installResult = installIntoAmp(providerKey, models, apiKey, paths)
|
|
840
840
|
} else if (canonicalToolMode === 'qwen') {
|
|
841
841
|
installResult = installIntoQwen(providerKey, models, apiKey, paths)
|
|
842
|
+
} else if (canonicalToolMode === 'jcode') {
|
|
843
|
+
installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
|
|
842
844
|
} else if (canonicalToolMode === 'openhands') {
|
|
843
845
|
installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
|
|
844
846
|
} else if (canonicalToolMode === 'fcm_router') {
|
|
@@ -118,6 +118,32 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
118
118
|
const CLI_ENTRY_PATH = join(__dirname, '..', '..', 'bin', 'free-coding-models.js')
|
|
119
119
|
const LOCAL_VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')).version
|
|
120
120
|
const MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
121
|
+
/**
|
|
122
|
+
* 📖 normalizeToolCallsResponse — fix malformed tool_calls from upstream.
|
|
123
|
+
* Some providers return finish_reason: "tool_calls" but message has no tool_calls array,
|
|
124
|
+
* which hangs OpenAI-compatible clients (pi agent waits for tool_calls that never arrives).
|
|
125
|
+
* Normalize to finish_reason: "stop" when tool_calls is missing/empty per OpenAI spec.
|
|
126
|
+
* This is defensive: it makes the router spec-compliant regardless of upstream quirks.
|
|
127
|
+
* @param {object} data - parsed JSON response object
|
|
128
|
+
* @returns {boolean} true if mutated
|
|
129
|
+
*/
|
|
130
|
+
function normalizeToolCallsResponse(data) {
|
|
131
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.choices)) return false
|
|
132
|
+
let mutated = false
|
|
133
|
+
for (const choice of data.choices) {
|
|
134
|
+
if (!choice || typeof choice !== 'object') continue
|
|
135
|
+
if (choice.finish_reason !== 'tool_calls') continue
|
|
136
|
+
const msg = choice.message
|
|
137
|
+
if (!msg || typeof msg !== 'object') continue
|
|
138
|
+
const tc = msg.tool_calls
|
|
139
|
+
if (!Array.isArray(tc) || tc.length === 0) {
|
|
140
|
+
choice.finish_reason = 'stop'
|
|
141
|
+
mutated = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return mutated
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
const MAX_REQUEST_LOG = 200
|
|
122
148
|
const MAX_SSE_CLIENTS = 10
|
|
123
149
|
const MAX_CONCURRENT_REQUESTS = 50
|
|
@@ -2226,13 +2252,20 @@ class RouterRuntime {
|
|
|
2226
2252
|
failover: attemptIndex > 0,
|
|
2227
2253
|
})
|
|
2228
2254
|
this.logger.info(`Routed to ${key} - ${latencyMs}ms`, { request_id: requestId, status: response.status })
|
|
2255
|
+
// 📖 Fix #124: normalize malformed tool_calls (finish_reason tool_calls without tool_calls array)
|
|
2256
|
+
let responseText = text
|
|
2257
|
+
try {
|
|
2258
|
+
if (normalizeToolCallsResponse(parsed.value)) {
|
|
2259
|
+
responseText = JSON.stringify(parsed.value)
|
|
2260
|
+
}
|
|
2261
|
+
} catch {}
|
|
2229
2262
|
if (!res.writableEnded) {
|
|
2230
2263
|
res.writeHead(response.status, {
|
|
2231
2264
|
...headerEntries(response.headers),
|
|
2232
2265
|
'x-fcm-router-model': key,
|
|
2233
2266
|
'x-request-id': requestId,
|
|
2234
2267
|
})
|
|
2235
|
-
res.end(
|
|
2268
|
+
res.end(responseText)
|
|
2236
2269
|
}
|
|
2237
2270
|
return { done: true }
|
|
2238
2271
|
}
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* → `clampTemperature` — clamps `temperature` to the provider's accepted range
|
|
31
31
|
* → `normalizeZai` — for `zai` (GLM) provider
|
|
32
32
|
* → `normalizeMistral` — for `mistral` and `codestral` providers
|
|
33
|
+
* → `normalizeNvidia` — for `nvidia` provider (strips unsupported params like prompt_cache_key)
|
|
33
34
|
*
|
|
34
|
-
* @exports normalizeRequestBody, normalizeZai, normalizeMistral, PROVIDER_NORMALIZERS
|
|
35
|
+
* @exports normalizeRequestBody, normalizeZai, normalizeMistral, normalizeNvidia, PROVIDER_NORMALIZERS
|
|
35
36
|
*
|
|
36
37
|
* @see src/core/router-daemon.js — calls `normalizeRequestBody` before forwarding upstream
|
|
37
38
|
*/
|
|
@@ -48,6 +49,7 @@ const STRIP_PARAMS = [
|
|
|
48
49
|
'echo', // not in OpenAI spec
|
|
49
50
|
'user', // PII risk; not used by FCM
|
|
50
51
|
'metadata', // not always supported
|
|
52
|
+
'prompt_cache_key', // OpenAI prompt-caching hint; NVIDIA rejects with 400
|
|
51
53
|
'store', // GLM rejects
|
|
52
54
|
]
|
|
53
55
|
|
|
@@ -146,6 +148,20 @@ export function normalizeMistral(body) {
|
|
|
146
148
|
return result
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* 📖 `normalizeNvidia` — transforms a request body for the `nvidia` provider.
|
|
153
|
+
*
|
|
154
|
+
* 1. Strips parameters NVIDIA rejects with 400 (e.g. prompt_cache_key)
|
|
155
|
+
* 2. Removes orphan `tool` messages that lack a matching assistant tool_call
|
|
156
|
+
* 3. Strips `stream_options` when not streaming
|
|
157
|
+
*/
|
|
158
|
+
export function normalizeNvidia(body) {
|
|
159
|
+
if (!body || typeof body !== 'object') return body
|
|
160
|
+
let result = stripUnsupportedParams(body)
|
|
161
|
+
result = dropOrphanToolMessages(result)
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
// 📖 Map of provider key → normalizer function. Providers that don't need
|
|
150
166
|
// 📖 any tweak (most OpenAI-compat ones like Groq, Cerebras, etc.) are not
|
|
151
167
|
// 📖 listed and pass through `normalizeRequestBody` unchanged.
|
|
@@ -153,6 +169,7 @@ export const PROVIDER_NORMALIZERS = {
|
|
|
153
169
|
zai: normalizeZai,
|
|
154
170
|
mistral: normalizeMistral,
|
|
155
171
|
codestral: normalizeMistral,
|
|
172
|
+
nvidia: normalizeNvidia,
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
/**
|
|
@@ -209,6 +209,12 @@ const JCODE_NATIVE_PROVIDERS = {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
function spawnCommand(command, args, env) {
|
|
212
|
+
// 📖 Defensive: ensure stdin is back in cooked mode before handing the TTY to
|
|
213
|
+
// 📖 an interactive child (jcode REPL etc). The TUI leaves raw mode enabled;
|
|
214
|
+
// 📖 if the caller forgot to reset it the child will appear to hang.
|
|
215
|
+
try {
|
|
216
|
+
if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false)
|
|
217
|
+
} catch {}
|
|
212
218
|
return new Promise((resolve, reject) => {
|
|
213
219
|
const child = spawn(command, args, {
|
|
214
220
|
stdio: 'inherit',
|
package/src/core/updater.js
CHANGED
|
@@ -110,6 +110,25 @@ export function isPackageDevMode() {
|
|
|
110
110
|
return process.env.FCM_DEV === '1' || existsSync(join(PACKAGE_ROOT, '.git'))
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* 📖 isRunningInDocker: detect Docker/container environment where global npm writes fail.
|
|
115
|
+
* In Docker the app lives at /app (not /usr/local/lib/node_modules) and runs as non-root `fcm`.
|
|
116
|
+
* Mandatory updates would always EACCES, so we skip them gracefully.
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isRunningInDocker() {
|
|
120
|
+
// 📖 Standard Docker marker file, plus explicit env override for testing
|
|
121
|
+
if (process.env.FCM_DOCKER === '1' || process.env.DOCKER_CONTAINER === '1') return true
|
|
122
|
+
try {
|
|
123
|
+
if (existsSync('/.dockerenv')) return true
|
|
124
|
+
} catch {}
|
|
125
|
+
// 📖 Fallback: check if package is at /app (Docker COPY) vs global prefix
|
|
126
|
+
try {
|
|
127
|
+
if (String(PACKAGE_ROOT) === '/app' || String(PACKAGE_ROOT).startsWith('/app/')) return true
|
|
128
|
+
} catch {}
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
/**
|
|
114
133
|
* 📖 getUpdateInstallFailureCount: sanitized persistent failure counter.
|
|
115
134
|
* @param {object} config
|
|
@@ -265,6 +284,11 @@ export async function enforceMandatoryStartupUpdate(config, options = {}) {
|
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
if (devMode) return base
|
|
287
|
+
if (isRunningInDocker()) {
|
|
288
|
+
// 📖 In Docker, global npm install as non-root always EACCES. Skip mandatory update
|
|
289
|
+
// 📖 and let the container run with the baked-in version. User can rebuild image for updates.
|
|
290
|
+
return base
|
|
291
|
+
}
|
|
268
292
|
|
|
269
293
|
const { latestVersion, error } = await checkForUpdateDetailed()
|
|
270
294
|
base.checked = true
|
package/src/tui/key-handler.js
CHANGED
|
@@ -278,9 +278,7 @@ export function createKeyHandler(ctx) {
|
|
|
278
278
|
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey, ctx: selected.ctx }
|
|
279
279
|
|
|
280
280
|
if (!uiAlreadyStopped) {
|
|
281
|
-
|
|
282
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
283
|
-
stopUi()
|
|
281
|
+
stopUi({ resetRawMode: true })
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
// 📖 If router is enabled, push [selected, ...favorites] to daemon as the active set
|
package/src/tui/tui-state.js
CHANGED
|
@@ -89,6 +89,13 @@ export function createTuiState({
|
|
|
89
89
|
|
|
90
90
|
// 📖 Dynamic normal interval from config (default 10s)
|
|
91
91
|
const normalInterval = config.settings?.pingInterval ?? PING_MODE_INTERVALS.normal
|
|
92
|
+
// 📖 Respect config.settings.pingInterval at startup (issue #155).
|
|
93
|
+
// 📖 CLI override is already merged into config.settings.pingInterval in app.js.
|
|
94
|
+
const configuredInterval = config.settings?.pingInterval
|
|
95
|
+
const hasConfiguredInterval =
|
|
96
|
+
typeof configuredInterval === 'number' && Number.isFinite(configuredInterval) && configuredInterval > 0
|
|
97
|
+
const initialInterval = hasConfiguredInterval ? configuredInterval : PING_MODE_INTERVALS.speed
|
|
98
|
+
const initialMode = intervalToPingMode(initialInterval)
|
|
92
99
|
|
|
93
100
|
return {
|
|
94
101
|
// 📖 Core data: model results (mutated in-place by ping loop)
|
|
@@ -107,14 +114,14 @@ export function createTuiState({
|
|
|
107
114
|
sortDirection: (config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
108
115
|
|
|
109
116
|
// 📖 Ping cadence — drives the interval between background ping cycles
|
|
110
|
-
pingInterval:
|
|
111
|
-
pingMode:
|
|
112
|
-
pingModeSource: 'startup',
|
|
117
|
+
pingInterval: initialInterval,
|
|
118
|
+
pingMode: initialMode,
|
|
119
|
+
pingModeSource: hasConfiguredInterval ? 'config' : 'startup',
|
|
113
120
|
pingModeIntervals: {
|
|
114
121
|
...PING_MODE_INTERVALS,
|
|
115
122
|
normal: normalInterval,
|
|
116
123
|
},
|
|
117
|
-
speedModeUntil: now + SPEED_MODE_DURATION_MS,
|
|
124
|
+
speedModeUntil: initialMode === 'speed' ? now + SPEED_MODE_DURATION_MS : null,
|
|
118
125
|
lastPingTime: now,
|
|
119
126
|
lastUserActivityAt: now,
|
|
120
127
|
resumeSpeedOnActivity: false,
|