free-coding-models 0.5.37 → 0.5.39
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.38.md +15 -0
- package/changelog/v0.5.39.md +16 -0
- package/package.json +1 -1
- package/sources.js +19 -23
- package/src/core/endpoint-installer.js +182 -1
- package/src/core/installed-models-manager.js +192 -36
- package/src/core/tool-launchers.js +148 -25
- package/src/core/utils.js +4 -2
- package/src/tui/app.js +5 -4
- package/src/tui/key-handler.js +7 -6
- package/web/dist/assets/{index-DcYKZCte.js → index-XA_bepbX.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/src/components/install/InstallEndpointsView.jsx +1 -0
- package/web/src/utils/m3.js +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog v0.5.38 - 2026-06-27
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- feat(zcode): add full ZCode install-target support across all surfaces (PR #128)
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
- fix: regenerate pnpm-lock.yaml to unblock Docker CI (PR #123)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- ci: pnpm-lock.yaml sync for @tanstack/react-virtual (PR #123)
|
|
11
|
+
|
|
12
|
+
### Closed (unresolved conflicts)
|
|
13
|
+
- deps(deps-dev): bump vite-plus from 0.1.24 to 0.2.1 (PR #127) - Closed due to unresolvable pnpm-lock.yaml conflicts
|
|
14
|
+
- deps(deps): bump kandown from 0.8.0 to 0.13.1 (PR #126) - Closed due to unresolvable pnpm-lock.yaml conflicts
|
|
15
|
+
- ci(deps): bump actions/checkout from 4 to 7 (PR #125) - Skipped due to failing audit check
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog v0.5.39 - 2026-06-27
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- **groq**: +`qwen/qwen3.6-27b` (preview, 131k, A tier)
|
|
5
|
+
- **cloudflare**: +`@cf/zai-org/glm-5.2` (S+, 262k), +`@cf/moonshotai/kimi-k2.5` (S+, 256k)
|
|
6
|
+
- **ovhcloud**: +`Qwen3-Embedding-8B`, +`bge-m3`, +`bge-multilingual-gemma2` (embeddings)
|
|
7
|
+
- **openrouter**: +`cohere/north-mini-code:free` (S tier, 256k), +`nvidia/nemotron-3-ultra-550b-a55b:free` (S+ tier, 1M)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **openrouter**: full cleanup — removed 11 models no longer on free tier (`qwen3-coder`, `glm-4.5-air`, `kimi-k2.6`, `qwen3-next-80b`, `arcee-trinity`, `hermes-3-405b`, `dolphin-mistral`, `llama-3.3-70b`, `llama-3.2-3b`, `lfm-2.5-1.2b`, `lfm-2.5-thinking`), updated tiers to match live free models page
|
|
11
|
+
- **cloudflare**: fixed `nemotron-3-120b-a12b` context window 32k→128k
|
|
12
|
+
- **openrouter**: corrected ctx for `laguna-xs.2` (131k→262k), `gemma-4-31b` (256k→262k), `gemma-4-26b` (256k→262k), `nemotron-3-super` (262k→1M)
|
|
13
|
+
- **openrouter**: `gemma-4-26b-a4b-it` tier A-→A (corrected)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **openrouter**: `nemotron-3-super-120b-a12b:free` ctx 262k→1M (confirmed on openrouter.ai)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.39",
|
|
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",
|
package/sources.js
CHANGED
|
@@ -86,6 +86,7 @@ export const groq = [
|
|
|
86
86
|
['openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '60.0%', '131k'],
|
|
87
87
|
['openai/gpt-oss-20b', 'GPT OSS 20B', 'A', '42.0%', '131k'],
|
|
88
88
|
['qwen/qwen3-32b', 'Qwen3 32B', 'A+', '50.0%', '131k'],
|
|
89
|
+
['qwen/qwen3.6-27b', 'Qwen3.6 27B', 'A', '-', '131k'],
|
|
89
90
|
['groq/compound', 'Groq Compound', 'A', '45.0%', '131k'],
|
|
90
91
|
['groq/compound-mini', 'Groq Compound Mini', 'B+', '32.0%', '131k'],
|
|
91
92
|
]
|
|
@@ -125,38 +126,27 @@ export const sambanova = [
|
|
|
125
126
|
// 📖 • Free-tier popular models may be additionally rate-limited by the provider itself during peak hours.
|
|
126
127
|
// 📖 API keys at https://openrouter.ai/keys
|
|
127
128
|
export const openrouter = [
|
|
128
|
-
// ── S+ tier —
|
|
129
|
-
['
|
|
130
|
-
['z-ai/glm-4.5-air:free', 'GLM 4.5 Air', 'S+', '72.0%', '131k'],
|
|
129
|
+
// ── S+ tier — confirmed :free on openrouter.ai/collections/free-models ──
|
|
130
|
+
['nvidia/nemotron-3-ultra-550b-a55b:free', 'Nemotron 3 Ultra', 'S+', '-', '1M'],
|
|
131
131
|
['poolside/laguna-m.1:free', 'Poolside Laguna M.1', 'S+', '-', '262k'],
|
|
132
|
-
['poolside/laguna-xs.2:free', 'Poolside Laguna XS.2','S+', '-', '
|
|
133
|
-
|
|
134
|
-
// ── S tier — live :free chat/coding models ──
|
|
135
|
-
['qwen/qwen3-next-80b-a3b-instruct:free', 'Qwen3 80B Instruct', 'S', '65.0%', '262k'],
|
|
132
|
+
['poolside/laguna-xs.2:free', 'Poolside Laguna XS.2','S+', '-', '262k'],
|
|
133
|
+
// ── S tier ──
|
|
136
134
|
['openai/gpt-oss-120b:free', 'GPT OSS 120B', 'S', '60.0%', '131k'],
|
|
137
|
-
['
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
['cohere/north-mini-code:free', 'North Mini Code', 'S', '-', '256k'],
|
|
136
|
+
// ── A+ tier ──
|
|
137
|
+
['nvidia/nemotron-3-super-120b-a12b:free', 'Nemotron 3 Super', 'A+', '56.0%', '1M'],
|
|
140
138
|
['nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', 'Nemotron 3 Omni', 'A+', '52.0%', '256k'],
|
|
141
|
-
['nvidia/nemotron-nano-12b-v2-vl:free', 'Nemotron Nano 12B VL','A', '20.0%', '128k'],
|
|
142
139
|
['openrouter/owl-alpha', 'Owl Alpha', 'A+', '-', '1M'],
|
|
143
|
-
// ── A tier
|
|
144
|
-
['nousresearch/hermes-3-llama-3.1-405b:free', 'Hermes 3 405B', 'A', '44.0%', '131k'],
|
|
140
|
+
// ── A tier ──
|
|
145
141
|
['openai/gpt-oss-20b:free', 'GPT OSS 20B', 'A', '42.0%', '131k'],
|
|
146
142
|
['nvidia/nemotron-nano-30b-a3b:free', 'Nemotron Nano 30B', 'A', '43.0%', '256k'],
|
|
147
|
-
['
|
|
148
|
-
['google/gemma-4-31b-it:free', 'Gemma 4 31B', 'A', '45.0%', '
|
|
149
|
-
['google/gemma-4-26b-a4b-it:free', 'Gemma 4 26B MoE', 'A
|
|
150
|
-
// ── A- tier — confirmed free ──
|
|
151
|
-
['meta-llama/llama-3.3-70b-instruct:free', 'Llama 3.3 70B', 'A-', '39.5%', '131k'],
|
|
152
|
-
['meta-llama/llama-3.2-3b-instruct:free', 'Llama 3.2 3B', 'B', '20.0%', '128k'],
|
|
143
|
+
['nvidia/nemotron-nano-12b-v2-vl:free', 'Nemotron Nano 12B VL','A', '20.0%', '128k'],
|
|
144
|
+
['google/gemma-4-31b-it:free', 'Gemma 4 31B', 'A', '45.0%', '262k'],
|
|
145
|
+
['google/gemma-4-26b-a4b-it:free', 'Gemma 4 26B MoE', 'A', '38.0%', '262k'],
|
|
153
146
|
// ── B+ tier ──
|
|
154
147
|
['nvidia/nemotron-nano-9b-v2:free', 'Nemotron Nano 9B', 'B+', '18.0%', '128k'],
|
|
155
148
|
// ── B tier ──
|
|
156
149
|
['openrouter/free', 'OpenRouter Free', 'B', '-', '200k'],
|
|
157
|
-
// ── C tier ──
|
|
158
|
-
['liquid/lfm-2.5-1.2b-instruct:free', 'LFM 2.5 1.2B', 'C', '-', '32k'],
|
|
159
|
-
['liquid/lfm-2.5-1.2b-thinking:free', 'LFM 2.5 Thinking', 'C', '-', '32k'],
|
|
160
150
|
]
|
|
161
151
|
|
|
162
152
|
// 📖 GitHub Models source - https://models.github.ai
|
|
@@ -270,7 +260,9 @@ export const cloudflare = [
|
|
|
270
260
|
['@cf/zai-org/glm-4.7-flash', 'GLM-4.7-Flash', 'S', '59.2%', '131k'],
|
|
271
261
|
['@cf/openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '60.0%', '128k'],
|
|
272
262
|
// ── A+ tier ──
|
|
273
|
-
['@cf/nvidia/nemotron-3-120b-a12b', 'Nemotron 3 Super', 'A+', '56.0%', '
|
|
263
|
+
['@cf/nvidia/nemotron-3-120b-a12b', 'Nemotron 3 Super', 'A+', '56.0%', '128k'],
|
|
264
|
+
['@cf/zai-org/glm-5.2', 'GLM-5.2', 'S+', '-', '262k'],
|
|
265
|
+
['@cf/moonshotai/kimi-k2.5', 'Kimi K2.5', 'S+', '-', '256k'],
|
|
274
266
|
// ── A tier ──
|
|
275
267
|
['@cf/meta/llama-4-scout-17b-16e-instruct', 'Llama 4 Scout', 'A', '44.0%', '131k'],
|
|
276
268
|
['@cf/mistralai/mistral-7b-instruct-v0.2', 'Mistral 7B v0.2', 'A', '38.0%', '128k'],
|
|
@@ -303,6 +295,10 @@ export const ovhcloud = [
|
|
|
303
295
|
['Mistral-7B-Instruct-v0.3', 'Mistral 7B Instruct', 'B', '25.0%', '127k'],
|
|
304
296
|
['Mistral-Nemo-Instruct-2407', 'Mistral Nemo', 'B+', '30.0%', '118k'],
|
|
305
297
|
['Qwen3.5-9B', 'Qwen3.5 9B', 'B+', '30.0%', '262k'],
|
|
298
|
+
// ── Embeddings ──
|
|
299
|
+
['Qwen3-Embedding-8B', 'Qwen3 Embedding 8B', 'B', '-', '-'],
|
|
300
|
+
['bge-m3', 'BGE M3', 'B', '-', '-'],
|
|
301
|
+
['bge-multilingual-gemma2', 'BGE Multilingual Gemma2','B','-', '-'],
|
|
306
302
|
// Fix (2026-05-26): Qwen3.5-9B ctx 128k→262k, Mistral-Small ctx 131k→128k, Mistral-Nemo ctx 128k→118k, Mistral-7B ctx 32k→127k
|
|
307
303
|
]
|
|
308
304
|
|
|
@@ -54,7 +54,7 @@ import { ensureDir, readJson as sharedReadJson } from './shared-helpers.js'
|
|
|
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
56
|
// 📖 Launch-only tools stay out: the Web dashboard configures endpoints, it never starts CLIs.
|
|
57
|
-
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'forgecode', 'fcm_router']
|
|
57
|
+
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'opencode-web', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp', 'forgecode', 'fcm_router', 'zcode']
|
|
58
58
|
|
|
59
59
|
function getDefaultPaths() {
|
|
60
60
|
const home = homedir()
|
|
@@ -70,6 +70,8 @@ function getDefaultPaths() {
|
|
|
70
70
|
ampConfigPath: join(home, '.config', 'amp', 'settings.json'),
|
|
71
71
|
qwenConfigPath: join(home, '.qwen', 'settings.json'),
|
|
72
72
|
forgeCodeConfigPath: join(home, '.forge', '.forge.toml'),
|
|
73
|
+
zcodeConfigPath: join(home, '.zcode', 'v2', 'config.json'),
|
|
74
|
+
zcodeModelCachePath: join(home, '.zcode', 'v2', 'bots-model-cache.v2.json'),
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -625,6 +627,183 @@ function installIntoFcmRouter(providerKey, models, apiKey) {
|
|
|
625
627
|
return { path: `FCM Router (${baseUrl})`, backupPath: null, providerId: providerKey, modelCount: models.length }
|
|
626
628
|
}
|
|
627
629
|
|
|
630
|
+
// 📖 installIntoZCode: Writes provider + models into ZCode's config.json and updates
|
|
631
|
+
// 📖 bots-model-cache.v2.json so the models appear immediately in ZCode's model picker.
|
|
632
|
+
// 📖 Uses deterministic provider ID (fcm-{providerKey}) so re-running replaces/merges
|
|
633
|
+
// 📖 without duplicating the provider entry.
|
|
634
|
+
// 📖 When scope === 'selected' — merges models with existing ones.
|
|
635
|
+
// 📖 When scope === 'all' — replaces all models (full sync).
|
|
636
|
+
function installIntoZCode(providerKey, models, apiKey, paths, scope) {
|
|
637
|
+
const configPath = paths.zcodeConfigPath
|
|
638
|
+
const cachePath = paths.zcodeModelCachePath
|
|
639
|
+
const providerId = `fcm-${providerKey}`
|
|
640
|
+
const baseUrl = resolveProviderBaseUrl(providerKey)
|
|
641
|
+
const providerLabel = getManagedProviderLabel(providerKey)
|
|
642
|
+
|
|
643
|
+
if (!baseUrl) {
|
|
644
|
+
throw new Error(`Cannot resolve base URL for ${getProviderLabel(providerKey)}`)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function buildModelEntry(model) {
|
|
648
|
+
const ctx = parseContextWindow(model.ctx)
|
|
649
|
+
const entry = {
|
|
650
|
+
id: model.modelId,
|
|
651
|
+
name: model.label || model.modelId,
|
|
652
|
+
kinds: ['openai-compatible'],
|
|
653
|
+
defaultKind: 'openai-compatible',
|
|
654
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
655
|
+
contextWindow: ctx,
|
|
656
|
+
}
|
|
657
|
+
if (ctx > 8192) {
|
|
658
|
+
entry.maxOutputTokens = getDefaultMaxTokens(ctx)
|
|
659
|
+
}
|
|
660
|
+
return entry
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function buildConfigModelEntry(model) {
|
|
664
|
+
const ctx = parseContextWindow(model.ctx)
|
|
665
|
+
const entry = {
|
|
666
|
+
limit: { context: ctx },
|
|
667
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
668
|
+
}
|
|
669
|
+
if (ctx > 8192) {
|
|
670
|
+
entry.limit.output = getDefaultMaxTokens(ctx)
|
|
671
|
+
}
|
|
672
|
+
return entry
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const newModelIds = new Set(models.map((m) => m.modelId))
|
|
676
|
+
let configModified = false
|
|
677
|
+
let cacheModified = false
|
|
678
|
+
|
|
679
|
+
// ── 1. Write to config.json ───────────────────────────────────────────────
|
|
680
|
+
const config = readJson(configPath, { $schema: 'https://opencode.ai/config.json' })
|
|
681
|
+
if (!config.provider || typeof config.provider !== 'object') config.provider = {}
|
|
682
|
+
|
|
683
|
+
const existingProvider = config.provider[providerId]
|
|
684
|
+
|
|
685
|
+
if (scope === 'selected' && existingProvider) {
|
|
686
|
+
// 📖 Merge mode: add/update selected models, keep existing ones
|
|
687
|
+
for (const model of models) {
|
|
688
|
+
existingProvider.models[model.modelId] = buildConfigModelEntry(model)
|
|
689
|
+
}
|
|
690
|
+
configModified = true
|
|
691
|
+
} else if (scope === 'selected' && !existingProvider) {
|
|
692
|
+
// 📖 No existing provider, but scope is selected — create with only selected models
|
|
693
|
+
config.provider[providerId] = {
|
|
694
|
+
name: providerLabel,
|
|
695
|
+
kind: 'openai-compatible',
|
|
696
|
+
options: { apiKey, baseURL: baseUrl, apiKeyRequired: true },
|
|
697
|
+
enabled: true,
|
|
698
|
+
source: 'custom',
|
|
699
|
+
models: Object.fromEntries(models.map((m) => [m.modelId, buildConfigModelEntry(m)])),
|
|
700
|
+
}
|
|
701
|
+
configModified = true
|
|
702
|
+
} else {
|
|
703
|
+
// 📖 scope === 'all' — replace all models (full sync), skip if identical
|
|
704
|
+
const existingModelIds = existingProvider ? Object.keys(existingProvider.models || {}) : []
|
|
705
|
+
const modelsUnchanged = existingProvider
|
|
706
|
+
&& existingModelIds.length === models.length
|
|
707
|
+
&& models.every((m) => existingModelIds.includes(m.modelId))
|
|
708
|
+
|
|
709
|
+
if (!modelsUnchanged) {
|
|
710
|
+
config.provider[providerId] = {
|
|
711
|
+
name: providerLabel,
|
|
712
|
+
kind: 'openai-compatible',
|
|
713
|
+
options: { apiKey, baseURL: baseUrl, apiKeyRequired: true },
|
|
714
|
+
enabled: true,
|
|
715
|
+
source: 'custom',
|
|
716
|
+
models: Object.fromEntries(models.map((m) => [m.modelId, buildConfigModelEntry(m)])),
|
|
717
|
+
}
|
|
718
|
+
configModified = true
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const configBackupPath = configModified ? writeJson(configPath, config) : null
|
|
723
|
+
|
|
724
|
+
// ── 2. Write to bots-model-cache.v2.json ──────────────────────────────────
|
|
725
|
+
const cache = readJson(cachePath, { version: 2, updatedAt: Date.now(), providers: [] })
|
|
726
|
+
if (!Array.isArray(cache.providers)) cache.providers = []
|
|
727
|
+
|
|
728
|
+
const existingCacheIdx = cache.providers.findIndex((p) => p?.id === providerId)
|
|
729
|
+
|
|
730
|
+
if (scope === 'selected') {
|
|
731
|
+
// 📖 Merge mode: add/update selected models in cache, keep existing ones
|
|
732
|
+
if (existingCacheIdx >= 0) {
|
|
733
|
+
const existingModels = cache.providers[existingCacheIdx].models || []
|
|
734
|
+
for (const model of models) {
|
|
735
|
+
const modelIdx = existingModels.findIndex((m) => m.id === model.modelId)
|
|
736
|
+
if (modelIdx >= 0) {
|
|
737
|
+
existingModels[modelIdx] = buildModelEntry(model)
|
|
738
|
+
} else {
|
|
739
|
+
existingModels.push(buildModelEntry(model))
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
cache.providers[existingCacheIdx].updatedAt = Date.now()
|
|
743
|
+
cacheModified = true
|
|
744
|
+
} else {
|
|
745
|
+
cache.providers.push({
|
|
746
|
+
id: providerId,
|
|
747
|
+
name: providerLabel,
|
|
748
|
+
enabled: true,
|
|
749
|
+
endpoints: { baseURL: baseUrl, paths: { 'openai-compatible': '/chat/completions' } },
|
|
750
|
+
apiFormat: 'openai-chat-completions',
|
|
751
|
+
source: 'custom',
|
|
752
|
+
apiKeyRequired: true,
|
|
753
|
+
apiKey: '__zcode_cached_api_key_present__',
|
|
754
|
+
defaultKind: 'openai-compatible',
|
|
755
|
+
models: models.map(buildModelEntry),
|
|
756
|
+
createdAt: Date.now(),
|
|
757
|
+
updatedAt: Date.now(),
|
|
758
|
+
})
|
|
759
|
+
cacheModified = true
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
// 📖 scope === 'all' — replace all models, skip if identical
|
|
763
|
+
const cachedModelIds = existingCacheIdx >= 0
|
|
764
|
+
? (cache.providers[existingCacheIdx].models || []).map((m) => m.id)
|
|
765
|
+
: []
|
|
766
|
+
const cacheUnchanged = existingCacheIdx >= 0
|
|
767
|
+
&& cachedModelIds.length === models.length
|
|
768
|
+
&& models.every((m) => cachedModelIds.includes(m.modelId))
|
|
769
|
+
|
|
770
|
+
if (!cacheUnchanged) {
|
|
771
|
+
if (existingCacheIdx >= 0) {
|
|
772
|
+
cache.providers[existingCacheIdx].models = models.map(buildModelEntry)
|
|
773
|
+
cache.providers[existingCacheIdx].updatedAt = Date.now()
|
|
774
|
+
} else {
|
|
775
|
+
cache.providers.push({
|
|
776
|
+
id: providerId,
|
|
777
|
+
name: providerLabel,
|
|
778
|
+
enabled: true,
|
|
779
|
+
endpoints: { baseURL: baseUrl, paths: { 'openai-compatible': '/chat/completions' } },
|
|
780
|
+
apiFormat: 'openai-chat-completions',
|
|
781
|
+
source: 'custom',
|
|
782
|
+
apiKeyRequired: true,
|
|
783
|
+
apiKey: '__zcode_cached_api_key_present__',
|
|
784
|
+
defaultKind: 'openai-compatible',
|
|
785
|
+
models: models.map(buildModelEntry),
|
|
786
|
+
createdAt: Date.now(),
|
|
787
|
+
updatedAt: Date.now(),
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
cache.updatedAt = Date.now()
|
|
791
|
+
cacheModified = true
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const cacheBackupPath = cacheModified ? writeJson(cachePath, cache) : null
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
path: configPath,
|
|
799
|
+
backupPath: configBackupPath,
|
|
800
|
+
providerId,
|
|
801
|
+
modelCount: models.length,
|
|
802
|
+
extraPath: cachePath,
|
|
803
|
+
extraBackupPath: cacheBackupPath,
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
628
807
|
export function installProviderEndpoints(config, providerKey, toolMode, options = {}) {
|
|
629
808
|
const canonicalToolMode = canonicalizeToolMode(toolMode)
|
|
630
809
|
const support = getDirectInstallSupport(providerKey)
|
|
@@ -664,6 +843,8 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
664
843
|
installResult = installIntoFcmRouter(providerKey, models, apiKey)
|
|
665
844
|
} else if (canonicalToolMode === 'forgecode') {
|
|
666
845
|
installResult = installIntoForgeCode(providerKey, models, apiKey, paths)
|
|
846
|
+
} else if (canonicalToolMode === 'zcode') {
|
|
847
|
+
installResult = installIntoZCode(providerKey, models, apiKey, paths, scope)
|
|
667
848
|
} else {
|
|
668
849
|
throw new Error(`Unsupported install target: ${toolMode}`)
|
|
669
850
|
}
|
|
@@ -56,17 +56,19 @@ const BACKUP_PATH = join(homedir(), '.free-coding-models-backups.json')
|
|
|
56
56
|
* 📖 Get tool config paths
|
|
57
57
|
*/
|
|
58
58
|
function getToolConfigPaths(homeDir = homedir()) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
return {
|
|
60
|
+
goose: join(homeDir, '.config', 'goose', 'config.yaml'),
|
|
61
|
+
crush: join(homeDir, '.config', 'crush', 'crush.json'),
|
|
62
|
+
aider: join(homeDir, '.aider.conf.yml'),
|
|
63
|
+
kilo: join(homeDir, '.config', 'kilo', 'opencode.json'),
|
|
64
|
+
qwen: join(homeDir, '.qwen', 'settings.json'),
|
|
65
|
+
piModels: join(homeDir, '.pi', 'agent', 'models.json'),
|
|
66
|
+
piSettings: join(homeDir, '.pi', 'agent', 'settings.json'),
|
|
67
|
+
openHands: join(homeDir, '.fcm-openhands-env'),
|
|
68
|
+
amp: join(homeDir, '.config', 'amp', 'settings.json'),
|
|
69
|
+
zcodeConfig: join(homeDir, '.zcode', 'v2', 'config.json'),
|
|
70
|
+
zcodeCache: join(homeDir, '.zcode', 'v2', 'bots-model-cache.v2.json'),
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -463,6 +465,111 @@ function parseAmpConfig(paths = getToolConfigPaths()) {
|
|
|
463
465
|
}
|
|
464
466
|
}
|
|
465
467
|
|
|
468
|
+
/**
|
|
469
|
+
* 📖 Parse ZCode config.json + bots-model-cache.v2.json for installed models.
|
|
470
|
+
* 📖 Uses a Map keyed by "providerId::modelId" to deduplicate across both files.
|
|
471
|
+
*/
|
|
472
|
+
function parseZCodeConfig(paths = getToolConfigPaths()) {
|
|
473
|
+
const configPath = paths.zcodeConfig
|
|
474
|
+
const cachePath = paths.zcodeCache
|
|
475
|
+
|
|
476
|
+
if (!existsSync(configPath)) {
|
|
477
|
+
return { isValid: false, models: [], configPath }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
/** @type {Map<string, object>} */
|
|
482
|
+
const modelMap = new Map()
|
|
483
|
+
|
|
484
|
+
const configContent = readFileSync(configPath, 'utf8')
|
|
485
|
+
const config = JSON.parse(configContent)
|
|
486
|
+
|
|
487
|
+
// ── 1. Source of truth: config.json provider.models ─────────────────────
|
|
488
|
+
if (config.provider && typeof config.provider === 'object') {
|
|
489
|
+
for (const [providerId, provider] of Object.entries(config.provider)) {
|
|
490
|
+
if (!provider.models || typeof provider.models !== 'object') continue
|
|
491
|
+
|
|
492
|
+
const isManaged = providerId.startsWith('fcm-')
|
|
493
|
+
const sourceKey = isManaged ? providerId.replace('fcm-', '') : providerId
|
|
494
|
+
|
|
495
|
+
for (const [modelId, modelConfig] of Object.entries(provider.models)) {
|
|
496
|
+
const ctx = modelConfig?.limit?.context || 0
|
|
497
|
+
const key = `${providerId}::${modelId}`
|
|
498
|
+
modelMap.set(key, {
|
|
499
|
+
modelId,
|
|
500
|
+
label: modelId,
|
|
501
|
+
tier: '-',
|
|
502
|
+
sweScore: '-',
|
|
503
|
+
providerKey: sourceKey,
|
|
504
|
+
isExternal: !isManaged,
|
|
505
|
+
canLaunch: true,
|
|
506
|
+
contextWindow: ctx,
|
|
507
|
+
enabled: provider.enabled !== false,
|
|
508
|
+
zcodeProviderId: providerId,
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── 2. Cache enrichment: use cache names only (no duplicates) ────────────
|
|
515
|
+
if (existsSync(cachePath)) {
|
|
516
|
+
try {
|
|
517
|
+
const cacheContent = readFileSync(cachePath, 'utf8')
|
|
518
|
+
const cache = JSON.parse(cacheContent)
|
|
519
|
+
|
|
520
|
+
if (Array.isArray(cache.providers)) {
|
|
521
|
+
for (const cachedProvider of cache.providers) {
|
|
522
|
+
if (!cachedProvider?.models) continue
|
|
523
|
+
const providerId = cachedProvider.id || 'unknown'
|
|
524
|
+
|
|
525
|
+
for (const cachedModel of cachedProvider.models) {
|
|
526
|
+
const key = `${providerId}::${cachedModel.id}`
|
|
527
|
+
const existing = modelMap.get(key)
|
|
528
|
+
|
|
529
|
+
if (existing) {
|
|
530
|
+
// 📖 Upgrade label from cache (prettier name) + fill context if missing
|
|
531
|
+
if (cachedModel.name) existing.label = cachedModel.name
|
|
532
|
+
if (cachedModel.contextWindow && !existing.contextWindow) {
|
|
533
|
+
existing.contextWindow = cachedModel.contextWindow
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
// 📖 Model only in cache (edge case — provider.models missing it)
|
|
537
|
+
const isManaged = providerId.startsWith('fcm-')
|
|
538
|
+
const sourceKey = isManaged ? providerId.replace('fcm-', '') : providerId
|
|
539
|
+
modelMap.set(key, {
|
|
540
|
+
modelId: cachedModel.id,
|
|
541
|
+
label: cachedModel.name || cachedModel.id,
|
|
542
|
+
tier: '-',
|
|
543
|
+
sweScore: '-',
|
|
544
|
+
providerKey: sourceKey,
|
|
545
|
+
isExternal: !isManaged,
|
|
546
|
+
canLaunch: true,
|
|
547
|
+
contextWindow: cachedModel.contextWindow || 0,
|
|
548
|
+
enabled: cachedProvider.enabled !== false,
|
|
549
|
+
zcodeProviderId: providerId,
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
// 📖 Cache file is optional — silently ignore errors
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const models = Array.from(modelMap.values())
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
isValid: models.length > 0,
|
|
564
|
+
hasManagedMarker: Object.keys(config.provider || {}).some((id) => id.startsWith('fcm-')),
|
|
565
|
+
models,
|
|
566
|
+
configPath,
|
|
567
|
+
}
|
|
568
|
+
} catch (err) {
|
|
569
|
+
return { isValid: false, models: [], configPath }
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
466
573
|
/**
|
|
467
574
|
* 📖 Enhance model with metadata from sources.js
|
|
468
575
|
*/
|
|
@@ -507,9 +614,11 @@ export function parseToolConfig(toolMode, paths = getToolConfigPaths()) {
|
|
|
507
614
|
return parsePiConfig(paths)
|
|
508
615
|
case 'openhands':
|
|
509
616
|
return parseOpenHandsConfig(paths)
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
617
|
+
case 'zcode':
|
|
618
|
+
return parseZCodeConfig(paths)
|
|
619
|
+
case 'amp':
|
|
620
|
+
return parseAmpConfig(paths)
|
|
621
|
+
default:
|
|
513
622
|
return { isValid: false, models: [], configPath: '' }
|
|
514
623
|
}
|
|
515
624
|
}
|
|
@@ -518,7 +627,7 @@ export function parseToolConfig(toolMode, paths = getToolConfigPaths()) {
|
|
|
518
627
|
* 📖 Scan all tool configs and return structured results
|
|
519
628
|
*/
|
|
520
629
|
export function scanAllToolConfigs(paths = getToolConfigPaths()) {
|
|
521
|
-
|
|
630
|
+
const toolModes = ['goose', 'crush', 'aider', 'kilo', 'qwen', 'pi', 'openhands', 'amp', 'zcode']
|
|
522
631
|
|
|
523
632
|
return toolModes.map((toolMode) => {
|
|
524
633
|
const result = parseToolConfig(toolMode, paths)
|
|
@@ -537,17 +646,18 @@ export function scanAllToolConfigs(paths = getToolConfigPaths()) {
|
|
|
537
646
|
* 📖 Get tool emoji
|
|
538
647
|
*/
|
|
539
648
|
function getToolEmoji(toolMode) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
649
|
+
const emojis = {
|
|
650
|
+
goose: '🪿',
|
|
651
|
+
crush: '💘',
|
|
652
|
+
aider: '🛠',
|
|
653
|
+
kilo: '⚡️',
|
|
654
|
+
qwen: '🐉',
|
|
655
|
+
pi: 'π',
|
|
656
|
+
openhands: '🤲',
|
|
657
|
+
amp: '⚡',
|
|
658
|
+
zcode: '🧊',
|
|
659
|
+
}
|
|
660
|
+
return emojis[toolMode] || '🧰'
|
|
551
661
|
}
|
|
552
662
|
|
|
553
663
|
/**
|
|
@@ -580,7 +690,10 @@ function saveBackups(backups) {
|
|
|
580
690
|
* 📖 Soft-delete a model from tool config with backup
|
|
581
691
|
*/
|
|
582
692
|
export function softDeleteModel(toolMode, modelId, paths = getToolConfigPaths()) {
|
|
583
|
-
|
|
693
|
+
const pathKey = toolMode === 'pi' ? 'piSettings'
|
|
694
|
+
: toolMode === 'zcode' ? 'zcodeConfig'
|
|
695
|
+
: toolMode
|
|
696
|
+
const configPath = paths[pathKey]
|
|
584
697
|
if (!existsSync(configPath)) {
|
|
585
698
|
return { success: false, error: 'Config file not found' }
|
|
586
699
|
}
|
|
@@ -653,15 +766,58 @@ export function softDeleteModel(toolMode, modelId, paths = getToolConfigPaths())
|
|
|
653
766
|
}
|
|
654
767
|
break
|
|
655
768
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
769
|
+
case 'zcode': {
|
|
770
|
+
const zconfig = JSON.parse(originalContent)
|
|
771
|
+
// 📖 Find the provider entry that contains this modelId
|
|
772
|
+
let foundProviderId = null
|
|
773
|
+
if (zconfig.provider && typeof zconfig.provider === 'object') {
|
|
774
|
+
for (const [provId, prov] of Object.entries(zconfig.provider)) {
|
|
775
|
+
if (prov.models && typeof prov.models === 'object' && modelId in prov.models) {
|
|
776
|
+
foundProviderId = provId
|
|
777
|
+
break
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (foundProviderId) {
|
|
782
|
+
delete zconfig.provider[foundProviderId].models[modelId]
|
|
783
|
+
// 📖 If no models left, keep the empty provider (don't remove it — user may want to re-add)
|
|
784
|
+
newContent = JSON.stringify(zconfig, null, 2)
|
|
785
|
+
modified = true
|
|
786
|
+
|
|
787
|
+
// 📖 Also remove from cache file if it exists
|
|
788
|
+
const cachePath = paths.zcodeCache
|
|
789
|
+
if (existsSync(cachePath)) {
|
|
790
|
+
try {
|
|
791
|
+
const cacheContent = readFileSync(cachePath, 'utf8')
|
|
792
|
+
const cache = JSON.parse(cacheContent)
|
|
793
|
+
if (Array.isArray(cache.providers)) {
|
|
794
|
+
const cacheProv = cache.providers.find((p) => p.id === foundProviderId)
|
|
795
|
+
if (cacheProv && Array.isArray(cacheProv.models)) {
|
|
796
|
+
const before = cacheProv.models.length
|
|
797
|
+
cacheProv.models = cacheProv.models.filter((m) => m.id !== modelId)
|
|
798
|
+
if (cacheProv.models.length !== before) {
|
|
799
|
+
cacheProv.updatedAt = Date.now()
|
|
800
|
+
writeFileSync(cachePath, JSON.stringify(cache, null, 2))
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} catch {
|
|
805
|
+
// 📖 Cache file is optional
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
break
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
case 'amp':
|
|
813
|
+
const ampConfig = JSON.parse(originalContent)
|
|
814
|
+
if (ampConfig['amp.model'] === modelId) {
|
|
815
|
+
delete ampConfig['amp.model']
|
|
816
|
+
newContent = JSON.stringify(ampConfig, null, 2)
|
|
817
|
+
modified = true
|
|
818
|
+
}
|
|
819
|
+
break
|
|
820
|
+
}
|
|
665
821
|
|
|
666
822
|
if (!modified) {
|
|
667
823
|
return { success: false, error: 'Model not found in config' }
|