free-coding-models 0.3.0 → 0.3.2
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 +24 -0
- package/README.md +29 -20
- package/bin/free-coding-models.js +50 -19
- package/package.json +1 -1
- package/src/anthropic-translator.js +78 -8
- package/src/cli-help.js +108 -0
- package/src/config.js +2 -1
- package/src/endpoint-installer.js +5 -4
- package/src/key-handler.js +31 -34
- package/src/opencode.js +17 -12
- package/src/overlays.js +40 -53
- package/src/proxy-server.js +335 -12
- package/src/proxy-sync.js +16 -4
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +34 -36
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +246 -19
- package/src/utils.js +31 -8
package/src/tool-launchers.js
CHANGED
|
@@ -19,15 +19,22 @@
|
|
|
19
19
|
* 📖 Crush: writes crush.json with provider config + models.large/small defaults
|
|
20
20
|
* 📖 Pi: uses --provider/--model CLI flags for guaranteed auto-selection
|
|
21
21
|
* 📖 Aider: writes ~/.aider.conf.yml + passes --model flag
|
|
22
|
-
* 📖 Claude Code: uses ANTHROPIC_BASE_URL
|
|
22
|
+
* 📖 Claude Code: uses ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN only (mirrors free-claude-code)
|
|
23
|
+
* 📖 Codex CLI: uses a custom model_provider override so Codex stays in explicit API-provider mode
|
|
24
|
+
* 📖 Gemini CLI: proxy mode is capability-gated because older builds do not support custom base URL routing cleanly
|
|
23
25
|
*
|
|
24
26
|
* @functions
|
|
25
27
|
* → `resolveLauncherModelId` — choose the provider-specific id or proxy slug for a launch
|
|
28
|
+
* → `applyClaudeCodeModelOverrides` — force Claude Code auxiliary model slots onto the chosen proxy model
|
|
29
|
+
* → `buildClaudeProxyAuthToken` — encode the proxy token + selected model hint for Claude-only fallback routing
|
|
30
|
+
* → `buildCodexProxyArgs` — force Codex into a proxy-backed custom provider config
|
|
31
|
+
* → `inspectGeminiCliSupport` — detect whether the installed Gemini CLI can use proxy mode safely
|
|
26
32
|
* → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
|
|
27
33
|
* → `writeCrushConfig` — write provider + models.large/small to crush.json
|
|
28
34
|
* → `startExternalTool` — configure and launch the selected external tool mode
|
|
29
35
|
*
|
|
30
|
-
* @exports resolveLauncherModelId,
|
|
36
|
+
* @exports resolveLauncherModelId, applyClaudeCodeModelOverrides, buildClaudeProxyAuthToken
|
|
37
|
+
* @exports buildCodexProxyArgs, inspectGeminiCliSupport, startExternalTool
|
|
31
38
|
*
|
|
32
39
|
* @see src/tool-metadata.js
|
|
33
40
|
* @see src/provider-metadata.js
|
|
@@ -35,10 +42,10 @@
|
|
|
35
42
|
*/
|
|
36
43
|
|
|
37
44
|
import chalk from 'chalk'
|
|
38
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'
|
|
45
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, copyFileSync } from 'fs'
|
|
39
46
|
import { homedir } from 'os'
|
|
40
|
-
import { dirname, join } from 'path'
|
|
41
|
-
import { spawn } from 'child_process'
|
|
47
|
+
import { delimiter, dirname, join } from 'path'
|
|
48
|
+
import { spawn, spawnSync } from 'child_process'
|
|
42
49
|
import { sources } from '../sources.js'
|
|
43
50
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
44
51
|
import { getApiKey, getProxySettings } from './config.js'
|
|
@@ -47,6 +54,36 @@ import { getToolMeta } from './tool-metadata.js'
|
|
|
47
54
|
import { ensureProxyRunning, resolveProxyModelId } from './opencode.js'
|
|
48
55
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
49
56
|
|
|
57
|
+
const OPENAI_COMPAT_ENV_KEYS = [
|
|
58
|
+
'OPENAI_API_KEY',
|
|
59
|
+
'OPENAI_BASE_URL',
|
|
60
|
+
'OPENAI_API_BASE',
|
|
61
|
+
'OPENAI_MODEL',
|
|
62
|
+
'LLM_API_KEY',
|
|
63
|
+
'LLM_BASE_URL',
|
|
64
|
+
'LLM_MODEL',
|
|
65
|
+
]
|
|
66
|
+
const ANTHROPIC_ENV_KEYS = [
|
|
67
|
+
'ANTHROPIC_API_KEY',
|
|
68
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
69
|
+
'ANTHROPIC_BASE_URL',
|
|
70
|
+
'ANTHROPIC_MODEL',
|
|
71
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
72
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
73
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
74
|
+
'ANTHROPIC_SMALL_FAST_MODEL',
|
|
75
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
76
|
+
]
|
|
77
|
+
const GEMINI_ENV_KEYS = [
|
|
78
|
+
'GEMINI_API_KEY',
|
|
79
|
+
'GOOGLE_API_KEY',
|
|
80
|
+
'GOOGLE_GEMINI_BASE_URL',
|
|
81
|
+
'GOOGLE_VERTEX_BASE_URL',
|
|
82
|
+
]
|
|
83
|
+
const PROXY_SANITIZED_ENV_KEYS = [...OPENAI_COMPAT_ENV_KEYS, ...ANTHROPIC_ENV_KEYS, ...GEMINI_ENV_KEYS]
|
|
84
|
+
const GEMINI_PROXY_MIN_VERSION = '0.34.0'
|
|
85
|
+
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.'
|
|
86
|
+
|
|
50
87
|
function ensureDir(filePath) {
|
|
51
88
|
const dir = dirname(filePath)
|
|
52
89
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
@@ -82,6 +119,16 @@ function getProviderBaseUrl(providerKey) {
|
|
|
82
119
|
.replace(/\/predictions$/i, '')
|
|
83
120
|
}
|
|
84
121
|
|
|
122
|
+
function deleteEnvKeys(env, keys) {
|
|
123
|
+
for (const key of keys) delete env[key]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cloneInheritedEnv(inheritedEnv = process.env, sanitizeKeys = []) {
|
|
127
|
+
const env = { ...inheritedEnv }
|
|
128
|
+
deleteEnvKeys(env, sanitizeKeys)
|
|
129
|
+
return env
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
function applyOpenAiCompatEnv(env, apiKey, baseUrl, modelId) {
|
|
86
133
|
if (!apiKey || !baseUrl || !modelId) return env
|
|
87
134
|
env.OPENAI_API_KEY = apiKey
|
|
@@ -107,17 +154,45 @@ export function resolveLauncherModelId(model, useProxy = false) {
|
|
|
107
154
|
return model?.modelId ?? ''
|
|
108
155
|
}
|
|
109
156
|
|
|
110
|
-
function
|
|
157
|
+
export function applyClaudeCodeModelOverrides(env, modelId) {
|
|
158
|
+
const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
|
|
159
|
+
if (!resolvedModelId) return env
|
|
160
|
+
|
|
161
|
+
// 📖 Claude Code still uses auxiliary model slots (opus/sonnet/haiku/subagents)
|
|
162
|
+
// 📖 even when a custom primary model is selected. Pin them all to the same slug.
|
|
163
|
+
env.ANTHROPIC_MODEL = resolvedModelId
|
|
164
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolvedModelId
|
|
165
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolvedModelId
|
|
166
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolvedModelId
|
|
167
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = resolvedModelId
|
|
168
|
+
env.CLAUDE_CODE_SUBAGENT_MODEL = resolvedModelId
|
|
169
|
+
return env
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function buildClaudeProxyAuthToken(proxyToken, modelId) {
|
|
173
|
+
const resolvedProxyToken = typeof proxyToken === 'string' ? proxyToken.trim() : ''
|
|
174
|
+
const resolvedModelId = typeof modelId === 'string' ? modelId.trim() : ''
|
|
175
|
+
if (!resolvedProxyToken) return ''
|
|
176
|
+
return resolvedModelId ? `${resolvedProxyToken}:${resolvedModelId}` : resolvedProxyToken
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildToolEnv(mode, model, config, options = {}) {
|
|
180
|
+
const {
|
|
181
|
+
sanitize = false,
|
|
182
|
+
includeCompatDefaults = true,
|
|
183
|
+
includeProviderEnv = true,
|
|
184
|
+
inheritedEnv = process.env,
|
|
185
|
+
} = options
|
|
111
186
|
const providerKey = model.providerKey
|
|
112
187
|
const providerUrl = sources[providerKey]?.url || ''
|
|
113
188
|
const baseUrl = getProviderBaseUrl(providerKey)
|
|
114
189
|
const apiKey = getApiKey(config, providerKey)
|
|
115
|
-
const env =
|
|
190
|
+
const env = cloneInheritedEnv(inheritedEnv, sanitize ? PROXY_SANITIZED_ENV_KEYS : [])
|
|
116
191
|
const providerEnvName = ENV_VAR_NAMES[providerKey]
|
|
117
|
-
if (providerEnvName && apiKey) env[providerEnvName] = apiKey
|
|
192
|
+
if (includeProviderEnv && providerEnvName && apiKey) env[providerEnvName] = apiKey
|
|
118
193
|
|
|
119
194
|
// 📖 OpenAI-compatible defaults reused by multiple CLIs.
|
|
120
|
-
if (apiKey && baseUrl) {
|
|
195
|
+
if (includeCompatDefaults && apiKey && baseUrl) {
|
|
121
196
|
env.OPENAI_API_KEY = apiKey
|
|
122
197
|
env.OPENAI_BASE_URL = baseUrl
|
|
123
198
|
env.OPENAI_API_BASE = baseUrl
|
|
@@ -135,6 +210,7 @@ function buildToolEnv(mode, model, config) {
|
|
|
135
210
|
}
|
|
136
211
|
|
|
137
212
|
if (mode === 'gemini' && apiKey && baseUrl) {
|
|
213
|
+
env.GEMINI_API_KEY = apiKey
|
|
138
214
|
env.GOOGLE_API_KEY = apiKey
|
|
139
215
|
env.GOOGLE_GEMINI_BASE_URL = baseUrl
|
|
140
216
|
}
|
|
@@ -142,6 +218,114 @@ function buildToolEnv(mode, model, config) {
|
|
|
142
218
|
return { env, apiKey, baseUrl, providerUrl }
|
|
143
219
|
}
|
|
144
220
|
|
|
221
|
+
export function buildCodexProxyArgs(baseUrl) {
|
|
222
|
+
return [
|
|
223
|
+
'-c', 'model_provider="fcm_proxy"',
|
|
224
|
+
'-c', 'model_providers.fcm_proxy.name="FCM Proxy V2"',
|
|
225
|
+
'-c', `model_providers.fcm_proxy.base_url=${JSON.stringify(baseUrl)}`,
|
|
226
|
+
'-c', 'model_providers.fcm_proxy.env_key="FCM_PROXY_API_KEY"',
|
|
227
|
+
'-c', 'model_providers.fcm_proxy.wire_api="responses"',
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function compareSemver(a, b) {
|
|
232
|
+
const left = String(a || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
233
|
+
const right = String(b || '').split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
234
|
+
const length = Math.max(left.length, right.length)
|
|
235
|
+
for (let idx = 0; idx < length; idx++) {
|
|
236
|
+
const lhs = left[idx] || 0
|
|
237
|
+
const rhs = right[idx] || 0
|
|
238
|
+
if (lhs > rhs) return 1
|
|
239
|
+
if (lhs < rhs) return -1
|
|
240
|
+
}
|
|
241
|
+
return 0
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function findExecutableOnPath(command) {
|
|
245
|
+
const pathValue = process.env.PATH || ''
|
|
246
|
+
const candidates = process.platform === 'win32'
|
|
247
|
+
? [command, `${command}.cmd`, `${command}.exe`]
|
|
248
|
+
: [command]
|
|
249
|
+
|
|
250
|
+
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
251
|
+
for (const candidate of candidates) {
|
|
252
|
+
const fullPath = join(dir, candidate)
|
|
253
|
+
try {
|
|
254
|
+
accessSync(fullPath, constants.X_OK)
|
|
255
|
+
return fullPath
|
|
256
|
+
} catch { /* not executable */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findPackageJsonUpwards(startPath) {
|
|
263
|
+
let current = dirname(startPath)
|
|
264
|
+
while (current && current !== dirname(current)) {
|
|
265
|
+
const packageJsonPath = join(current, 'package.json')
|
|
266
|
+
if (existsSync(packageJsonPath)) return packageJsonPath
|
|
267
|
+
current = dirname(current)
|
|
268
|
+
}
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function detectGeminiCliVersion(binaryPath) {
|
|
273
|
+
if (!binaryPath) return null
|
|
274
|
+
try {
|
|
275
|
+
const realPath = realpathSync(binaryPath)
|
|
276
|
+
const versionMatch = realPath.match(/gemini-cli[\\/](\d+\.\d+\.\d+)(?:[\\/]|$)/)
|
|
277
|
+
if (versionMatch?.[1]) return versionMatch[1]
|
|
278
|
+
|
|
279
|
+
const packageJsonPath = findPackageJsonUpwards(realPath)
|
|
280
|
+
if (!packageJsonPath) return null
|
|
281
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
282
|
+
if (typeof pkg?.version === 'string' && pkg.version.length > 0) {
|
|
283
|
+
return pkg.version
|
|
284
|
+
}
|
|
285
|
+
} catch { /* best effort */ }
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function extractGeminiConfigError(output) {
|
|
290
|
+
const text = String(output || '').trim()
|
|
291
|
+
if (!text.includes('Invalid configuration in ')) return null
|
|
292
|
+
const lines = text.split(/\r?\n/).filter(Boolean)
|
|
293
|
+
return lines.slice(0, 8).join('\n')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function inspectGeminiCliSupport(options = {}) {
|
|
297
|
+
const binaryPath = options.binaryPath || findExecutableOnPath(options.command || 'gemini')
|
|
298
|
+
if (!binaryPath) {
|
|
299
|
+
return {
|
|
300
|
+
installed: false,
|
|
301
|
+
version: null,
|
|
302
|
+
supportsProxyBaseUrl: false,
|
|
303
|
+
configError: null,
|
|
304
|
+
reason: 'Gemini CLI is not installed in PATH.',
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const version = options.version || detectGeminiCliVersion(binaryPath)
|
|
309
|
+
const helpResult = options.helpResult || spawnSync(binaryPath, ['--help'], {
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
timeout: 5000,
|
|
312
|
+
env: options.inheritedEnv || process.env,
|
|
313
|
+
})
|
|
314
|
+
const helpOutput = `${helpResult.stdout || ''}\n${helpResult.stderr || ''}`.trim()
|
|
315
|
+
const configError = extractGeminiConfigError(helpOutput)
|
|
316
|
+
const supportsProxyBaseUrl = version ? compareSemver(version, GEMINI_PROXY_MIN_VERSION) >= 0 : false
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
installed: true,
|
|
320
|
+
version,
|
|
321
|
+
supportsProxyBaseUrl,
|
|
322
|
+
configError,
|
|
323
|
+
reason: supportsProxyBaseUrl
|
|
324
|
+
? null
|
|
325
|
+
: `Gemini CLI ${version || '(unknown version)'} does not expose stable custom base URL support for proxy mode yet.`,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
145
329
|
function spawnCommand(command, args, env) {
|
|
146
330
|
return new Promise((resolve, reject) => {
|
|
147
331
|
const child = spawn(command, args, {
|
|
@@ -347,6 +531,10 @@ function printConfigResult(toolName, result) {
|
|
|
347
531
|
if (result.backupPath) console.log(chalk.dim(` 💾 Backup: ${result.backupPath}`))
|
|
348
532
|
}
|
|
349
533
|
|
|
534
|
+
function printExperimentalProxyNote() {
|
|
535
|
+
console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
|
|
536
|
+
}
|
|
537
|
+
|
|
350
538
|
export async function startExternalTool(mode, model, config) {
|
|
351
539
|
const meta = getToolMeta(mode)
|
|
352
540
|
const { env, apiKey, baseUrl } = buildToolEnv(mode, model, config)
|
|
@@ -429,6 +617,7 @@ export async function startExternalTool(mode, model, config) {
|
|
|
429
617
|
console.log()
|
|
430
618
|
console.log(chalk.yellow(' The proxy translates between provider protocols and handles key rotation,'))
|
|
431
619
|
console.log(chalk.yellow(' which is required for this tool to connect.'))
|
|
620
|
+
console.log(chalk.dim(` ${EXPERIMENTAL_PROXY_TOOLS_NOTE}`))
|
|
432
621
|
console.log()
|
|
433
622
|
console.log(chalk.white(' To enable it:'))
|
|
434
623
|
console.log(chalk.dim(' 1. Press ') + chalk.bold.white('J') + chalk.dim(' to open FCM Proxy V2 settings'))
|
|
@@ -441,33 +630,71 @@ export async function startExternalTool(mode, model, config) {
|
|
|
441
630
|
|
|
442
631
|
if (mode === 'claude-code') {
|
|
443
632
|
// 📖 Claude Code needs Anthropic-compatible wire format (POST /v1/messages).
|
|
444
|
-
// 📖
|
|
633
|
+
// 📖 Mirror free-claude-code: one auth env only (`ANTHROPIC_AUTH_TOKEN`) plus base URL.
|
|
445
634
|
const started = await ensureProxyRunning(config)
|
|
635
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
636
|
+
sanitize: true,
|
|
637
|
+
includeCompatDefaults: false,
|
|
638
|
+
includeProviderEnv: false,
|
|
639
|
+
})
|
|
446
640
|
const proxyBase = `http://127.0.0.1:${started.port}`
|
|
447
|
-
env.ANTHROPIC_BASE_URL = proxyBase
|
|
448
|
-
env.ANTHROPIC_API_KEY = started.proxyToken
|
|
449
641
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
642
|
+
proxyEnv.ANTHROPIC_BASE_URL = proxyBase
|
|
643
|
+
proxyEnv.ANTHROPIC_AUTH_TOKEN = buildClaudeProxyAuthToken(started.proxyToken, launchModelId)
|
|
644
|
+
applyClaudeCodeModelOverrides(proxyEnv, launchModelId)
|
|
450
645
|
console.log(chalk.dim(` 📖 Claude Code routed through FCM proxy on :${started.port} (Anthropic translation enabled)`))
|
|
451
|
-
return spawnCommand('claude', ['--model', launchModelId],
|
|
646
|
+
return spawnCommand('claude', ['--model', launchModelId], proxyEnv)
|
|
452
647
|
}
|
|
453
648
|
|
|
454
649
|
if (mode === 'codex') {
|
|
455
650
|
const started = await ensureProxyRunning(config)
|
|
456
|
-
env
|
|
457
|
-
|
|
651
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
652
|
+
sanitize: true,
|
|
653
|
+
includeCompatDefaults: false,
|
|
654
|
+
includeProviderEnv: false,
|
|
655
|
+
})
|
|
458
656
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
657
|
+
const proxyBaseUrl = `http://127.0.0.1:${started.port}/v1`
|
|
658
|
+
proxyEnv.FCM_PROXY_API_KEY = started.proxyToken
|
|
459
659
|
console.log(chalk.dim(` 📖 Codex routed through FCM proxy on :${started.port}`))
|
|
460
|
-
return spawnCommand('codex', ['--model', launchModelId],
|
|
660
|
+
return spawnCommand('codex', [...buildCodexProxyArgs(proxyBaseUrl), '--model', launchModelId], proxyEnv)
|
|
461
661
|
}
|
|
462
662
|
|
|
463
663
|
if (mode === 'gemini') {
|
|
664
|
+
const geminiSupport = inspectGeminiCliSupport()
|
|
665
|
+
if (geminiSupport.configError) {
|
|
666
|
+
console.log()
|
|
667
|
+
console.log(chalk.red(' ✖ Gemini CLI configuration is invalid, so the proxy launch is blocked before auth.'))
|
|
668
|
+
console.log(chalk.dim(` ${geminiSupport.configError.split('\n').join('\n ')}`))
|
|
669
|
+
printExperimentalProxyNote()
|
|
670
|
+
console.log(chalk.dim(' Fix ~/.gemini/settings.json, then try again.'))
|
|
671
|
+
console.log()
|
|
672
|
+
return 1
|
|
673
|
+
}
|
|
674
|
+
if (!geminiSupport.supportsProxyBaseUrl) {
|
|
675
|
+
console.log()
|
|
676
|
+
const versionLabel = geminiSupport.version ? `v${geminiSupport.version}` : 'this installed version'
|
|
677
|
+
console.log(chalk.red(` ✖ Gemini CLI ${versionLabel} is not proxy-compatible in FCM yet.`))
|
|
678
|
+
console.log(chalk.yellow(' This build does not expose the custom base-URL contract we need, so launching it through the proxy would be misleading.'))
|
|
679
|
+
printExperimentalProxyNote()
|
|
680
|
+
console.log(chalk.dim(` Expected: Gemini CLI ${GEMINI_PROXY_MIN_VERSION}+ with stable proxy base URL support.`))
|
|
681
|
+
console.log()
|
|
682
|
+
return 1
|
|
683
|
+
}
|
|
684
|
+
|
|
464
685
|
const started = await ensureProxyRunning(config)
|
|
465
|
-
env.OPENAI_API_KEY = started.proxyToken
|
|
466
|
-
env.OPENAI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
|
|
467
686
|
const launchModelId = resolveLauncherModelId(model, true)
|
|
687
|
+
const { env: proxyEnv } = buildToolEnv(mode, model, config, {
|
|
688
|
+
sanitize: true,
|
|
689
|
+
includeCompatDefaults: false,
|
|
690
|
+
includeProviderEnv: false,
|
|
691
|
+
})
|
|
692
|
+
proxyEnv.GEMINI_API_KEY = started.proxyToken
|
|
693
|
+
proxyEnv.GOOGLE_API_KEY = started.proxyToken
|
|
694
|
+
proxyEnv.GOOGLE_GEMINI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
|
|
468
695
|
printConfigResult(meta.label, writeGeminiConfig({ ...model, modelId: launchModelId }))
|
|
469
696
|
console.log(chalk.dim(` 📖 Gemini routed through FCM proxy on :${started.port}`))
|
|
470
|
-
return spawnCommand('gemini', ['--model', launchModelId],
|
|
697
|
+
return spawnCommand('gemini', ['--model', launchModelId], proxyEnv)
|
|
471
698
|
}
|
|
472
699
|
|
|
473
700
|
if (mode === 'qwen') {
|
package/src/utils.js
CHANGED
|
@@ -386,16 +386,16 @@ export function findBestModel(results) {
|
|
|
386
386
|
// 📖 Slices from index 2 to get user-provided arguments only.
|
|
387
387
|
//
|
|
388
388
|
// 📖 Argument types:
|
|
389
|
-
// - API key: first positional arg that
|
|
389
|
+
// - API key: first positional arg that does not look like a CLI flag (e.g., "nvapi-xxx")
|
|
390
390
|
// - Boolean flags: --best, --fiable, --opencode, --opencode-desktop, --openclaw,
|
|
391
391
|
// --aider, --crush, --goose, --claude-code, --codex, --gemini, --qwen,
|
|
392
|
-
// --openhands, --amp, --pi, --no-telemetry, --json (case-insensitive)
|
|
392
|
+
// --openhands, --amp, --pi, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
393
393
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
394
394
|
//
|
|
395
395
|
// 📖 Returns:
|
|
396
396
|
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode,
|
|
397
397
|
// aiderMode, crushMode, gooseMode, claudeCodeMode, codexMode, geminiMode,
|
|
398
|
-
// qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, tierFilter }
|
|
398
|
+
// qwenMode, openHandsMode, ampMode, piMode, noTelemetry, jsonMode, helpMode, tierFilter }
|
|
399
399
|
//
|
|
400
400
|
// 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
|
|
401
401
|
export function parseArgs(argv) {
|
|
@@ -420,7 +420,7 @@ export function parseArgs(argv) {
|
|
|
420
420
|
if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
|
|
421
421
|
|
|
422
422
|
for (const [i, arg] of args.entries()) {
|
|
423
|
-
if (arg.startsWith('--')) {
|
|
423
|
+
if (arg.startsWith('--') || arg === '-h') {
|
|
424
424
|
flags.push(arg.toLowerCase())
|
|
425
425
|
} else if (skipIndices.has(i)) {
|
|
426
426
|
// 📖 Skip — this is a value for --tier or --profile, not an API key
|
|
@@ -447,6 +447,7 @@ export function parseArgs(argv) {
|
|
|
447
447
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
448
448
|
const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
|
|
449
449
|
const jsonMode = flags.includes('--json')
|
|
450
|
+
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
450
451
|
|
|
451
452
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
452
453
|
|
|
@@ -475,6 +476,7 @@ export function parseArgs(argv) {
|
|
|
475
476
|
noTelemetry,
|
|
476
477
|
cleanProxyMode,
|
|
477
478
|
jsonMode,
|
|
479
|
+
helpMode,
|
|
478
480
|
tierFilter,
|
|
479
481
|
profileName,
|
|
480
482
|
recommendMode
|
|
@@ -707,17 +709,31 @@ export function getProxyStatusInfo(proxyStartupStatus, isProxyActive, isProxyEna
|
|
|
707
709
|
}
|
|
708
710
|
|
|
709
711
|
/**
|
|
710
|
-
* 📖 getVersionStatusInfo turns
|
|
712
|
+
* 📖 getVersionStatusInfo turns startup + manual update-check state into a compact,
|
|
711
713
|
* 📖 render-friendly footer descriptor for the main table.
|
|
712
714
|
*
|
|
713
|
-
* 📖
|
|
714
|
-
* 📖
|
|
715
|
+
* 📖 Priority:
|
|
716
|
+
* 📖 1. Manual Settings check found an update (`available`)
|
|
717
|
+
* 📖 2. Startup auto-check already found a newer npm version
|
|
718
|
+
* 📖 3. Otherwise stay quiet
|
|
719
|
+
* 📖
|
|
720
|
+
* 📖 `versionAlertsEnabled` lets the CLI suppress npm-specific warnings in dev checkouts,
|
|
721
|
+
* 📖 where telling contributors to run a global npm update would be bogus.
|
|
715
722
|
*
|
|
716
723
|
* @param {'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'} updateState
|
|
717
724
|
* @param {string|null} latestVersion
|
|
725
|
+
* @param {string|null} [startupLatestVersion=null]
|
|
726
|
+
* @param {boolean} [versionAlertsEnabled=true]
|
|
718
727
|
* @returns {{ isOutdated: boolean, latestVersion: string|null }}
|
|
719
728
|
*/
|
|
720
|
-
export function getVersionStatusInfo(updateState, latestVersion) {
|
|
729
|
+
export function getVersionStatusInfo(updateState, latestVersion, startupLatestVersion = null, versionAlertsEnabled = true) {
|
|
730
|
+
if (!versionAlertsEnabled) {
|
|
731
|
+
return {
|
|
732
|
+
isOutdated: false,
|
|
733
|
+
latestVersion: null,
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
721
737
|
if (updateState === 'available' && typeof latestVersion === 'string' && latestVersion.trim()) {
|
|
722
738
|
return {
|
|
723
739
|
isOutdated: true,
|
|
@@ -725,6 +741,13 @@ export function getVersionStatusInfo(updateState, latestVersion) {
|
|
|
725
741
|
}
|
|
726
742
|
}
|
|
727
743
|
|
|
744
|
+
if (typeof startupLatestVersion === 'string' && startupLatestVersion.trim()) {
|
|
745
|
+
return {
|
|
746
|
+
isOutdated: true,
|
|
747
|
+
latestVersion: startupLatestVersion.trim(),
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
728
751
|
return {
|
|
729
752
|
isOutdated: false,
|
|
730
753
|
latestVersion: null,
|