free-coding-models 0.2.17 → 0.3.0
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 +53 -0
- package/README.md +96 -32
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +98 -20
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +370 -0
- package/src/config.js +24 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +41 -16
- package/src/key-handler.js +327 -148
- package/src/opencode.js +30 -32
- package/src/overlays.js +272 -184
- package/src/proxy-server.js +488 -6
- package/src/proxy-sync.js +552 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-table.js +24 -15
- package/src/tool-launchers.js +138 -18
package/src/render-table.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
15
15
|
* - Smart badges (mode, tier filter, origin filter, profile)
|
|
16
|
-
* - Proxy
|
|
16
|
+
* - Footer J badge: green "Proxy On" / red "Proxy Off" indicator with direct overlay access
|
|
17
17
|
* - Install-endpoints shortcut surfaced directly in the footer hints
|
|
18
18
|
* - Distinct auth-failure vs missing-key health labels so configured providers stay honest
|
|
19
19
|
*
|
|
@@ -40,7 +40,7 @@ import { TIER_COLOR } from './tier-colors.js'
|
|
|
40
40
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
41
41
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
42
42
|
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
43
|
-
import { calculateViewport, sortResultsWithPinnedFavorites,
|
|
43
|
+
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay } from './render-helpers.js'
|
|
44
44
|
import { getToolMeta } from './tool-metadata.js'
|
|
45
45
|
|
|
46
46
|
const ACTIVE_FILTER_BG_BY_TIER = {
|
|
@@ -92,7 +92,7 @@ export function setActiveProxy(proxyInstance) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
95
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, isOutdated = false, latestVersion = null) {
|
|
95
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, isOutdated = false, latestVersion = null) {
|
|
96
96
|
// 📖 Filter out hidden models for display
|
|
97
97
|
const visibleResults = results.filter(r => !r.hidden)
|
|
98
98
|
|
|
@@ -193,23 +193,25 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
193
193
|
const W_TOKENS = 7
|
|
194
194
|
const W_USAGE = 7
|
|
195
195
|
const MIN_TABLE_WIDTH = 166
|
|
196
|
-
const warningDurationMs =
|
|
196
|
+
const warningDurationMs = 4_000
|
|
197
197
|
const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
|
|
198
198
|
const remainingMs = Math.max(0, warningDurationMs - elapsed)
|
|
199
|
-
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && remainingMs > 0
|
|
199
|
+
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && widthWarningShowCount < 2 && remainingMs > 0
|
|
200
200
|
|
|
201
201
|
if (showWidthWarning) {
|
|
202
202
|
const lines = []
|
|
203
|
-
const blankLines = Math.max(0, Math.floor(((terminalRows || 24) -
|
|
204
|
-
const warning = 'Please maximize your terminal for optimal use.'
|
|
205
|
-
const warning2 = 'The current terminal is too small.'
|
|
206
|
-
const warning3 = 'Reduce font size or maximize width of terminal.'
|
|
203
|
+
const blankLines = Math.max(0, Math.floor(((terminalRows || 24) - 7) / 2))
|
|
204
|
+
const warning = '🖥️ Please maximize your terminal for optimal use.'
|
|
205
|
+
const warning2 = '⚠️ The current terminal is too small.'
|
|
206
|
+
const warning3 = '📏 Reduce font size or maximize width of terminal.'
|
|
207
207
|
const padLeft = Math.max(0, Math.floor((terminalCols - warning.length) / 2))
|
|
208
208
|
const padLeft2 = Math.max(0, Math.floor((terminalCols - warning2.length) / 2))
|
|
209
209
|
const padLeft3 = Math.max(0, Math.floor((terminalCols - warning3.length) / 2))
|
|
210
210
|
for (let i = 0; i < blankLines; i++) lines.push('')
|
|
211
211
|
lines.push(' '.repeat(padLeft) + chalk.red.bold(warning))
|
|
212
|
+
lines.push('')
|
|
212
213
|
lines.push(' '.repeat(padLeft2) + chalk.red(warning2))
|
|
214
|
+
lines.push('')
|
|
213
215
|
lines.push(' '.repeat(padLeft3) + chalk.red(warning3))
|
|
214
216
|
lines.push('')
|
|
215
217
|
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 34) / 2))) + chalk.yellow(`this message will hide in ${(remainingMs / 1000).toFixed(1)}s`))
|
|
@@ -620,7 +622,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
620
622
|
const activeHotkey = (keyLabel, text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg)(` ${keyLabel}${text} `)
|
|
621
623
|
// 📖 Line 1: core navigation + filtering shortcuts
|
|
622
624
|
lines.push(
|
|
623
|
-
|
|
625
|
+
(proxyEnabled ? activeHotkey('J', ' 📡 FCM Proxy V2 On') : activeHotkey('J', ' 📡 FCM Proxy V2 Off', [180, 30, 30], [255, 255, 255])) +
|
|
626
|
+
chalk.dim(` • `) +
|
|
624
627
|
hotkey('F', ' Toggle Favorite') +
|
|
625
628
|
chalk.dim(` • `) +
|
|
626
629
|
(tierFilterMode > 0
|
|
@@ -631,7 +634,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
631
634
|
? activeHotkey('D', ` Provider (${activeOriginLabel})`, [0, 0, 0], PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
632
635
|
: hotkey('D', ' Provider')) +
|
|
633
636
|
chalk.dim(` • `) +
|
|
634
|
-
(hideUnconfiguredModels ? activeHotkey('E', ' Configured Only') : hotkey('E', ' Configured Only')) +
|
|
637
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only') : hotkey('E', ' Configured Models Only')) +
|
|
635
638
|
chalk.dim(` • `) +
|
|
636
639
|
hotkey('X', ' Token Logs') +
|
|
637
640
|
chalk.dim(` • `) +
|
|
@@ -639,10 +642,16 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
639
642
|
chalk.dim(` • `) +
|
|
640
643
|
hotkey('K', ' Help')
|
|
641
644
|
)
|
|
642
|
-
// 📖 Line 2: profiles, install flow, recommend,
|
|
643
|
-
lines.push(
|
|
644
|
-
|
|
645
|
-
|
|
645
|
+
// 📖 Line 2: profiles, install flow, recommend, proxy shortcut, feedback, and extended hints.
|
|
646
|
+
lines.push(
|
|
647
|
+
chalk.dim(` `) +
|
|
648
|
+
hotkey('⇧P', ' Cycle profile') + chalk.dim(` • `) +
|
|
649
|
+
hotkey('⇧S', ' Save profile') + chalk.dim(` • `) +
|
|
650
|
+
hotkey('Y', ' Install endpoints') + chalk.dim(` • `) +
|
|
651
|
+
hotkey('Q', ' Smart Recommend') + chalk.dim(` • `) +
|
|
652
|
+
hotkey('I', ' Feedback, bugs & requests')
|
|
653
|
+
)
|
|
654
|
+
// 📖 Proxy status is now shown via the J badge in line 2 above — no need for a dedicated line
|
|
646
655
|
if (versionStatus.isOutdated) {
|
|
647
656
|
const outdatedBadge = chalk.bgRed.bold.yellow(' This version is outdated . ')
|
|
648
657
|
const latestLabel = chalk.redBright(` local v${LOCAL_VERSION} · latest v${versionStatus.latestVersion}`)
|
package/src/tool-launchers.js
CHANGED
|
@@ -15,8 +15,16 @@
|
|
|
15
15
|
* For those, we prefer a transparent warning over pretending the integration is
|
|
16
16
|
* fully official. The user still gets a reproducible env/config handoff.
|
|
17
17
|
*
|
|
18
|
+
* 📖 Goose: writes custom provider JSON + secrets.yaml + updates config.yaml (GOOSE_PROVIDER/GOOSE_MODEL)
|
|
19
|
+
* 📖 Crush: writes crush.json with provider config + models.large/small defaults
|
|
20
|
+
* 📖 Pi: uses --provider/--model CLI flags for guaranteed auto-selection
|
|
21
|
+
* 📖 Aider: writes ~/.aider.conf.yml + passes --model flag
|
|
22
|
+
* 📖 Claude Code: uses ANTHROPIC_BASE_URL env + --model flag (proxy translates Anthropic ↔ OpenAI)
|
|
23
|
+
*
|
|
18
24
|
* @functions
|
|
19
25
|
* → `resolveLauncherModelId` — choose the provider-specific id or proxy slug for a launch
|
|
26
|
+
* → `writeGooseConfig` — install provider + set GOOSE_PROVIDER/GOOSE_MODEL in config.yaml
|
|
27
|
+
* → `writeCrushConfig` — write provider + models.large/small to crush.json
|
|
20
28
|
* → `startExternalTool` — configure and launch the selected external tool mode
|
|
21
29
|
*
|
|
22
30
|
* @exports resolveLauncherModelId, startExternalTool
|
|
@@ -37,6 +45,7 @@ import { getApiKey, getProxySettings } from './config.js'
|
|
|
37
45
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
38
46
|
import { getToolMeta } from './tool-metadata.js'
|
|
39
47
|
import { ensureProxyRunning, resolveProxyModelId } from './opencode.js'
|
|
48
|
+
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
40
49
|
|
|
41
50
|
function ensureDir(filePath) {
|
|
42
51
|
const dir = dirname(filePath)
|
|
@@ -173,8 +182,10 @@ function writeCrushConfig(model, apiKey, baseUrl, providerId) {
|
|
|
173
182
|
const filePath = join(homedir(), '.config', 'crush', 'crush.json')
|
|
174
183
|
const backupPath = backupIfExists(filePath)
|
|
175
184
|
const config = readJson(filePath, { $schema: 'https://charm.land/crush.json' })
|
|
176
|
-
|
|
177
|
-
config.options.disable_default_providers
|
|
185
|
+
// 📖 Remove legacy disable_default_providers — it can prevent Crush from auto-selecting models
|
|
186
|
+
if (config.options && config.options.disable_default_providers) {
|
|
187
|
+
delete config.options.disable_default_providers
|
|
188
|
+
}
|
|
178
189
|
if (!config.providers || typeof config.providers !== 'object') config.providers = {}
|
|
179
190
|
config.providers[providerId] = {
|
|
180
191
|
name: 'Free Coding Models',
|
|
@@ -189,10 +200,11 @@ function writeCrushConfig(model, apiKey, baseUrl, providerId) {
|
|
|
189
200
|
],
|
|
190
201
|
}
|
|
191
202
|
// 📖 Crush expects structured selected models at config.models.{large,small}.
|
|
192
|
-
// 📖
|
|
203
|
+
// 📖 Setting both large AND small ensures Crush auto-selects the model in interactive mode.
|
|
193
204
|
config.models = {
|
|
194
205
|
...(config.models && typeof config.models === 'object' ? config.models : {}),
|
|
195
206
|
large: { model: model.modelId, provider: providerId },
|
|
207
|
+
small: { model: model.modelId, provider: providerId },
|
|
196
208
|
}
|
|
197
209
|
writeJson(filePath, config)
|
|
198
210
|
return { filePath, backupPath }
|
|
@@ -252,6 +264,73 @@ function writePiConfig(model, apiKey, baseUrl) {
|
|
|
252
264
|
return { filePath: modelsFilePath, backupPath: modelsBackupPath, settingsFilePath, settingsBackupPath }
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
// 📖 writeGooseConfig: Install/update the provider in Goose's custom_providers/, set the
|
|
268
|
+
// 📖 API key in secrets.yaml, and update config.yaml with GOOSE_PROVIDER + GOOSE_MODEL
|
|
269
|
+
// 📖 so Goose auto-selects the model on launch.
|
|
270
|
+
function writeGooseConfig(model, apiKey, baseUrl, providerKey) {
|
|
271
|
+
const home = homedir()
|
|
272
|
+
const providerId = `fcm-${providerKey}`
|
|
273
|
+
const providerLabel = PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
|
|
274
|
+
const secretEnvName = `FCM_${providerKey.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`
|
|
275
|
+
|
|
276
|
+
// 📖 Step 1: Write custom provider JSON (same format as endpoint-installer)
|
|
277
|
+
const providerDir = join(home, '.config', 'goose', 'custom_providers')
|
|
278
|
+
const providerFilePath = join(providerDir, `${providerId}.json`)
|
|
279
|
+
ensureDir(providerFilePath)
|
|
280
|
+
const providerConfig = {
|
|
281
|
+
name: providerId,
|
|
282
|
+
engine: 'openai',
|
|
283
|
+
display_name: `FCM ${providerLabel}`,
|
|
284
|
+
description: `Managed by free-coding-models for ${providerLabel}`,
|
|
285
|
+
api_key_env: secretEnvName,
|
|
286
|
+
base_url: baseUrl?.endsWith('/chat/completions') ? baseUrl : (baseUrl || ''),
|
|
287
|
+
models: [{ name: model.modelId, context_limit: 128000 }],
|
|
288
|
+
supports_streaming: true,
|
|
289
|
+
requires_auth: true,
|
|
290
|
+
}
|
|
291
|
+
writeFileSync(providerFilePath, JSON.stringify(providerConfig, null, 2) + '\n')
|
|
292
|
+
|
|
293
|
+
// 📖 Step 2: Write API key to secrets.yaml (simple key: value format)
|
|
294
|
+
const secretsPath = join(home, '.config', 'goose', 'secrets.yaml')
|
|
295
|
+
let secretsContent = ''
|
|
296
|
+
if (existsSync(secretsPath)) {
|
|
297
|
+
secretsContent = readFileSync(secretsPath, 'utf8')
|
|
298
|
+
}
|
|
299
|
+
// 📖 Replace existing secret or append new one
|
|
300
|
+
const secretLine = `${secretEnvName}: ${JSON.stringify(apiKey)}`
|
|
301
|
+
const secretRegex = new RegExp(`^${secretEnvName}:.*$`, 'm')
|
|
302
|
+
if (secretRegex.test(secretsContent)) {
|
|
303
|
+
secretsContent = secretsContent.replace(secretRegex, secretLine)
|
|
304
|
+
} else {
|
|
305
|
+
secretsContent = secretsContent.trimEnd() + '\n' + secretLine + '\n'
|
|
306
|
+
}
|
|
307
|
+
ensureDir(secretsPath)
|
|
308
|
+
writeFileSync(secretsPath, secretsContent)
|
|
309
|
+
|
|
310
|
+
// 📖 Step 3: Update config.yaml — set GOOSE_PROVIDER and GOOSE_MODEL at top level
|
|
311
|
+
const configPath = join(home, '.config', 'goose', 'config.yaml')
|
|
312
|
+
let configContent = ''
|
|
313
|
+
if (existsSync(configPath)) {
|
|
314
|
+
configContent = readFileSync(configPath, 'utf8')
|
|
315
|
+
}
|
|
316
|
+
// 📖 Replace or add GOOSE_PROVIDER line
|
|
317
|
+
if (/^GOOSE_PROVIDER:.*/m.test(configContent)) {
|
|
318
|
+
configContent = configContent.replace(/^GOOSE_PROVIDER:.*/m, `GOOSE_PROVIDER: ${providerId}`)
|
|
319
|
+
} else {
|
|
320
|
+
configContent = `GOOSE_PROVIDER: ${providerId}\n` + configContent
|
|
321
|
+
}
|
|
322
|
+
// 📖 Replace or add GOOSE_MODEL line
|
|
323
|
+
if (/^GOOSE_MODEL:.*/m.test(configContent)) {
|
|
324
|
+
configContent = configContent.replace(/^GOOSE_MODEL:.*/m, `GOOSE_MODEL: ${model.modelId}`)
|
|
325
|
+
} else {
|
|
326
|
+
// 📖 Insert after GOOSE_PROVIDER line
|
|
327
|
+
configContent = configContent.replace(/^(GOOSE_PROVIDER:.*)/m, `$1\nGOOSE_MODEL: ${model.modelId}`)
|
|
328
|
+
}
|
|
329
|
+
writeFileSync(configPath, configContent)
|
|
330
|
+
|
|
331
|
+
return { providerFilePath, secretsPath, configPath }
|
|
332
|
+
}
|
|
333
|
+
|
|
255
334
|
function writeAmpConfig(model, baseUrl) {
|
|
256
335
|
const filePath = join(homedir(), '.config', 'amp', 'settings.json')
|
|
257
336
|
const backupPath = backupIfExists(filePath)
|
|
@@ -315,40 +394,80 @@ export async function startExternalTool(mode, model, config) {
|
|
|
315
394
|
}
|
|
316
395
|
|
|
317
396
|
if (mode === 'goose') {
|
|
318
|
-
let gooseBaseUrl = baseUrl
|
|
397
|
+
let gooseBaseUrl = sources[model.providerKey]?.url || baseUrl || ''
|
|
319
398
|
let gooseApiKey = apiKey
|
|
320
399
|
let gooseModelId = resolveLauncherModelId(model, false)
|
|
400
|
+
let gooseProviderKey = model.providerKey
|
|
321
401
|
|
|
322
402
|
if (proxySettings.enabled) {
|
|
323
403
|
const started = await ensureProxyRunning(config)
|
|
324
404
|
gooseApiKey = started.proxyToken
|
|
325
|
-
gooseBaseUrl = `http://127.0.0.1:${started.port}/v1`
|
|
405
|
+
gooseBaseUrl = `http://127.0.0.1:${started.port}/v1/chat/completions`
|
|
326
406
|
gooseModelId = resolveLauncherModelId(model, true)
|
|
327
|
-
|
|
407
|
+
gooseProviderKey = 'proxy'
|
|
328
408
|
console.log(chalk.dim(` 📖 Goose will use the local FCM proxy on :${started.port} for this launch.`))
|
|
329
409
|
}
|
|
330
410
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
console.log(chalk.dim(`
|
|
411
|
+
// 📖 Write Goose config: custom provider JSON + secrets.yaml + config.yaml (GOOSE_PROVIDER/GOOSE_MODEL)
|
|
412
|
+
const gooseResult = writeGooseConfig({ ...model, modelId: gooseModelId }, gooseApiKey, gooseBaseUrl, gooseProviderKey)
|
|
413
|
+
console.log(chalk.dim(` 📄 Goose config updated: ${gooseResult.configPath}`))
|
|
414
|
+
console.log(chalk.dim(` 📄 Provider installed: ${gooseResult.providerFilePath}`))
|
|
415
|
+
|
|
416
|
+
// 📖 Also set env vars as belt-and-suspenders
|
|
417
|
+
env.GOOSE_PROVIDER = `fcm-${gooseProviderKey}`
|
|
418
|
+
env.GOOSE_MODEL = gooseModelId
|
|
419
|
+
applyOpenAiCompatEnv(env, gooseApiKey, gooseBaseUrl.replace(/\/chat\/completions$/, ''), gooseModelId)
|
|
335
420
|
return spawnCommand('goose', [], env)
|
|
336
421
|
}
|
|
337
422
|
|
|
423
|
+
// 📖 Claude Code, Codex, and Gemini require the FCM Proxy V2 background service.
|
|
424
|
+
// 📖 Without it, these tools cannot connect to the free providers (protocol mismatch / no direct support).
|
|
425
|
+
if (mode === 'claude-code' || mode === 'codex' || mode === 'gemini') {
|
|
426
|
+
if (!proxySettings.enabled) {
|
|
427
|
+
console.log()
|
|
428
|
+
console.log(chalk.red(` ✖ ${meta.label} requires FCM Proxy V2 to work with free providers.`))
|
|
429
|
+
console.log()
|
|
430
|
+
console.log(chalk.yellow(' The proxy translates between provider protocols and handles key rotation,'))
|
|
431
|
+
console.log(chalk.yellow(' which is required for this tool to connect.'))
|
|
432
|
+
console.log()
|
|
433
|
+
console.log(chalk.white(' To enable it:'))
|
|
434
|
+
console.log(chalk.dim(' 1. Press ') + chalk.bold.white('J') + chalk.dim(' to open FCM Proxy V2 settings'))
|
|
435
|
+
console.log(chalk.dim(' 2. Enable ') + chalk.bold.white('Proxy mode') + chalk.dim(' and install the ') + chalk.bold.white('background service'))
|
|
436
|
+
console.log(chalk.dim(' 3. Come back and select your model again'))
|
|
437
|
+
console.log()
|
|
438
|
+
return 1
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
338
442
|
if (mode === 'claude-code') {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
443
|
+
// 📖 Claude Code needs Anthropic-compatible wire format (POST /v1/messages).
|
|
444
|
+
// 📖 The FCM proxy natively translates Anthropic ↔ OpenAI.
|
|
445
|
+
const started = await ensureProxyRunning(config)
|
|
446
|
+
const proxyBase = `http://127.0.0.1:${started.port}`
|
|
447
|
+
env.ANTHROPIC_BASE_URL = proxyBase
|
|
448
|
+
env.ANTHROPIC_API_KEY = started.proxyToken
|
|
449
|
+
const launchModelId = resolveLauncherModelId(model, true)
|
|
450
|
+
console.log(chalk.dim(` 📖 Claude Code routed through FCM proxy on :${started.port} (Anthropic translation enabled)`))
|
|
451
|
+
return spawnCommand('claude', ['--model', launchModelId], env)
|
|
342
452
|
}
|
|
343
453
|
|
|
344
454
|
if (mode === 'codex') {
|
|
345
|
-
|
|
346
|
-
|
|
455
|
+
const started = await ensureProxyRunning(config)
|
|
456
|
+
env.OPENAI_API_KEY = started.proxyToken
|
|
457
|
+
env.OPENAI_BASE_URL = `http://127.0.0.1:${started.port}/v1`
|
|
458
|
+
const launchModelId = resolveLauncherModelId(model, true)
|
|
459
|
+
console.log(chalk.dim(` 📖 Codex routed through FCM proxy on :${started.port}`))
|
|
460
|
+
return spawnCommand('codex', ['--model', launchModelId], env)
|
|
347
461
|
}
|
|
348
462
|
|
|
349
463
|
if (mode === 'gemini') {
|
|
350
|
-
|
|
351
|
-
|
|
464
|
+
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
|
+
const launchModelId = resolveLauncherModelId(model, true)
|
|
468
|
+
printConfigResult(meta.label, writeGeminiConfig({ ...model, modelId: launchModelId }))
|
|
469
|
+
console.log(chalk.dim(` 📖 Gemini routed through FCM proxy on :${started.port}`))
|
|
470
|
+
return spawnCommand('gemini', ['--model', launchModelId], env)
|
|
352
471
|
}
|
|
353
472
|
|
|
354
473
|
if (mode === 'qwen') {
|
|
@@ -375,7 +494,8 @@ export async function startExternalTool(mode, model, config) {
|
|
|
375
494
|
const piResult = writePiConfig(model, apiKey, baseUrl)
|
|
376
495
|
printConfigResult(meta.label, { filePath: piResult.filePath, backupPath: piResult.backupPath })
|
|
377
496
|
printConfigResult(meta.label, { filePath: piResult.settingsFilePath, backupPath: piResult.settingsBackupPath })
|
|
378
|
-
|
|
497
|
+
// 📖 Pi supports --provider and --model flags for guaranteed auto-selection
|
|
498
|
+
return spawnCommand('pi', ['--provider', 'freeCodingModels', '--model', model.modelId, '--api-key', apiKey], env)
|
|
379
499
|
}
|
|
380
500
|
|
|
381
501
|
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|