free-coding-models 0.5.85 → 0.5.87
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 +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.86.md +10 -0
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +47 -16
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +35 -75
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +46 -5
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-DWUely84.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @exports cleanupLegacyProxyArtifacts
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs'
|
|
26
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs'
|
|
27
27
|
import { homedir } from 'node:os'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
29
|
|
|
@@ -82,7 +82,9 @@ function noteError(summary, filePath, error) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// 📖 Thin wrappers: readJsonFile propagates parse errors (callers have try/catch).
|
|
85
|
-
// 📖 writeJsonFile appends a trailing newline for clean diffs.
|
|
85
|
+
// 📖 writeJsonFile appends a trailing newline for clean diffs. These configs can
|
|
86
|
+
// 📖 carry plaintext API keys, so tighten permissions after every write (chmod
|
|
87
|
+
// 📖 also fixes pre-existing files that were created wide-permission).
|
|
86
88
|
function readJsonFile(filePath, fallback = null) {
|
|
87
89
|
if (!existsSync(filePath)) return fallback
|
|
88
90
|
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
@@ -90,6 +92,7 @@ function readJsonFile(filePath, fallback = null) {
|
|
|
90
92
|
|
|
91
93
|
function writeJsonFile(filePath, value) {
|
|
92
94
|
writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n')
|
|
95
|
+
chmodSync(filePath, 0o600)
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
function deleteFileIfExists(filePath, summary) {
|
|
@@ -262,7 +265,8 @@ function writeSimpleYamlMap(filePath, entries) {
|
|
|
262
265
|
const lines = Object.keys(entries)
|
|
263
266
|
.sort()
|
|
264
267
|
.map((key) => `${key}: ${JSON.stringify(String(entries[key] ?? ''))}`)
|
|
265
|
-
writeFileSync(filePath, lines.join('\n') + '\n')
|
|
268
|
+
writeFileSync(filePath, lines.join('\n') + '\n', { mode: 0o600 })
|
|
269
|
+
chmodSync(filePath, 0o600)
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
function cleanupGoose(paths, summary) {
|
|
@@ -328,8 +332,16 @@ function cleanupAider(filePath, summary) {
|
|
|
328
332
|
if (!existsSync(filePath)) return
|
|
329
333
|
try {
|
|
330
334
|
const content = readFileSync(filePath, 'utf8')
|
|
331
|
-
|
|
335
|
+
// 📖 Only treat the file as proxy-era when BOTH signals hold: the FCM proxy
|
|
336
|
+
// 📖 banner we used to write AND a loopback openai-api-base. Matching on the
|
|
337
|
+
// 📖 loopback base alone deleted unrelated local setups (e.g. ollama) wholesale.
|
|
338
|
+
const isLegacyProxyConfig = content.includes('FCM Proxy V2')
|
|
339
|
+
&& /openai-api-base:\s*http:\/\/127\.0\.0\.1:/i.test(content)
|
|
332
340
|
if (isLegacyProxyConfig) {
|
|
341
|
+
// 📖 Back up before any unlink: the file may contain user edits we cannot
|
|
342
|
+
// 📖 reconstruct.
|
|
343
|
+
const backupPath = `${filePath}.backup-${Date.now()}`
|
|
344
|
+
try { writeFileSync(backupPath, content, { mode: 0o600 }) } catch { /* best-effort */ }
|
|
333
345
|
unlinkSync(filePath)
|
|
334
346
|
noteRemovedFile(summary, filePath)
|
|
335
347
|
}
|
|
@@ -341,7 +353,12 @@ function cleanupAider(filePath, summary) {
|
|
|
341
353
|
function cleanupAmp(filePath, summary) {
|
|
342
354
|
updateJsonFile(filePath, (config) => {
|
|
343
355
|
let removedEntries = 0
|
|
344
|
-
|
|
356
|
+
// 📖 The old proxy wrote its identity into the URL. Only loopback URLs that
|
|
357
|
+
// 📖 are actually FCM proxy artifacts (banner value in the URL) are removed;
|
|
358
|
+
// 📖 a user's own localhost amp.url (ollama, llama.cpp, ...) stays untouched.
|
|
359
|
+
const usesLegacyLocalhost = typeof config['amp.url'] === 'string'
|
|
360
|
+
&& /127\.0\.0\.1|localhost/.test(config['amp.url'])
|
|
361
|
+
&& /fcm|free-coding-models/i.test(config['amp.url'])
|
|
345
362
|
if (usesLegacyLocalhost) {
|
|
346
363
|
delete config['amp.url']
|
|
347
364
|
removedEntries += 1
|
package/src/core/model-merger.js
CHANGED
|
@@ -160,6 +160,10 @@ export async function overlayModelsDevMetadata(mergedModels, opts = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
if (!bestMatch && m.slug) bestMatch = lookup(m.slug, m.label, index)
|
|
163
|
+
// 📖 Skip substring matches entirely: a label-only match can bind the WRONG
|
|
164
|
+
// 📖 catalog model and overwrite the curated ctx window with its value
|
|
165
|
+
// 📖 (detectDrift ignores substring matches for the same reason).
|
|
166
|
+
if (bestMatch && bestMatch.matchKind === 'substring') bestMatch = null
|
|
163
167
|
if (!bestMatch) {
|
|
164
168
|
if (opts.mutate) {
|
|
165
169
|
Object.assign(m, { modelsDevMeta: null, metaSource: 'sources.js' })
|
package/src/core/models-drift.js
CHANGED
|
@@ -131,8 +131,10 @@ export function detectDrift(models, catalog, opts = {}) {
|
|
|
131
131
|
for (const entry of models) {
|
|
132
132
|
// 📖 Accept both tuple form and object form
|
|
133
133
|
let modelId, label, ctx, reasoning, vision, thinking, maxTokens
|
|
134
|
+
let isTupleForm = false
|
|
134
135
|
if (Array.isArray(entry)) {
|
|
135
136
|
[modelId, label, , , ctx] = entry
|
|
137
|
+
isTupleForm = true
|
|
136
138
|
// 📖 The 6th+ elements of the sources.js tuple are providerKey + metadata;
|
|
137
139
|
// 📖 for drift we only need the first 5.
|
|
138
140
|
} else if (entry && typeof entry === 'object') {
|
|
@@ -162,6 +164,13 @@ export function detectDrift(models, catalog, opts = {}) {
|
|
|
162
164
|
matchKind,
|
|
163
165
|
})
|
|
164
166
|
}
|
|
167
|
+
|
|
168
|
+
// 📖 Tuple-form entries carry no capability flags or maxTokens (arity 5), so
|
|
169
|
+
// 📖 flagMatch(undefined, true) used to report a bogus "add" for every capable
|
|
170
|
+
// 📖 model and bury the real ctx drift under noise. Only object-form entries
|
|
171
|
+
// 📖 (which can actually hold these fields) are compared.
|
|
172
|
+
if (isTupleForm) continue
|
|
173
|
+
|
|
165
174
|
// maxTokens
|
|
166
175
|
const maxCmp = numMatch(maxTokens, norm.maxOutputTokens)
|
|
167
176
|
if (maxCmp.status === 'drift' || maxCmp.status === 'add') {
|
|
@@ -8,37 +8,89 @@
|
|
|
8
8
|
* 📖 This module deliberately stays tiny so OpenCode launch code is not
|
|
9
9
|
* 📖 coupled to old bridge-specific sync behavior anymore.
|
|
10
10
|
*
|
|
11
|
+
* 📖 Data-loss guard: an existing opencode.json that fails to parse returns
|
|
12
|
+
* 📖 null from `loadOpenCodeConfig`, and `saveOpenCodeConfig` refuses to write
|
|
13
|
+
* 📖 over an unreadable file. Saving from the empty `{}` base would otherwise
|
|
14
|
+
* 📖 silently replace the user's whole config with a near-empty one.
|
|
15
|
+
*
|
|
11
16
|
* @functions
|
|
12
|
-
* → `loadOpenCodeConfig`
|
|
13
|
-
* → `saveOpenCodeConfig`
|
|
17
|
+
* → `loadOpenCodeConfig` - read `~/.config/opencode/opencode.json` safely (null when unreadable)
|
|
18
|
+
* → `saveOpenCodeConfig` - write `opencode.json` atomically with a simple backup
|
|
14
19
|
* → `restoreOpenCodeBackup` — restore the last `.bak` copy if needed
|
|
15
20
|
*
|
|
16
21
|
* @exports loadOpenCodeConfig, saveOpenCodeConfig, restoreOpenCodeBackup
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
|
-
import { readFileSync,
|
|
24
|
+
import { readFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
20
25
|
import { join } from 'node:path'
|
|
21
26
|
import { homedir } from 'node:os'
|
|
27
|
+
import { atomicWriteJson } from './shared-helpers.js'
|
|
22
28
|
|
|
23
29
|
const OPENCODE_CONFIG_DIR = join(homedir(), '.config', 'opencode')
|
|
24
30
|
const OPENCODE_CONFIG_PATH = join(OPENCODE_CONFIG_DIR, 'opencode.json')
|
|
25
31
|
const OPENCODE_BACKUP_PATH = join(OPENCODE_CONFIG_DIR, 'opencode.json.bak')
|
|
26
32
|
|
|
33
|
+
const UNREADABLE_CONFIG_HINT = ` ⚠ ${OPENCODE_CONFIG_PATH} contains invalid JSON and was NOT modified. Fix or remove the file, then retry.`
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 📖 True when the file exists, is non-empty, and does not parse as JSON.
|
|
37
|
+
* 📖 Missing and empty files are considered readable (nothing to lose).
|
|
38
|
+
*/
|
|
39
|
+
function isExistingFileUnreadable(configPath) {
|
|
40
|
+
if (!existsSync(configPath)) return false
|
|
41
|
+
try {
|
|
42
|
+
const raw = readFileSync(configPath, 'utf8')
|
|
43
|
+
if (!raw.trim()) return false
|
|
44
|
+
JSON.parse(raw)
|
|
45
|
+
return false
|
|
46
|
+
} catch {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
export function loadOpenCodeConfig() {
|
|
28
52
|
try {
|
|
29
53
|
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
30
|
-
|
|
54
|
+
const raw = readFileSync(OPENCODE_CONFIG_PATH, 'utf8')
|
|
55
|
+
// 📖 Missing file and empty file behave as before: fresh empty base.
|
|
56
|
+
if (!raw.trim()) return {}
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw)
|
|
59
|
+
} catch {
|
|
60
|
+
// 📖 Existing non-empty file with broken JSON: signal "unreadable" so
|
|
61
|
+
// 📖 callers never build a save on an empty base and wipe the file.
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
31
64
|
}
|
|
32
65
|
} catch {}
|
|
33
66
|
return {}
|
|
34
67
|
}
|
|
35
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 📖 Save opencode.json atomically (tmp + rename) with a one-shot backup.
|
|
71
|
+
* 📖 Returns false and aborts (with a clear message) instead of overwriting
|
|
72
|
+
* 📖 when the config is null or the on-disk file is unreadable.
|
|
73
|
+
* @param {object|null} config
|
|
74
|
+
* @returns {boolean} true when written
|
|
75
|
+
*/
|
|
36
76
|
export function saveOpenCodeConfig(config) {
|
|
77
|
+
if (config === null || config === undefined) {
|
|
78
|
+
console.error(UNREADABLE_CONFIG_HINT)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
// 📖 Belt-and-suspenders for callers that normalized null to {}: never let a
|
|
82
|
+
// 📖 stale in-memory copy clobber a file we cannot read.
|
|
83
|
+
if (isExistingFileUnreadable(OPENCODE_CONFIG_PATH)) {
|
|
84
|
+
console.error(UNREADABLE_CONFIG_HINT)
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
37
87
|
mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true })
|
|
38
88
|
if (existsSync(OPENCODE_CONFIG_PATH)) {
|
|
39
89
|
copyFileSync(OPENCODE_CONFIG_PATH, OPENCODE_BACKUP_PATH)
|
|
40
90
|
}
|
|
41
|
-
|
|
91
|
+
// 📖 Atomic write: a crash mid-write must never truncate the shared config.
|
|
92
|
+
atomicWriteJson(OPENCODE_CONFIG_PATH, config, 0o600)
|
|
93
|
+
return true
|
|
42
94
|
}
|
|
43
95
|
|
|
44
96
|
export function restoreOpenCodeBackup() {
|
package/src/core/opencode.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Auto-pick tmux port for OpenCode sub-agents
|
|
15
15
|
*
|
|
16
16
|
* → Functions:
|
|
17
|
-
* - `setOpenCodeModelData`
|
|
17
|
+
* - `setOpenCodeModelData` - No-op kept for call-site stability (dead refs removed)
|
|
18
18
|
* - `startOpenCode` — Launch OpenCode CLI with selected model
|
|
19
19
|
* - `startOpenCodeDesktop` — Set model and open Desktop app
|
|
20
20
|
*
|
|
@@ -41,15 +41,10 @@ const OPENCODE_CONFIG = join(homedir(), '.config', 'opencode', 'opencode.json')
|
|
|
41
41
|
const OPENCODE_PORT_RANGE_START = 4096
|
|
42
42
|
const OPENCODE_PORT_RANGE_END = 5096
|
|
43
43
|
|
|
44
|
-
// 📖
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// 📖 setOpenCodeModelData: Provide mergedModels + mergedModelByLabel to this module.
|
|
49
|
-
export function setOpenCodeModelData(mergedModels, mergedModelByLabel) {
|
|
50
|
-
mergedModelsRef = Array.isArray(mergedModels) ? mergedModels : []
|
|
51
|
-
mergedModelByLabelRef = mergedModelByLabel instanceof Map ? mergedModelByLabel : new Map()
|
|
52
|
-
}
|
|
44
|
+
// 📖 setOpenCodeModelData: kept as a stable no-op export. The merged model refs it
|
|
45
|
+
// 📖 used to store were written but never read anywhere (verified by grep), so the
|
|
46
|
+
// 📖 dead module state was removed; src/tui/app.js still calls this on startup.
|
|
47
|
+
export function setOpenCodeModelData() {}
|
|
53
48
|
|
|
54
49
|
// 📖 isTcpPortAvailable: checks if a local TCP port is free for OpenCode.
|
|
55
50
|
// 📖 Used to avoid tmux sub-agent port conflicts when multiple projects run in parallel.
|
|
@@ -147,7 +142,19 @@ async function createZaiProxy(apiKey) {
|
|
|
147
142
|
res.writeHead(proxyRes.statusCode, proxyRes.headers)
|
|
148
143
|
proxyRes.pipe(res)
|
|
149
144
|
})
|
|
150
|
-
proxyReq.on('error', () => {
|
|
145
|
+
proxyReq.on('error', () => {
|
|
146
|
+
// 📖 Headers may already be flushed to the client mid-stream; writeHead after
|
|
147
|
+
// 📖 that throws ERR_HTTP_HEADERS_SENT and masks the real proxy error.
|
|
148
|
+
if (!res.headersSent) res.writeHead(502)
|
|
149
|
+
res.end()
|
|
150
|
+
})
|
|
151
|
+
// 📖 Hang guard: an upstream that never responds used to stall the client
|
|
152
|
+
// 📖 forever. Destroy the socket and answer 502 when headers are not sent yet.
|
|
153
|
+
proxyReq.setTimeout(15000, () => {
|
|
154
|
+
proxyReq.destroy(new Error('ZAI proxy upstream timeout'))
|
|
155
|
+
if (!res.headersSent) res.writeHead(502)
|
|
156
|
+
res.end()
|
|
157
|
+
})
|
|
151
158
|
req.pipe(proxyReq)
|
|
152
159
|
})
|
|
153
160
|
await new Promise(r => server.listen(0, '127.0.0.1', r))
|
|
@@ -196,7 +203,9 @@ async function spawnOpenCode(args, providerKey, fcmConfig, existingZaiProxy = nu
|
|
|
196
203
|
const { spawn } = await import('child_process')
|
|
197
204
|
const child = spawn(resolveToolBinaryPath('opencode') || 'opencode', finalArgs, {
|
|
198
205
|
stdio: 'inherit',
|
|
199
|
-
shell
|
|
206
|
+
// 📖 Windows keeps a shell because npm-installed CLIs resolve to .cmd shims;
|
|
207
|
+
// 📖 everywhere else the resolved absolute binary path is spawned directly.
|
|
208
|
+
shell: isWindows,
|
|
200
209
|
detached: false,
|
|
201
210
|
env: childEnv
|
|
202
211
|
})
|
|
@@ -207,7 +216,7 @@ async function spawnOpenCode(args, providerKey, fcmConfig, existingZaiProxy = nu
|
|
|
207
216
|
// 📖 ZAI cleanup: remove the ephemeral proxy provider from opencode.json
|
|
208
217
|
if (providerKey === 'zai') {
|
|
209
218
|
try {
|
|
210
|
-
const cfg = loadOpenCodeConfig()
|
|
219
|
+
const cfg = loadOpenCodeConfig() || {}
|
|
211
220
|
if (cfg.provider?.zai) delete cfg.provider.zai
|
|
212
221
|
if (typeof cfg.model === 'string' && cfg.model.startsWith('zai/')) delete cfg.model
|
|
213
222
|
saveOpenCodeConfig(cfg)
|
|
@@ -236,7 +245,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
236
245
|
const modelRef = `${providerKey}/${ocModelId}`
|
|
237
246
|
|
|
238
247
|
if (providerKey === 'nvidia') {
|
|
239
|
-
const config = loadOpenCodeConfig()
|
|
248
|
+
const config = loadOpenCodeConfig() || {}
|
|
240
249
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
241
250
|
|
|
242
251
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -271,7 +280,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
271
280
|
|
|
272
281
|
saveOpenCodeConfig(config)
|
|
273
282
|
|
|
274
|
-
const savedConfig = loadOpenCodeConfig()
|
|
283
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
275
284
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
276
285
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
277
286
|
console.log()
|
|
@@ -312,7 +321,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
312
321
|
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
313
322
|
console.log()
|
|
314
323
|
|
|
315
|
-
const config = loadOpenCodeConfig()
|
|
324
|
+
const config = loadOpenCodeConfig() || {}
|
|
316
325
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
317
326
|
|
|
318
327
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -335,7 +344,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
335
344
|
|
|
336
345
|
saveOpenCodeConfig(config)
|
|
337
346
|
|
|
338
|
-
const savedConfig = loadOpenCodeConfig()
|
|
347
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
339
348
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
340
349
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
341
350
|
console.log()
|
|
@@ -361,7 +370,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
361
370
|
console.log(chalk.dim(` Model: ${zenModelRef}`))
|
|
362
371
|
console.log()
|
|
363
372
|
|
|
364
|
-
const config = loadOpenCodeConfig()
|
|
373
|
+
const config = loadOpenCodeConfig() || {}
|
|
365
374
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
366
375
|
|
|
367
376
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -372,7 +381,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
372
381
|
config.model = zenModelRef
|
|
373
382
|
saveOpenCodeConfig(config)
|
|
374
383
|
|
|
375
|
-
const savedConfig = loadOpenCodeConfig()
|
|
384
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
376
385
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
377
386
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
378
387
|
console.log()
|
|
@@ -392,7 +401,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
392
401
|
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
393
402
|
console.log()
|
|
394
403
|
|
|
395
|
-
const config = loadOpenCodeConfig()
|
|
404
|
+
const config = loadOpenCodeConfig() || {}
|
|
396
405
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
397
406
|
|
|
398
407
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -559,7 +568,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
559
568
|
config.model = modelRef
|
|
560
569
|
saveOpenCodeConfig(config)
|
|
561
570
|
|
|
562
|
-
const savedConfig = loadOpenCodeConfig()
|
|
571
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
563
572
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
564
573
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
565
574
|
console.log()
|
|
@@ -587,7 +596,7 @@ export async function startOpenCodeWeb(model, fcmConfig) {
|
|
|
587
596
|
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
588
597
|
console.log()
|
|
589
598
|
|
|
590
|
-
const config = loadOpenCodeConfig()
|
|
599
|
+
const config = loadOpenCodeConfig() || {}
|
|
591
600
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
592
601
|
|
|
593
602
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -639,33 +648,50 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
639
648
|
const ocModelId = getOpenCodeModelId(providerKey, model.modelId)
|
|
640
649
|
const modelRef = `${providerKey}/${ocModelId}`
|
|
641
650
|
|
|
651
|
+
const printDesktopLaunchError = () => {
|
|
652
|
+
console.error(chalk.red(' Could not open OpenCode Desktop'))
|
|
653
|
+
if (isWindows) {
|
|
654
|
+
console.error(chalk.dim(' Make sure OpenCode is installed from https://opencode.ai'))
|
|
655
|
+
} else if (isLinux) {
|
|
656
|
+
console.error(chalk.dim(' Install via: snap install opencode OR flatpak install ai.opencode.OpenCode'))
|
|
657
|
+
console.error(chalk.dim(' Or download from https://opencode.ai'))
|
|
658
|
+
} else {
|
|
659
|
+
console.error(chalk.dim(' Is it installed at /Applications/OpenCode.app?'))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
642
663
|
const launchDesktop = async () => {
|
|
664
|
+
// 📖 Linux: try each launcher directly (shell:false) so modelRef is never
|
|
665
|
+
// 📖 interpolated into a shell command string. The final candidate opens the
|
|
666
|
+
// 📖 .desktop entry without a model argument.
|
|
667
|
+
if (isLinux) {
|
|
668
|
+
const { execFile } = await import('child_process')
|
|
669
|
+
const tryExecFile = (cmd, args) => new Promise((resolve) => {
|
|
670
|
+
execFile(cmd, args, (err) => resolve(!err))
|
|
671
|
+
})
|
|
672
|
+
const launched =
|
|
673
|
+
(await tryExecFile('opencode-desktop', ['--model', modelRef])) ||
|
|
674
|
+
(await tryExecFile('flatpak', ['run', 'ai.opencode.OpenCode', '--model', modelRef])) ||
|
|
675
|
+
(await tryExecFile('snap', ['run', 'opencode', '--model', modelRef])) ||
|
|
676
|
+
(await tryExecFile('xdg-open', ['/usr/share/applications/opencode.desktop']))
|
|
677
|
+
if (!launched) printDesktopLaunchError()
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
643
681
|
const { exec } = await import('child_process')
|
|
644
682
|
let command
|
|
645
683
|
if (isMac) {
|
|
646
684
|
command = 'open -a OpenCode'
|
|
647
685
|
} else if (isWindows) {
|
|
648
686
|
command = 'start "" "%LOCALAPPDATA%\\Programs\\OpenCode\\OpenCode.exe" 2>nul || start "" "%PROGRAMFILES%\\OpenCode\\OpenCode.exe" 2>nul || start OpenCode'
|
|
649
|
-
} else if (isLinux) {
|
|
650
|
-
command = `opencode-desktop --model ${modelRef} 2>/dev/null || flatpak run ai.opencode.OpenCode --model ${modelRef} 2>/dev/null || snap run opencode --model ${modelRef} 2>/dev/null || xdg-open /usr/share/applications/opencode.desktop 2>/dev/null || echo "OpenCode not found"`
|
|
651
687
|
}
|
|
652
688
|
exec(command, (err) => {
|
|
653
|
-
if (err)
|
|
654
|
-
console.error(chalk.red(' Could not open OpenCode Desktop'))
|
|
655
|
-
if (isWindows) {
|
|
656
|
-
console.error(chalk.dim(' Make sure OpenCode is installed from https://opencode.ai'))
|
|
657
|
-
} else if (isLinux) {
|
|
658
|
-
console.error(chalk.dim(' Install via: snap install opencode OR flatpak install ai.opencode.OpenCode'))
|
|
659
|
-
console.error(chalk.dim(' Or download from https://opencode.ai'))
|
|
660
|
-
} else {
|
|
661
|
-
console.error(chalk.dim(' Is it installed at /Applications/OpenCode.app?'))
|
|
662
|
-
}
|
|
663
|
-
}
|
|
689
|
+
if (err) printDesktopLaunchError()
|
|
664
690
|
})
|
|
665
691
|
}
|
|
666
692
|
|
|
667
693
|
if (providerKey === 'nvidia') {
|
|
668
|
-
const config = loadOpenCodeConfig()
|
|
694
|
+
const config = loadOpenCodeConfig() || {}
|
|
669
695
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
670
696
|
|
|
671
697
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -700,7 +726,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
700
726
|
|
|
701
727
|
saveOpenCodeConfig(config)
|
|
702
728
|
|
|
703
|
-
const savedConfig = loadOpenCodeConfig()
|
|
729
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
704
730
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
705
731
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
706
732
|
console.log()
|
|
@@ -741,7 +767,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
741
767
|
console.log(chalk.dim(` Model: ${zenModelRef}`))
|
|
742
768
|
console.log()
|
|
743
769
|
|
|
744
|
-
const config = loadOpenCodeConfig()
|
|
770
|
+
const config = loadOpenCodeConfig() || {}
|
|
745
771
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
746
772
|
|
|
747
773
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -752,7 +778,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
752
778
|
config.model = zenModelRef
|
|
753
779
|
saveOpenCodeConfig(config)
|
|
754
780
|
|
|
755
|
-
const savedConfig = loadOpenCodeConfig()
|
|
781
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
756
782
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
757
783
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
758
784
|
console.log()
|
|
@@ -774,7 +800,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
774
800
|
console.log(chalk.dim(` Model: ${modelRef}`))
|
|
775
801
|
console.log()
|
|
776
802
|
|
|
777
|
-
const config = loadOpenCodeConfig()
|
|
803
|
+
const config = loadOpenCodeConfig() || {}
|
|
778
804
|
const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
|
|
779
805
|
|
|
780
806
|
if (existsSync(getOpenCodeConfigPath())) {
|
|
@@ -941,7 +967,7 @@ export async function startOpenCodeDesktop(model, fcmConfig) {
|
|
|
941
967
|
config.model = modelRef
|
|
942
968
|
saveOpenCodeConfig(config)
|
|
943
969
|
|
|
944
|
-
const savedConfig = loadOpenCodeConfig()
|
|
970
|
+
const savedConfig = loadOpenCodeConfig() || {}
|
|
945
971
|
console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
|
|
946
972
|
console.log(chalk.dim(` Default model in config: ${savedConfig.model || 'NOT SET'}`))
|
|
947
973
|
console.log()
|
package/src/core/playground.js
CHANGED
|
@@ -190,6 +190,11 @@ export async function playgroundSubmit(state, deps = {}) {
|
|
|
190
190
|
const url = `http://127.0.0.1:${port}/v1/chat/completions`
|
|
191
191
|
const fetchFn = deps.fetchFn || globalThis.fetch
|
|
192
192
|
|
|
193
|
+
// 📖 Non-streaming responses have no per-chunk timer, so give the whole
|
|
194
|
+
// 📖 request the same 90s ceiling. The streaming branch switches to its own
|
|
195
|
+
// 📖 per-chunk timer inside readSseStream.
|
|
196
|
+
let requestTimer = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
|
|
197
|
+
|
|
193
198
|
try {
|
|
194
199
|
const resp = await fetchFn(url, {
|
|
195
200
|
method: 'POST',
|
|
@@ -205,6 +210,7 @@ export async function playgroundSubmit(state, deps = {}) {
|
|
|
205
210
|
return
|
|
206
211
|
}
|
|
207
212
|
if (PLAYGROUND_OVERLAY_STATE.streamOn) {
|
|
213
|
+
clearTimeout(requestTimer)
|
|
208
214
|
await readSseStream(resp, fetchFn, controller)
|
|
209
215
|
} else {
|
|
210
216
|
const json = await resp.json().catch(() => null)
|
|
@@ -231,6 +237,7 @@ export async function playgroundSubmit(state, deps = {}) {
|
|
|
231
237
|
finalizeAssistantMeta({ error: PLAYGROUND_OVERLAY_STATE.lastError, aborted: true })
|
|
232
238
|
}
|
|
233
239
|
} finally {
|
|
240
|
+
clearTimeout(requestTimer)
|
|
234
241
|
PLAYGROUND_OVERLAY_STATE.busy = false
|
|
235
242
|
PLAYGROUND_OVERLAY_STATE.abortController = null
|
|
236
243
|
PLAYGROUND_OVERLAY_STATE.statusMessage = null
|
package/src/core/probe-cache.js
CHANGED
|
@@ -104,6 +104,14 @@ const CACHE_DIRNAME = 'free-coding-models'
|
|
|
104
104
|
let _cache = null
|
|
105
105
|
let _cacheLoadedFrom = null // path we last loaded from (for write-back)
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 📖 Composite `provider::model` keys pruned this session. flushCache() re-reads
|
|
109
|
+
* 📖 the disk file before writing, and the read-merge-write pass would otherwise
|
|
110
|
+
* 📖 resurrect exactly the entries pruneStaleEntries() just deleted (the disk file
|
|
111
|
+
* 📖 still contains them until our write lands).
|
|
112
|
+
*/
|
|
113
|
+
const _sessionPrunedKeys = new Set()
|
|
114
|
+
|
|
107
115
|
// ─── Path resolution ──────────────────────────────────────────────────────────
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -180,16 +188,24 @@ export function loadCache({ path: cachePath } = {}) {
|
|
|
180
188
|
* 📖 from incoming do NOT delete fields from base. Used by flushCache to merge
|
|
181
189
|
* 📖 our in-memory mirror with whatever the on-disk file now contains (covers
|
|
182
190
|
* 📖 the daemon + CLI running concurrently case).
|
|
191
|
+
*
|
|
192
|
+
* 📖 `excludeKeys` (optional Set of `provider::model`) drops matching base
|
|
193
|
+
* 📖 entries: keys pruned this session must stay pruned instead of coming back
|
|
194
|
+
* 📖 from the stale disk snapshot.
|
|
183
195
|
*/
|
|
184
|
-
function mergeCache(base, incoming) {
|
|
196
|
+
function mergeCache(base, incoming, excludeKeys = null) {
|
|
185
197
|
if (!base || typeof base !== 'object') return incoming
|
|
186
198
|
if (!incoming || typeof incoming !== 'object') return base
|
|
187
199
|
const out = { ...incoming, providers: { ...(incoming.providers || {}) } }
|
|
188
200
|
for (const [providerKey, providerBucket] of Object.entries(base.providers || {})) {
|
|
189
201
|
const incomingBucket = out.providers[providerKey] || { models: {} }
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
const mergedModels = { ...(providerBucket?.models || {}), ...(incomingBucket.models || {}) }
|
|
203
|
+
if (excludeKeys && excludeKeys.size > 0) {
|
|
204
|
+
for (const modelId of Object.keys(mergedModels)) {
|
|
205
|
+
if (excludeKeys.has(`${providerKey}::${modelId}`)) delete mergedModels[modelId]
|
|
206
|
+
}
|
|
192
207
|
}
|
|
208
|
+
out.providers[providerKey] = { models: mergedModels }
|
|
193
209
|
}
|
|
194
210
|
return out
|
|
195
211
|
}
|
|
@@ -220,12 +236,15 @@ export function flushCache({ path: cachePath, cache } = {}) {
|
|
|
220
236
|
onDisk = null
|
|
221
237
|
}
|
|
222
238
|
|
|
223
|
-
const merged = onDisk ? mergeCache(onDisk, localData) : localData
|
|
239
|
+
const merged = onDisk ? mergeCache(onDisk, localData, _sessionPrunedKeys) : localData
|
|
224
240
|
|
|
225
241
|
try {
|
|
226
242
|
atomicWriteJson(target, merged, 0o600)
|
|
227
243
|
_cacheLoadedFrom = target
|
|
228
244
|
_cache = merged
|
|
245
|
+
// 📖 Deletions are now persisted: clear the set so entries re-added later by
|
|
246
|
+
// 📖 another process (e.g. the model returned to the catalog) still merge in.
|
|
247
|
+
_sessionPrunedKeys.clear()
|
|
229
248
|
return true
|
|
230
249
|
} catch {
|
|
231
250
|
return false
|
|
@@ -244,6 +263,8 @@ export function clearCache({ path: cachePath } = {}) {
|
|
|
244
263
|
const target = cachePath ?? getProbeCachePath()
|
|
245
264
|
_cache = null
|
|
246
265
|
_cacheLoadedFrom = null
|
|
266
|
+
// 📖 The file is gone, so tracked deletions have nothing left to override.
|
|
267
|
+
_sessionPrunedKeys.clear()
|
|
247
268
|
try {
|
|
248
269
|
fs.unlinkSync(target)
|
|
249
270
|
return true
|
|
@@ -406,6 +427,11 @@ export function recordProbeResults(providerKey, results, opts = {}) {
|
|
|
406
427
|
probeVersion: CURRENT_PROBE_VERSION,
|
|
407
428
|
consecutiveFailures,
|
|
408
429
|
}
|
|
430
|
+
// 📖 A live re-probe of a pruned model means it is catalogued again: stop
|
|
431
|
+
// 📖 excluding it from disk merges.
|
|
432
|
+
if (!opts || !Object.prototype.hasOwnProperty.call(opts, 'cache')) {
|
|
433
|
+
_sessionPrunedKeys.delete(`${providerKey}::${r.modelId}`)
|
|
434
|
+
}
|
|
409
435
|
written++
|
|
410
436
|
} catch {
|
|
411
437
|
dropped++
|
|
@@ -553,6 +579,11 @@ export function pruneStaleEntries(providerKey, liveModelIds, opts = {}) {
|
|
|
553
579
|
for (const id of Object.keys(bucket)) {
|
|
554
580
|
if (!live.has(id)) {
|
|
555
581
|
delete bucket[id]
|
|
582
|
+
// 📖 Remember the deletion so flushCache's read-merge-write does not
|
|
583
|
+
// 📖 resurrect this entry from the still-stale disk file.
|
|
584
|
+
if (!opts || !Object.prototype.hasOwnProperty.call(opts, 'cache')) {
|
|
585
|
+
_sessionPrunedKeys.add(`${providerKey}::${id}`)
|
|
586
|
+
}
|
|
556
587
|
pruned++
|
|
557
588
|
}
|
|
558
589
|
}
|
|
@@ -305,7 +305,11 @@ export async function runProviderKeyTest(apiKey, providerKey, source, options =
|
|
|
305
305
|
headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
|
|
306
306
|
headers['X-Title'] = 'free-coding-models'
|
|
307
307
|
}
|
|
308
|
-
const modelsResp = await fetch(modelsUrl, {
|
|
308
|
+
const modelsResp = await fetch(modelsUrl, {
|
|
309
|
+
headers,
|
|
310
|
+
// 📖 Same 8s ceiling as the fast-path probes so discovery can never hang.
|
|
311
|
+
signal: AbortSignal.timeout(8000),
|
|
312
|
+
})
|
|
309
313
|
if (modelsResp.ok) {
|
|
310
314
|
const data = await modelsResp.json()
|
|
311
315
|
discoveredModelIds.push(...parseProviderModelIds(data))
|
|
@@ -316,7 +320,11 @@ export async function runProviderKeyTest(apiKey, providerKey, source, options =
|
|
|
316
320
|
discoveryNote = `Live model discovery returned HTTP ${modelsResp.status}; falling back to the repo catalog.`
|
|
317
321
|
}
|
|
318
322
|
} catch (err) {
|
|
319
|
-
|
|
323
|
+
// 📖 AbortSignal.timeout rejects with TimeoutError; treat it like the fast-path timeout.
|
|
324
|
+
const timedOut = err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
325
|
+
discoveryNote = timedOut
|
|
326
|
+
? 'Live model discovery timed out; falling back to the repo catalog.'
|
|
327
|
+
: `Live model discovery failed (${err?.name || 'error'}); falling back to the repo catalog.`
|
|
320
328
|
}
|
|
321
329
|
}
|
|
322
330
|
|
|
@@ -67,6 +67,16 @@ export const ENV_VAR_NAMES = {
|
|
|
67
67
|
requesty: 'REQUESTY_API_KEY',
|
|
68
68
|
orcarouter: 'ORCAROUTER_API_KEY',
|
|
69
69
|
'vercel-gateway': 'VERCEL_AI_GATEWAY_API_KEY',
|
|
70
|
+
// 📖 Providers whose OpenCode/Kilo config writers reference {env:VAR} - names
|
|
71
|
+
// 📖 must match those tables exactly (see opencode.js / kilo.js).
|
|
72
|
+
huggingface: 'HUGGINGFACE_API_KEY',
|
|
73
|
+
deepinfra: 'DEEPINFRA_API_KEY',
|
|
74
|
+
fireworks: 'FIREWORKS_API_KEY',
|
|
75
|
+
together: 'TOGETHER_API_KEY',
|
|
76
|
+
hyperbolic: 'HYPERBOLIC_API_KEY',
|
|
77
|
+
perplexity: 'PERPLEXITY_API_KEY',
|
|
78
|
+
chutes: 'CHUTES_API_KEY',
|
|
79
|
+
iflow: 'IFLOW_API_KEY',
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
// 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|