free-coding-models 0.3.9 → 0.3.12
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.md +40 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +34 -188
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +17 -351
- package/src/endpoint-installer.js +26 -64
- package/src/favorites.js +0 -14
- package/src/key-handler.js +74 -641
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +26 -550
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +14 -33
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +8 -77
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -154
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-server.js +0 -1477
- package/src/proxy-sync.js +0 -565
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/tool-launchers.js
CHANGED
|
@@ -9,34 +9,23 @@
|
|
|
9
9
|
* 📖 The design is pragmatic:
|
|
10
10
|
* - Write a small managed config file when the tool's config shape is stable enough
|
|
11
11
|
* - Always export the runtime environment variables before spawning the tool
|
|
12
|
+
* - Persist the selected model into the tool config before launch so Enter
|
|
13
|
+
* really means "open this tool on this model right now"
|
|
12
14
|
* - Keep each launcher isolated so a partial integration does not break others
|
|
13
15
|
*
|
|
14
|
-
* 📖 Some tools still have weaker official support for arbitrary custom providers.
|
|
15
|
-
* For those, we prefer a transparent warning over pretending the integration is
|
|
16
|
-
* fully official. The user still gets a reproducible env/config handoff.
|
|
17
|
-
*
|
|
18
16
|
* 📖 Goose: writes custom provider JSON + secrets.yaml + updates config.yaml (GOOSE_PROVIDER/GOOSE_MODEL)
|
|
19
17
|
* 📖 Crush: writes crush.json with provider config + models.large/small defaults
|
|
20
18
|
* 📖 Pi: uses --provider/--model CLI flags for guaranteed auto-selection
|
|
21
19
|
* 📖 Aider: writes ~/.aider.conf.yml + passes --model flag
|
|
22
|
-
* 📖 Claude Code: mirrors Claude proxy by keeping fake Claude model ids on the client,
|
|
23
|
-
* forcing a valid Claude alias at launch, and moving MODEL / MODEL_OPUS / MODEL_SONNET /
|
|
24
|
-
* MODEL_HAIKU routing into the proxy
|
|
25
|
-
* 📖 Codex CLI: uses a custom model_provider override so Codex stays in explicit API-provider mode
|
|
26
|
-
* 📖 Gemini CLI: proxy mode is capability-gated because older builds do not support custom base URL routing cleanly
|
|
27
20
|
*
|
|
28
21
|
* @functions
|
|
29
|
-
* → `resolveLauncherModelId` — choose the provider-specific id
|
|
30
|
-
* → `waitForClaudeProxyRouting` — wait until the daemon/proxy has reloaded the Claude proxy style Claude-family mapping
|
|
31
|
-
* → `buildClaudeProxyArgs` — force a valid Claude alias so stale local non-Claude selections cannot break launch
|
|
32
|
-
* → `buildCodexProxyArgs` — force Codex into a proxy-backed custom provider config
|
|
33
|
-
* → `inspectGeminiCliSupport` — detect whether the installed Gemini CLI can use proxy mode safely
|
|
22
|
+
* → `resolveLauncherModelId` — choose the provider-specific id for a launch
|
|
34
23
|
* → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
|
|
35
24
|
* → `writeCrushConfig` — write provider + models.large/small to crush.json
|
|
25
|
+
* → `prepareExternalToolLaunch` — persist selected-model defaults and compute the launch command
|
|
36
26
|
* → `startExternalTool` — configure and launch the selected external tool mode
|
|
37
27
|
*
|
|
38
|
-
* @exports resolveLauncherModelId,
|
|
39
|
-
* @exports inspectGeminiCliSupport, startExternalTool
|
|
28
|
+
* @exports resolveLauncherModelId, buildToolEnv, prepareExternalToolLaunch, startExternalTool
|
|
40
29
|
*
|
|
41
30
|
* @see src/tool-metadata.js
|
|
42
31
|
* @see src/provider-metadata.js
|
|
@@ -44,16 +33,15 @@
|
|
|
44
33
|
*/
|
|
45
34
|
|
|
46
35
|
import chalk from 'chalk'
|
|
47
|
-
import {
|
|
36
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'
|
|
48
37
|
import { homedir } from 'os'
|
|
49
|
-
import {
|
|
50
|
-
import { spawn
|
|
38
|
+
import { dirname, join } from 'path'
|
|
39
|
+
import { spawn } from 'child_process'
|
|
51
40
|
import { sources } from '../sources.js'
|
|
52
41
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
53
|
-
import { getApiKey
|
|
42
|
+
import { getApiKey } from './config.js'
|
|
54
43
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
55
44
|
import { getToolMeta } from './tool-metadata.js'
|
|
56
|
-
import { ensureProxyRunning, resolveProxyModelId } from './opencode.js'
|
|
57
45
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
58
46
|
|
|
59
47
|
const OPENAI_COMPAT_ENV_KEYS = [
|
|
@@ -65,35 +53,28 @@ const OPENAI_COMPAT_ENV_KEYS = [
|
|
|
65
53
|
'LLM_BASE_URL',
|
|
66
54
|
'LLM_MODEL',
|
|
67
55
|
]
|
|
68
|
-
const
|
|
69
|
-
'ANTHROPIC_API_KEY',
|
|
70
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
71
|
-
'ANTHROPIC_BASE_URL',
|
|
72
|
-
'ANTHROPIC_MODEL',
|
|
73
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
74
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
75
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
76
|
-
'ANTHROPIC_SMALL_FAST_MODEL',
|
|
77
|
-
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
78
|
-
]
|
|
79
|
-
const GEMINI_ENV_KEYS = [
|
|
80
|
-
'GEMINI_API_KEY',
|
|
81
|
-
'GOOGLE_API_KEY',
|
|
82
|
-
'GOOGLE_GEMINI_BASE_URL',
|
|
83
|
-
'GOOGLE_VERTEX_BASE_URL',
|
|
84
|
-
]
|
|
85
|
-
const PROXY_SANITIZED_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS, ...ANTHROPIC_ENV_KEYS, ...GEMINI_ENV_KEYS]
|
|
86
|
-
const GEMINI_PROXY_MIN_VERSION = '0.34.0'
|
|
87
|
-
const EXPERIMENTAL_PROXY_TOOLS_NOTE = 'FCM Proxy V2 support for external tools is still in beta, so some launch and authentication flows can remain flaky while the integration stabilizes.'
|
|
88
|
-
const CLAUDE_PROXY_RELOAD_TIMEOUT_MS = 4000
|
|
89
|
-
const CLAUDE_PROXY_RELOAD_INTERVAL_MS = 200
|
|
90
|
-
const CLAUDE_PROXY_CLIENT_MODEL = 'sonnet'
|
|
56
|
+
const SANITIZED_TOOL_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS]
|
|
91
57
|
|
|
92
58
|
function ensureDir(filePath) {
|
|
93
59
|
const dir = dirname(filePath)
|
|
94
60
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
95
61
|
}
|
|
96
62
|
|
|
63
|
+
function getDefaultToolPaths(homeDir = homedir()) {
|
|
64
|
+
return {
|
|
65
|
+
aiderConfigPath: join(homeDir, '.aider.conf.yml'),
|
|
66
|
+
crushConfigPath: join(homeDir, '.config', 'crush', 'crush.json'),
|
|
67
|
+
gooseProvidersDir: join(homeDir, '.config', 'goose', 'custom_providers'),
|
|
68
|
+
gooseSecretsPath: join(homeDir, '.config', 'goose', 'secrets.yaml'),
|
|
69
|
+
gooseConfigPath: join(homeDir, '.config', 'goose', 'config.yaml'),
|
|
70
|
+
qwenConfigPath: join(homeDir, '.qwen', 'settings.json'),
|
|
71
|
+
ampConfigPath: join(homeDir, '.config', 'amp', 'settings.json'),
|
|
72
|
+
piModelsPath: join(homeDir, '.pi', 'agent', 'models.json'),
|
|
73
|
+
piSettingsPath: join(homeDir, '.pi', 'agent', 'settings.json'),
|
|
74
|
+
openHandsEnvPath: join(homeDir, '.fcm-openhands-env'),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
97
78
|
function backupIfExists(filePath) {
|
|
98
79
|
if (!existsSync(filePath)) return null
|
|
99
80
|
const backupPath = `${filePath}.backup-${Date.now()}`
|
|
@@ -147,29 +128,16 @@ function applyOpenAiCompatEnv(env, apiKey, baseUrl, modelId) {
|
|
|
147
128
|
}
|
|
148
129
|
|
|
149
130
|
/**
|
|
150
|
-
* 📖 resolveLauncherModelId
|
|
151
|
-
* 📖
|
|
131
|
+
* 📖 resolveLauncherModelId returns the provider-native id used by the direct
|
|
132
|
+
* 📖 launchers. Legacy bridge-specific model remapping has been removed.
|
|
152
133
|
*
|
|
153
134
|
* @param {{ label?: string, modelId?: string }} model
|
|
154
|
-
* @param {boolean} useProxy
|
|
155
135
|
* @returns {string}
|
|
156
136
|
*/
|
|
157
|
-
export function resolveLauncherModelId(model
|
|
158
|
-
if (useProxy) return resolveProxyModelId(model)
|
|
137
|
+
export function resolveLauncherModelId(model) {
|
|
159
138
|
return model?.modelId ?? ''
|
|
160
139
|
}
|
|
161
140
|
|
|
162
|
-
/**
|
|
163
|
-
* 📖 Force Claude Code to start on a real Claude alias, never on an FCM slug.
|
|
164
|
-
* 📖 Older FCM launches poisoned Claude's local model state with `gpt-oss-*`,
|
|
165
|
-
* 📖 and Claude rejects those client-side before any proxy request is made.
|
|
166
|
-
*
|
|
167
|
-
* @returns {string[]}
|
|
168
|
-
*/
|
|
169
|
-
export function buildClaudeProxyArgs() {
|
|
170
|
-
return ['--model', CLAUDE_PROXY_CLIENT_MODEL]
|
|
171
|
-
}
|
|
172
|
-
|
|
173
141
|
export function buildToolEnv(mode, model, config, options = {}) {
|
|
174
142
|
const {
|
|
175
143
|
sanitize = false,
|
|
@@ -181,7 +149,7 @@ export function buildToolEnv(mode, model, config, options = {}) {
|
|
|
181
149
|
const providerUrl = sources[providerKey]?.url || ''
|
|
182
150
|
const baseUrl = getProviderBaseUrl(providerKey)
|
|
183
151
|
const apiKey = sanitize ? (config?.apiKeys?.[providerKey] ?? null) : getApiKey(config, providerKey)
|
|
184
|
-
const env = cloneInheritedEnv(inheritedEnv, sanitize ?
|
|
152
|
+
const env = cloneInheritedEnv(inheritedEnv, sanitize ? SANITIZED_TOOL_ENV_KEYS : [])
|
|
185
153
|
const providerEnvName = ENV_VAR_NAMES[providerKey]
|
|
186
154
|
if (includeProviderEnv && providerEnvName && apiKey) env[providerEnvName] = apiKey
|
|
187
155
|
|
|
@@ -196,154 +164,9 @@ export function buildToolEnv(mode, model, config, options = {}) {
|
|
|
196
164
|
env.LLM_MODEL = `openai/${model.modelId}`
|
|
197
165
|
}
|
|
198
166
|
|
|
199
|
-
// 📖 Provider-specific envs for tools that expect a different wire format.
|
|
200
|
-
if (mode === 'claude-code' && apiKey && baseUrl) {
|
|
201
|
-
env.ANTHROPIC_AUTH_TOKEN = apiKey
|
|
202
|
-
env.ANTHROPIC_BASE_URL = baseUrl
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (mode === 'gemini' && apiKey && baseUrl) {
|
|
206
|
-
env.GEMINI_API_KEY = apiKey
|
|
207
|
-
env.GOOGLE_API_KEY = apiKey
|
|
208
|
-
env.GOOGLE_GEMINI_BASE_URL = baseUrl
|
|
209
|
-
}
|
|
210
|
-
|
|
211
167
|
return { env, apiKey, baseUrl, providerUrl }
|
|
212
168
|
}
|
|
213
169
|
|
|
214
|
-
export async function waitForClaudeProxyRouting(port, token, expectedModelId) {
|
|
215
|
-
const expected = typeof expectedModelId === 'string' ? expectedModelId.trim().replace(/^fcm-proxy\//, '') : ''
|
|
216
|
-
if (!expected || !port || !token) return false
|
|
217
|
-
|
|
218
|
-
const deadline = Date.now() + CLAUDE_PROXY_RELOAD_TIMEOUT_MS
|
|
219
|
-
while (Date.now() < deadline) {
|
|
220
|
-
try {
|
|
221
|
-
const res = await fetch(`http://127.0.0.1:${port}/v1/stats`, {
|
|
222
|
-
headers: { authorization: `Bearer ${token}` },
|
|
223
|
-
})
|
|
224
|
-
if (res.ok) {
|
|
225
|
-
const payload = await res.json()
|
|
226
|
-
const active = payload?.anthropicRouting?.model
|
|
227
|
-
if (typeof active === 'string' && active.replace(/^fcm-proxy\//, '') === expected) {
|
|
228
|
-
return true
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} catch { /* daemon may still be reloading — keep polling */ }
|
|
232
|
-
|
|
233
|
-
await new Promise(resolve => setTimeout(resolve, CLAUDE_PROXY_RELOAD_INTERVAL_MS))
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return false
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function buildCodexProxyArgs(baseUrl) {
|
|
240
|
-
return [
|
|
241
|
-
'-c', 'model_provider="fcm_proxy"',
|
|
242
|
-
'-c', 'model_providers.fcm_proxy.name="FCM Proxy V2"',
|
|
243
|
-
'-c', `model_providers.fcm_proxy.base_url=${JSON.stringify(baseUrl)}`,
|
|
244
|
-
'-c', 'model_providers.fcm_proxy.env_key="FCM_PROXY_API_KEY"',
|
|
245
|
-
'-c', 'model_providers.fcm_proxy.wire_api="responses"',
|
|
246
|
-
]
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function compareSemver(a, b) {
|
|
250
|
-
const left = String(a || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
251
|
-
const right = String(b || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
252
|
-
const length = Math.max(left.length, right.length)
|
|
253
|
-
for (let idx = 0; idx < length; idx++) {
|
|
254
|
-
const lhs = left[idx] || 0
|
|
255
|
-
const rhs = right[idx] || 0
|
|
256
|
-
if (lhs > rhs) return 1
|
|
257
|
-
if (lhs < rhs) return -1
|
|
258
|
-
}
|
|
259
|
-
return 0
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function findExecutableOnPath(command) {
|
|
263
|
-
const pathValue = process.env.PATH || ''
|
|
264
|
-
const candidates = process.platform === 'win32'
|
|
265
|
-
? [command, `${command}.cmd`, `${command}.exe`]
|
|
266
|
-
: [command]
|
|
267
|
-
|
|
268
|
-
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
269
|
-
for (const candidate of candidates) {
|
|
270
|
-
const fullPath = join(dir, candidate)
|
|
271
|
-
try {
|
|
272
|
-
accessSync(fullPath, constants.X_OK)
|
|
273
|
-
return fullPath
|
|
274
|
-
} catch { /* not executable */ }
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return null
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function findPackageJsonUpwards(startPath) {
|
|
281
|
-
let current = dirname(startPath)
|
|
282
|
-
while (current && current !== dirname(current)) {
|
|
283
|
-
const packageJsonPath = join(current, 'package.json')
|
|
284
|
-
if (existsSync(packageJsonPath)) return packageJsonPath
|
|
285
|
-
current = dirname(current)
|
|
286
|
-
}
|
|
287
|
-
return null
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function detectGeminiCliVersion(binaryPath) {
|
|
291
|
-
if (!binaryPath) return null
|
|
292
|
-
try {
|
|
293
|
-
const realPath = realpathSync(binaryPath)
|
|
294
|
-
const versionMatch = realPath.match(/gemini-cli[\\/](\d+\.\d+\.\d+)(?:[\\/]|$)/)
|
|
295
|
-
if (versionMatch?.[1]) return versionMatch[1]
|
|
296
|
-
|
|
297
|
-
const packageJsonPath = findPackageJsonUpwards(realPath)
|
|
298
|
-
if (!packageJsonPath) return null
|
|
299
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
300
|
-
if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
|
|
301
|
-
return pkg.version
|
|
302
|
-
}
|
|
303
|
-
} catch { /* best effort */ }
|
|
304
|
-
return null
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function extractGeminiConfigError(output) {
|
|
308
|
-
const text = String(output || '').trim()
|
|
309
|
-
if (!text.includes('Invalid configuration in ')) return null
|
|
310
|
-
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
311
|
-
return lines.slice(0, 8).join('\n')
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export function inspectGeminiCliSupport(options = {}) {
|
|
315
|
-
const binaryPath = options.binaryPath || findExecutableOnPath(options.command || 'gemini')
|
|
316
|
-
if (!binaryPath) {
|
|
317
|
-
return {
|
|
318
|
-
installed: false,
|
|
319
|
-
version: null,
|
|
320
|
-
supportsProxyBaseUrl: false,
|
|
321
|
-
configError: null,
|
|
322
|
-
reason: 'Gemini CLI is not installed in PATH.',
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const version = options.version || detectGeminiCliVersion(binaryPath)
|
|
327
|
-
const helpResult = options.helpResult || spawnSync(binaryPath, ['--help'], {
|
|
328
|
-
encoding: 'utf8',
|
|
329
|
-
timeout: 5000,
|
|
330
|
-
env: options.inheritedEnv || process.env,
|
|
331
|
-
})
|
|
332
|
-
const helpOutput = `${helpResult.stdout || ''}\n${helpResult.stderr || ''}`.trim()
|
|
333
|
-
const configError = extractGeminiConfigError(helpOutput)
|
|
334
|
-
const supportsProxyBaseUrl = version ? compareSemver(version, GEMINI_PROXY_MIN_VERSION) >= 0 : false
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
installed: true,
|
|
338
|
-
version,
|
|
339
|
-
supportsProxyBaseUrl,
|
|
340
|
-
configError,
|
|
341
|
-
reason: supportsProxyBaseUrl
|
|
342
|
-
? null
|
|
343
|
-
: `Gemini CLI ${version || '(unknown version)'} does not expose stable custom base URL support for proxy mode yet.`,
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
170
|
function spawnCommand(command, args, env) {
|
|
348
171
|
return new Promise((resolve, reject) => {
|
|
349
172
|
const child = spawn(command, args, {
|
|
@@ -365,8 +188,8 @@ function spawnCommand(command, args, env) {
|
|
|
365
188
|
})
|
|
366
189
|
}
|
|
367
190
|
|
|
368
|
-
function writeAiderConfig(model, apiKey, baseUrl) {
|
|
369
|
-
const filePath =
|
|
191
|
+
function writeAiderConfig(model, apiKey, baseUrl, paths = getDefaultToolPaths()) {
|
|
192
|
+
const filePath = paths.aiderConfigPath
|
|
370
193
|
const backupPath = backupIfExists(filePath)
|
|
371
194
|
const content = [
|
|
372
195
|
'# 📖 Managed by free-coding-models',
|
|
@@ -380,8 +203,8 @@ function writeAiderConfig(model, apiKey, baseUrl) {
|
|
|
380
203
|
return { filePath, backupPath }
|
|
381
204
|
}
|
|
382
205
|
|
|
383
|
-
function writeCrushConfig(model, apiKey, baseUrl, providerId) {
|
|
384
|
-
const filePath =
|
|
206
|
+
function writeCrushConfig(model, apiKey, baseUrl, providerId, paths = getDefaultToolPaths()) {
|
|
207
|
+
const filePath = paths.crushConfigPath
|
|
385
208
|
const backupPath = backupIfExists(filePath)
|
|
386
209
|
const config = readJson(filePath, { $schema: 'https://charm.land/crush.json' })
|
|
387
210
|
// 📖 Remove legacy disable_default_providers — it can prevent Crush from auto-selecting models
|
|
@@ -412,17 +235,8 @@ function writeCrushConfig(model, apiKey, baseUrl, providerId) {
|
|
|
412
235
|
return { filePath, backupPath }
|
|
413
236
|
}
|
|
414
237
|
|
|
415
|
-
function
|
|
416
|
-
const filePath =
|
|
417
|
-
const backupPath = backupIfExists(filePath)
|
|
418
|
-
const config = readJson(filePath, {})
|
|
419
|
-
config.model = model.modelId
|
|
420
|
-
writeJson(filePath, config)
|
|
421
|
-
return { filePath, backupPath }
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function writeQwenConfig(model, providerKey, apiKey, baseUrl) {
|
|
425
|
-
const filePath = join(homedir(), '.qwen', 'settings.json')
|
|
238
|
+
function writeQwenConfig(model, providerKey, apiKey, baseUrl, paths = getDefaultToolPaths()) {
|
|
239
|
+
const filePath = paths.qwenConfigPath
|
|
426
240
|
const backupPath = backupIfExists(filePath)
|
|
427
241
|
const config = readJson(filePath, {})
|
|
428
242
|
if (!config.modelProviders || typeof config.modelProviders !== 'object') config.modelProviders = {}
|
|
@@ -441,9 +255,9 @@ function writeQwenConfig(model, providerKey, apiKey, baseUrl) {
|
|
|
441
255
|
return { filePath, backupPath, envKey: nextEntry.envKey, apiKey }
|
|
442
256
|
}
|
|
443
257
|
|
|
444
|
-
function writePiConfig(model, apiKey, baseUrl) {
|
|
258
|
+
function writePiConfig(model, apiKey, baseUrl, paths = getDefaultToolPaths()) {
|
|
445
259
|
// 📖 Write models.json with the selected provider config
|
|
446
|
-
const modelsFilePath =
|
|
260
|
+
const modelsFilePath = paths.piModelsPath
|
|
447
261
|
const modelsBackupPath = backupIfExists(modelsFilePath)
|
|
448
262
|
const modelsConfig = readJson(modelsFilePath, { providers: {} })
|
|
449
263
|
if (!modelsConfig.providers || typeof modelsConfig.providers !== 'object') modelsConfig.providers = {}
|
|
@@ -456,7 +270,7 @@ function writePiConfig(model, apiKey, baseUrl) {
|
|
|
456
270
|
writeJson(modelsFilePath, modelsConfig)
|
|
457
271
|
|
|
458
272
|
// 📖 Write settings.json to set the model as default on next launch
|
|
459
|
-
const settingsFilePath =
|
|
273
|
+
const settingsFilePath = paths.piSettingsPath
|
|
460
274
|
const settingsBackupPath = backupIfExists(settingsFilePath)
|
|
461
275
|
const settingsConfig = readJson(settingsFilePath, {})
|
|
462
276
|
settingsConfig.defaultProvider = 'freeCodingModels'
|
|
@@ -469,15 +283,13 @@ function writePiConfig(model, apiKey, baseUrl) {
|
|
|
469
283
|
// 📖 writeGooseConfig: Install/update the provider in Goose's custom_providers/, set the
|
|
470
284
|
// 📖 API key in secrets.yaml, and update config.yaml with GOOSE_PROVIDER + GOOSE_MODEL
|
|
471
285
|
// 📖 so Goose auto-selects the model on launch.
|
|
472
|
-
function writeGooseConfig(model, apiKey, baseUrl, providerKey) {
|
|
473
|
-
const home = homedir()
|
|
286
|
+
function writeGooseConfig(model, apiKey, baseUrl, providerKey, paths = getDefaultToolPaths()) {
|
|
474
287
|
const providerId = `fcm-${providerKey}`
|
|
475
288
|
const providerLabel = PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
|
|
476
289
|
const secretEnvName = `FCM_${providerKey.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`
|
|
477
290
|
|
|
478
291
|
// 📖 Step 1: Write custom provider JSON (same format as endpoint-installer)
|
|
479
|
-
const
|
|
480
|
-
const providerFilePath = join(providerDir, `${providerId}.json`)
|
|
292
|
+
const providerFilePath = join(paths.gooseProvidersDir, `${providerId}.json`)
|
|
481
293
|
ensureDir(providerFilePath)
|
|
482
294
|
const providerConfig = {
|
|
483
295
|
name: providerId,
|
|
@@ -493,7 +305,7 @@ function writeGooseConfig(model, apiKey, baseUrl, providerKey) {
|
|
|
493
305
|
writeFileSync(providerFilePath, JSON.stringify(providerConfig, null, 2) + '\n')
|
|
494
306
|
|
|
495
307
|
// 📖 Step 2: Write API key to secrets.yaml (simple key: value format)
|
|
496
|
-
const secretsPath =
|
|
308
|
+
const secretsPath = paths.gooseSecretsPath
|
|
497
309
|
let secretsContent = ''
|
|
498
310
|
if (existsSync(secretsPath)) {
|
|
499
311
|
secretsContent = readFileSync(secretsPath, 'utf8')
|
|
@@ -510,7 +322,8 @@ function writeGooseConfig(model, apiKey, baseUrl, providerKey) {
|
|
|
510
322
|
writeFileSync(secretsPath, secretsContent)
|
|
511
323
|
|
|
512
324
|
// 📖 Step 3: Update config.yaml — set GOOSE_PROVIDER and GOOSE_MODEL at top level
|
|
513
|
-
const configPath =
|
|
325
|
+
const configPath = paths.gooseConfigPath
|
|
326
|
+
const configBackupPath = backupIfExists(configPath)
|
|
514
327
|
let configContent = ''
|
|
515
328
|
if (existsSync(configPath)) {
|
|
516
329
|
configContent = readFileSync(configPath, 'utf8')
|
|
@@ -530,11 +343,11 @@ function writeGooseConfig(model, apiKey, baseUrl, providerKey) {
|
|
|
530
343
|
}
|
|
531
344
|
writeFileSync(configPath, configContent)
|
|
532
345
|
|
|
533
|
-
return { providerFilePath, secretsPath, configPath }
|
|
346
|
+
return { providerFilePath, secretsPath, configPath, configBackupPath }
|
|
534
347
|
}
|
|
535
348
|
|
|
536
|
-
function writeAmpConfig(model, baseUrl) {
|
|
537
|
-
const filePath =
|
|
349
|
+
function writeAmpConfig(model, baseUrl, paths = getDefaultToolPaths()) {
|
|
350
|
+
const filePath = paths.ampConfigPath
|
|
538
351
|
const backupPath = backupIfExists(filePath)
|
|
539
352
|
const config = readJson(filePath, {})
|
|
540
353
|
config['amp.url'] = baseUrl
|
|
@@ -543,228 +356,241 @@ function writeAmpConfig(model, baseUrl) {
|
|
|
543
356
|
return { filePath, backupPath }
|
|
544
357
|
}
|
|
545
358
|
|
|
546
|
-
function
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
359
|
+
function writeOpenHandsEnv(model, apiKey, baseUrl, paths = getDefaultToolPaths()) {
|
|
360
|
+
const filePath = paths.openHandsEnvPath
|
|
361
|
+
const backupPath = backupIfExists(filePath)
|
|
362
|
+
const lines = [
|
|
363
|
+
'# 📖 Managed by free-coding-models',
|
|
364
|
+
`export OPENAI_API_KEY="${apiKey}"`,
|
|
365
|
+
`export OPENAI_BASE_URL="${baseUrl}"`,
|
|
366
|
+
`export OPENAI_MODEL="${model.modelId}"`,
|
|
367
|
+
`export LLM_API_KEY="${apiKey}"`,
|
|
368
|
+
`export LLM_BASE_URL="${baseUrl}"`,
|
|
369
|
+
`export LLM_MODEL="openai/${model.modelId}"`,
|
|
370
|
+
]
|
|
371
|
+
ensureDir(filePath)
|
|
372
|
+
writeFileSync(filePath, lines.join('\n') + '\n')
|
|
373
|
+
return { filePath, backupPath }
|
|
550
374
|
}
|
|
551
375
|
|
|
552
|
-
function
|
|
553
|
-
|
|
376
|
+
function printConfigArtifacts(toolName, artifacts = []) {
|
|
377
|
+
for (const artifact of artifacts) {
|
|
378
|
+
if (!artifact?.path) continue
|
|
379
|
+
const label = artifact.label ? `${artifact.label}: ` : ''
|
|
380
|
+
console.log(chalk.dim(` 📄 ${toolName} ${label}${artifact.path}`))
|
|
381
|
+
if (artifact.backupPath) console.log(chalk.dim(` 💾 Backup: ${artifact.backupPath}`))
|
|
382
|
+
}
|
|
554
383
|
}
|
|
555
384
|
|
|
556
|
-
|
|
385
|
+
/**
|
|
386
|
+
* 📖 prepareExternalToolLaunch persists the selected model into the target tool's
|
|
387
|
+
* 📖 config before launch, then returns the exact command/env/args that should
|
|
388
|
+
* 📖 be spawned. This makes launcher behavior unit-testable without requiring
|
|
389
|
+
* 📖 the real CLIs in PATH.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} mode
|
|
392
|
+
* @param {{ providerKey: string, modelId: string, label: string }} model
|
|
393
|
+
* @param {Record<string, unknown>} config
|
|
394
|
+
* @param {{
|
|
395
|
+
* paths?: Partial<ReturnType<typeof getDefaultToolPaths>>,
|
|
396
|
+
* inheritedEnv?: NodeJS.ProcessEnv,
|
|
397
|
+
* }} [options]
|
|
398
|
+
* @returns {{
|
|
399
|
+
* blocked?: boolean,
|
|
400
|
+
* exitCode?: number,
|
|
401
|
+
* warnings?: string[],
|
|
402
|
+
* command?: string,
|
|
403
|
+
* args?: string[],
|
|
404
|
+
* env?: NodeJS.ProcessEnv,
|
|
405
|
+
* apiKey?: string | null,
|
|
406
|
+
* baseUrl?: string | null,
|
|
407
|
+
* meta: { label: string, emoji: string, flag: string | null },
|
|
408
|
+
* configArtifacts: Array<{ path: string, backupPath: string | null, label?: string }>
|
|
409
|
+
* }}
|
|
410
|
+
*/
|
|
411
|
+
export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
557
412
|
const meta = getToolMeta(mode)
|
|
558
|
-
const {
|
|
559
|
-
const
|
|
413
|
+
const paths = { ...getDefaultToolPaths(), ...(options.paths || {}) }
|
|
414
|
+
const { env, apiKey, baseUrl } = buildToolEnv(mode, model, config, {
|
|
415
|
+
inheritedEnv: options.inheritedEnv,
|
|
416
|
+
})
|
|
560
417
|
|
|
561
418
|
if (!apiKey && mode !== 'amp') {
|
|
562
|
-
// 📖 Color provider name the same way as in the main table
|
|
563
419
|
const providerRgb = PROVIDER_COLOR[model.providerKey] ?? [105, 190, 245]
|
|
564
420
|
const providerName = sources[model.providerKey]?.name || model.providerKey
|
|
565
421
|
const coloredProviderName = chalk.bold.rgb(...providerRgb)(providerName)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
422
|
+
return {
|
|
423
|
+
blocked: true,
|
|
424
|
+
exitCode: 1,
|
|
425
|
+
warnings: [
|
|
426
|
+
` ⚠ No API key configured for ${coloredProviderName}.`,
|
|
427
|
+
' Configure the provider first from the Settings screen (P) or via env vars.',
|
|
428
|
+
],
|
|
429
|
+
meta,
|
|
430
|
+
configArtifacts: [],
|
|
431
|
+
}
|
|
570
432
|
}
|
|
571
433
|
|
|
572
|
-
console.log(chalk.cyan(` ▶ Launching ${meta.label} with ${chalk.bold(model.label)}...`))
|
|
573
|
-
|
|
574
434
|
if (mode === 'aider') {
|
|
575
|
-
|
|
576
|
-
return
|
|
435
|
+
const result = writeAiderConfig(model, apiKey, baseUrl, paths)
|
|
436
|
+
return {
|
|
437
|
+
command: 'aider',
|
|
438
|
+
args: ['--model', `openai/${model.modelId}`],
|
|
439
|
+
env,
|
|
440
|
+
apiKey,
|
|
441
|
+
baseUrl,
|
|
442
|
+
meta,
|
|
443
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
444
|
+
}
|
|
577
445
|
}
|
|
578
446
|
|
|
579
447
|
if (mode === 'crush') {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
console.log(chalk.dim(` 📖 Crush will use the local FCM proxy on :${started.port} for this launch.`))
|
|
592
|
-
} else {
|
|
593
|
-
console.log(chalk.dim(' 📖 Crush will use the provider directly for this launch.'))
|
|
448
|
+
const launchModelId = resolveLauncherModelId(model)
|
|
449
|
+
applyOpenAiCompatEnv(env, apiKey, baseUrl, launchModelId)
|
|
450
|
+
const result = writeCrushConfig({ ...model, modelId: launchModelId }, apiKey, baseUrl, 'freeCodingModels', paths)
|
|
451
|
+
return {
|
|
452
|
+
command: 'crush',
|
|
453
|
+
args: [],
|
|
454
|
+
env,
|
|
455
|
+
apiKey,
|
|
456
|
+
baseUrl,
|
|
457
|
+
meta,
|
|
458
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
594
459
|
}
|
|
595
|
-
|
|
596
|
-
const launchModel = { ...model, modelId: launchModelId }
|
|
597
|
-
applyOpenAiCompatEnv(env, crushApiKey, crushBaseUrl, launchModelId)
|
|
598
|
-
printConfigResult(meta.label, writeCrushConfig(launchModel, crushApiKey, crushBaseUrl, providerId))
|
|
599
|
-
return spawnCommand('crush', [], env)
|
|
600
460
|
}
|
|
601
461
|
|
|
602
462
|
if (mode === 'goose') {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (proxySettings.enabled) {
|
|
609
|
-
const started = await ensureProxyRunning(config)
|
|
610
|
-
gooseApiKey = started.proxyToken
|
|
611
|
-
gooseBaseUrl = `http://127.0.0.1:${started.port}/v1/chat/completions`
|
|
612
|
-
gooseModelId = resolveLauncherModelId(model, true)
|
|
613
|
-
gooseProviderKey = 'proxy'
|
|
614
|
-
console.log(chalk.dim(` 📖 Goose will use the local FCM proxy on :${started.port} for this launch.`))
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// 📖 Write Goose config: custom provider JSON + secrets.yaml + config.yaml (GOOSE_PROVIDER/GOOSE_MODEL)
|
|
618
|
-
const gooseResult = writeGooseConfig({ ...model, modelId: gooseModelId }, gooseApiKey, gooseBaseUrl, gooseProviderKey)
|
|
619
|
-
console.log(chalk.dim(` 📄 Goose config updated: ${gooseResult.configPath}`))
|
|
620
|
-
console.log(chalk.dim(` 📄 Provider installed: ${gooseResult.providerFilePath}`))
|
|
621
|
-
|
|
622
|
-
// 📖 Also set env vars as belt-and-suspenders
|
|
623
|
-
env.GOOSE_PROVIDER = `fcm-${gooseProviderKey}`
|
|
463
|
+
const gooseBaseUrl = sources[model.providerKey]?.url || baseUrl || ''
|
|
464
|
+
const gooseModelId = resolveLauncherModelId(model)
|
|
465
|
+
const result = writeGooseConfig({ ...model, modelId: gooseModelId }, apiKey, gooseBaseUrl, model.providerKey, paths)
|
|
466
|
+
env.GOOSE_PROVIDER = `fcm-${model.providerKey}`
|
|
624
467
|
env.GOOSE_MODEL = gooseModelId
|
|
625
|
-
applyOpenAiCompatEnv(env,
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
console.log()
|
|
639
|
-
console.log(chalk.white(' To enable it:'))
|
|
640
|
-
console.log(chalk.dim(' 1. Press ') + chalk.bold.white('J') + chalk.dim(' to open FCM Proxy V2 settings'))
|
|
641
|
-
console.log(chalk.dim(' 2. Enable ') + chalk.bold.white('Proxy mode') + chalk.dim(' and install the ') + chalk.bold.white('background service'))
|
|
642
|
-
console.log(chalk.dim(' 3. Come back and select your model again'))
|
|
643
|
-
console.log()
|
|
644
|
-
return 1
|
|
468
|
+
applyOpenAiCompatEnv(env, apiKey, gooseBaseUrl.replace(/\/chat\/completions$/, ''), gooseModelId)
|
|
469
|
+
return {
|
|
470
|
+
command: 'goose',
|
|
471
|
+
args: [],
|
|
472
|
+
env,
|
|
473
|
+
apiKey,
|
|
474
|
+
baseUrl,
|
|
475
|
+
meta,
|
|
476
|
+
configArtifacts: [
|
|
477
|
+
{ path: result.providerFilePath, backupPath: null, label: 'provider' },
|
|
478
|
+
{ path: result.secretsPath, backupPath: null, label: 'secrets' },
|
|
479
|
+
{ path: result.configPath, backupPath: result.configBackupPath || null, label: 'config' },
|
|
480
|
+
],
|
|
645
481
|
}
|
|
646
482
|
}
|
|
647
483
|
|
|
648
|
-
if (mode === '
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
console.log(chalk.red(' ✖ Failed to persist the Claude proxy routing before launch.'))
|
|
659
|
-
console.log(chalk.dim(` ${saveResult.error || 'Unknown config write error.'}`))
|
|
660
|
-
console.log()
|
|
661
|
-
return 1
|
|
662
|
-
}
|
|
484
|
+
if (mode === 'qwen') {
|
|
485
|
+
const result = writeQwenConfig(model, model.providerKey, apiKey, baseUrl, paths)
|
|
486
|
+
return {
|
|
487
|
+
command: 'qwen',
|
|
488
|
+
args: [],
|
|
489
|
+
env,
|
|
490
|
+
apiKey,
|
|
491
|
+
baseUrl,
|
|
492
|
+
meta,
|
|
493
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
663
494
|
}
|
|
495
|
+
}
|
|
664
496
|
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
console.log(chalk.yellow(` ⚠ Claude proxy routing reload is taking longer than expected; launching anyway.`))
|
|
679
|
-
console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
|
|
497
|
+
if (mode === 'openhands') {
|
|
498
|
+
const result = writeOpenHandsEnv(model, apiKey, baseUrl, paths)
|
|
499
|
+
env.LLM_MODEL = model.modelId
|
|
500
|
+
env.LLM_API_KEY = apiKey || env.LLM_API_KEY
|
|
501
|
+
if (baseUrl) env.LLM_BASE_URL = baseUrl
|
|
502
|
+
return {
|
|
503
|
+
command: 'openhands',
|
|
504
|
+
args: ['--override-with-envs'],
|
|
505
|
+
env,
|
|
506
|
+
apiKey,
|
|
507
|
+
baseUrl,
|
|
508
|
+
meta,
|
|
509
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'env file' }],
|
|
680
510
|
}
|
|
511
|
+
}
|
|
681
512
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const started = await ensureProxyRunning(config)
|
|
693
|
-
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
694
|
-
sanitize: true,
|
|
695
|
-
includeCompatDefaults: false,
|
|
696
|
-
includeProviderEnv: false,
|
|
697
|
-
})
|
|
698
|
-
const launchModelId = resolveLauncherModelId(model, true)
|
|
699
|
-
const proxyBaseUrl = `http://127.0.0.1:${started.port}/v1`
|
|
700
|
-
proxyEnv.FCM_PROXY_API_KEY = started.proxyToken
|
|
701
|
-
console.log(chalk.dim(` 📖 Codex routed through FCM proxy on :${started.port}`))
|
|
702
|
-
return spawnCommand('codex', [...buildCodexProxyArgs(proxyBaseUrl), '--model', launchModelId], proxyEnv)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (mode === 'gemini') {
|
|
706
|
-
const geminiSupport = inspectGeminiCliSupport()
|
|
707
|
-
if (geminiSupport.configError) {
|
|
708
|
-
console.log()
|
|
709
|
-
console.log(chalk.red(' ✖ Gemini CLI configuration is invalid, so the proxy launch is blocked before auth.'))
|
|
710
|
-
console.log(chalk.dim(` ${geminiSupport.configError.split('\n').join('\n ')}`))
|
|
711
|
-
printExperimentalProxyNote()
|
|
712
|
-
console.log(chalk.dim(' Fix ~/.gemini/settings.json, then try again.'))
|
|
713
|
-
console.log()
|
|
714
|
-
return 1
|
|
513
|
+
if (mode === 'amp') {
|
|
514
|
+
const result = writeAmpConfig(model, baseUrl, paths)
|
|
515
|
+
return {
|
|
516
|
+
command: 'amp',
|
|
517
|
+
args: [],
|
|
518
|
+
env,
|
|
519
|
+
apiKey,
|
|
520
|
+
baseUrl,
|
|
521
|
+
meta,
|
|
522
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
715
523
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (mode === 'pi') {
|
|
527
|
+
const result = writePiConfig(model, apiKey, baseUrl, paths)
|
|
528
|
+
return {
|
|
529
|
+
command: 'pi',
|
|
530
|
+
args: ['--provider', 'freeCodingModels', '--model', model.modelId, '--api-key', apiKey],
|
|
531
|
+
env,
|
|
532
|
+
apiKey,
|
|
533
|
+
baseUrl,
|
|
534
|
+
meta,
|
|
535
|
+
configArtifacts: [
|
|
536
|
+
{ path: result.filePath, backupPath: result.backupPath, label: 'models' },
|
|
537
|
+
{ path: result.settingsFilePath, backupPath: result.settingsBackupPath, label: 'settings' },
|
|
538
|
+
],
|
|
725
539
|
}
|
|
540
|
+
}
|
|
726
541
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
542
|
+
return {
|
|
543
|
+
blocked: true,
|
|
544
|
+
exitCode: 1,
|
|
545
|
+
warnings: [chalk.red(` X Unsupported external tool mode: ${mode}`)],
|
|
546
|
+
meta,
|
|
547
|
+
configArtifacts: [],
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function startExternalTool(mode, model, config) {
|
|
552
|
+
const launchPlan = prepareExternalToolLaunch(mode, model, config)
|
|
553
|
+
const { meta } = launchPlan
|
|
554
|
+
|
|
555
|
+
if (launchPlan.blocked) {
|
|
556
|
+
for (const warning of launchPlan.warnings || []) console.log(warning)
|
|
557
|
+
console.log()
|
|
558
|
+
return launchPlan.exitCode || 1
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
console.log(chalk.cyan(` ▶ Launching ${meta.label} with ${chalk.bold(model.label)}...`))
|
|
562
|
+
printConfigArtifacts(meta.label, launchPlan.configArtifacts)
|
|
563
|
+
|
|
564
|
+
if (mode === 'aider') {
|
|
565
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (mode === 'crush') {
|
|
569
|
+
console.log(chalk.dim(' 📖 Crush will use the provider directly for this launch.'))
|
|
570
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (mode === 'goose') {
|
|
574
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
740
575
|
}
|
|
741
576
|
|
|
742
577
|
if (mode === 'qwen') {
|
|
743
|
-
|
|
744
|
-
return spawnCommand('qwen', [], env)
|
|
578
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
745
579
|
}
|
|
746
580
|
|
|
747
581
|
if (mode === 'openhands') {
|
|
748
|
-
// 📖 OpenHands supports LLM_MODEL env var to set the default model
|
|
749
|
-
env.LLM_MODEL = model.modelId
|
|
750
|
-
env.LLM_API_KEY = apiKey || env.LLM_API_KEY
|
|
751
|
-
if (baseUrl) env.LLM_BASE_URL = baseUrl
|
|
752
582
|
console.log(chalk.dim(` 📖 OpenHands launched with model: ${model.modelId}`))
|
|
753
|
-
return spawnCommand(
|
|
583
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
754
584
|
}
|
|
755
585
|
|
|
756
586
|
if (mode === 'amp') {
|
|
757
|
-
printConfigResult(meta.label, writeAmpConfig(model, baseUrl))
|
|
758
587
|
console.log(chalk.dim(` 📖 Amp config updated with model: ${model.modelId}`))
|
|
759
|
-
return spawnCommand(
|
|
588
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
760
589
|
}
|
|
761
590
|
|
|
762
591
|
if (mode === 'pi') {
|
|
763
|
-
const piResult = writePiConfig(model, apiKey, baseUrl)
|
|
764
|
-
printConfigResult(meta.label, { filePath: piResult.filePath, backupPath: piResult.backupPath })
|
|
765
|
-
printConfigResult(meta.label, { filePath: piResult.settingsFilePath, backupPath: piResult.settingsBackupPath })
|
|
766
592
|
// 📖 Pi supports --provider and --model flags for guaranteed auto-selection
|
|
767
|
-
return spawnCommand(
|
|
593
|
+
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
768
594
|
}
|
|
769
595
|
|
|
770
596
|
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|