free-coding-models 0.3.11 → 0.3.13
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 +112 -1134
- package/bin/free-coding-models.js +18 -170
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +90 -443
- 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 +28 -520
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +11 -19
- 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 +3 -68
- 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 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- 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
|
@@ -13,10 +13,6 @@
|
|
|
13
13
|
* - it merges into existing config files instead of replacing them
|
|
14
14
|
* - it records successful installs in `~/.free-coding-models.json` so catalogs can be refreshed automatically later
|
|
15
15
|
*
|
|
16
|
-
* 📖 Connection modes:
|
|
17
|
-
* - `direct` — connect the tool straight to the provider API endpoint (no proxy)
|
|
18
|
-
* - `proxy` — route through the local FCM proxy (key rotation + usage tracking)
|
|
19
|
-
*
|
|
20
16
|
* 📖 Tool-specific notes:
|
|
21
17
|
* - OpenCode CLI and OpenCode Desktop share the same `opencode.json`
|
|
22
18
|
* - Crush gets a managed provider block in `crush.json`
|
|
@@ -25,7 +21,6 @@
|
|
|
25
21
|
* - Pi gets models.json + settings.json under ~/.pi/agent/
|
|
26
22
|
* - Aider gets ~/.aider.conf.yml with OpenAI-compatible config
|
|
27
23
|
* - Amp gets ~/.config/amp/settings.json
|
|
28
|
-
* - Gemini gets ~/.gemini/settings.json
|
|
29
24
|
* - Qwen gets ~/.qwen/settings.json with modelProviders
|
|
30
25
|
* - OpenHands gets a sourceable env file (~/.fcm-openhands-env)
|
|
31
26
|
*
|
|
@@ -49,21 +44,15 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
|
49
44
|
import { homedir } from 'node:os'
|
|
50
45
|
import { dirname, join } from 'node:path'
|
|
51
46
|
import { MODELS, sources } from '../sources.js'
|
|
52
|
-
import { getApiKey, saveConfig
|
|
47
|
+
import { getApiKey, saveConfig } from './config.js'
|
|
53
48
|
import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
54
49
|
import { getToolMeta } from './tool-metadata.js'
|
|
55
50
|
|
|
56
51
|
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai'])
|
|
57
52
|
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
58
|
-
// 📖 Claude Code, Codex, and Gemini stay
|
|
53
|
+
// 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
|
|
59
54
|
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp']
|
|
60
55
|
|
|
61
|
-
// 📖 Connection modes: direct (pure provider) vs FCM proxy (rotates keys)
|
|
62
|
-
export const CONNECTION_MODES = [
|
|
63
|
-
{ key: 'direct', label: 'Direct Provider', hint: 'Connect the tool straight to the provider API — no proxy involved.' },
|
|
64
|
-
{ key: 'proxy', label: 'FCM Proxy V2', hint: 'Route through FCM Proxy V2 with key rotation and usage tracking.' },
|
|
65
|
-
]
|
|
66
|
-
|
|
67
56
|
function getDefaultPaths() {
|
|
68
57
|
const home = homedir()
|
|
69
58
|
return {
|
|
@@ -76,7 +65,6 @@ function getDefaultPaths() {
|
|
|
76
65
|
piSettingsPath: join(home, '.pi', 'agent', 'settings.json'),
|
|
77
66
|
aiderConfigPath: join(home, '.aider.conf.yml'),
|
|
78
67
|
ampConfigPath: join(home, '.config', 'amp', 'settings.json'),
|
|
79
|
-
geminiConfigPath: join(home, '.gemini', 'settings.json'),
|
|
80
68
|
qwenConfigPath: join(home, '.qwen', 'settings.json'),
|
|
81
69
|
}
|
|
82
70
|
}
|
|
@@ -199,7 +187,7 @@ function getDirectInstallSupport(providerKey) {
|
|
|
199
187
|
return { supported: false, reason: 'Unknown provider' }
|
|
200
188
|
}
|
|
201
189
|
if (DIRECT_INSTALL_UNSUPPORTED_PROVIDERS.has(providerKey)) {
|
|
202
|
-
return { supported: false, reason: 'This provider needs a
|
|
190
|
+
return { supported: false, reason: 'This provider still needs a dedicated runtime bridge' }
|
|
203
191
|
}
|
|
204
192
|
if (providerKey === 'cloudflare' && !(process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()) {
|
|
205
193
|
return { supported: false, reason: 'CLOUDFLARE_ACCOUNT_ID is required for direct installs' }
|
|
@@ -360,13 +348,17 @@ function installIntoOpenClaw(providerKey, models, apiKey, paths) {
|
|
|
360
348
|
const filePath = paths.openclawConfigPath
|
|
361
349
|
const providerId = getManagedProviderId(providerKey)
|
|
362
350
|
const config = readJson(filePath, {})
|
|
351
|
+
const primaryModel = models[0]
|
|
352
|
+
const primaryModelRef = primaryModel ? `${providerId}/${primaryModel.modelId}` : null
|
|
363
353
|
|
|
364
354
|
if (!config.models || typeof config.models !== 'object') config.models = {}
|
|
365
355
|
if (config.models.mode !== 'replace') config.models.mode = 'merge'
|
|
366
356
|
if (!config.models.providers || typeof config.models.providers !== 'object') config.models.providers = {}
|
|
367
357
|
if (!config.agents || typeof config.agents !== 'object') config.agents = {}
|
|
368
358
|
if (!config.agents.defaults || typeof config.agents.defaults !== 'object') config.agents.defaults = {}
|
|
359
|
+
if (!config.agents.defaults.model || typeof config.agents.defaults.model !== 'object') config.agents.defaults.model = {}
|
|
369
360
|
if (!config.agents.defaults.models || typeof config.agents.defaults.models !== 'object') config.agents.defaults.models = {}
|
|
361
|
+
if (!config.env || typeof config.env !== 'object') config.env = {}
|
|
370
362
|
|
|
371
363
|
config.models.providers[providerId] = {
|
|
372
364
|
baseUrl: resolveProviderBaseUrl(providerKey),
|
|
@@ -394,8 +386,17 @@ function installIntoOpenClaw(providerKey, models, apiKey, paths) {
|
|
|
394
386
|
config.agents.defaults.models[`${providerId}/${model.modelId}`] = {}
|
|
395
387
|
}
|
|
396
388
|
|
|
389
|
+
if (primaryModelRef) {
|
|
390
|
+
config.agents.defaults.model.primary = primaryModelRef
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const providerEnvName = ENV_VAR_NAMES[providerKey]
|
|
394
|
+
if (providerEnvName && apiKey) {
|
|
395
|
+
config.env[providerEnvName] = apiKey
|
|
396
|
+
}
|
|
397
|
+
|
|
397
398
|
const backupPath = writeJson(filePath, config)
|
|
398
|
-
return { path: filePath, backupPath, providerId, modelCount: models.length }
|
|
399
|
+
return { path: filePath, backupPath, providerId, modelCount: models.length, primaryModelRef }
|
|
399
400
|
}
|
|
400
401
|
|
|
401
402
|
// 📖 installIntoPi writes models.json + settings.json under ~/.pi/agent/
|
|
@@ -453,15 +454,6 @@ function installIntoAmp(providerKey, models, apiKey, paths) {
|
|
|
453
454
|
return { path: paths.ampConfigPath, backupPath, providerId, modelCount: models.length }
|
|
454
455
|
}
|
|
455
456
|
|
|
456
|
-
// 📖 installIntoGemini writes ~/.gemini/settings.json with model ID
|
|
457
|
-
function installIntoGemini(providerKey, models, apiKey, paths) {
|
|
458
|
-
const providerId = getManagedProviderId(providerKey)
|
|
459
|
-
const config = readJson(paths.geminiConfigPath, {})
|
|
460
|
-
config.model = models[0]?.modelId ?? ''
|
|
461
|
-
const backupPath = writeJson(paths.geminiConfigPath, config)
|
|
462
|
-
return { path: paths.geminiConfigPath, backupPath, providerId, modelCount: models.length }
|
|
463
|
-
}
|
|
464
|
-
|
|
465
457
|
// 📖 installIntoQwen writes ~/.qwen/settings.json with modelProviders config
|
|
466
458
|
function installIntoQwen(providerKey, models, apiKey, paths) {
|
|
467
459
|
const providerId = getManagedProviderId(providerKey)
|
|
@@ -486,36 +478,22 @@ function installIntoQwen(providerKey, models, apiKey, paths) {
|
|
|
486
478
|
return { path: paths.qwenConfigPath, backupPath, providerId, modelCount: models.length }
|
|
487
479
|
}
|
|
488
480
|
|
|
489
|
-
// 📖 installIntoEnvBasedTool handles tools that rely on env vars only
|
|
481
|
+
// 📖 installIntoEnvBasedTool handles tools that rely on env vars only.
|
|
490
482
|
// 📖 We write a small .env-style helper file so users can source it before launching.
|
|
491
|
-
|
|
492
|
-
function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, connectionMode = 'direct') {
|
|
483
|
+
function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode) {
|
|
493
484
|
const providerId = getManagedProviderId(providerKey)
|
|
494
485
|
const home = homedir()
|
|
495
486
|
const envFileName = `.fcm-${toolMode}-env`
|
|
496
487
|
const envFilePath = join(home, envFileName)
|
|
497
488
|
const primaryModel = models[0]
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
let effectiveBaseUrl = resolveProviderBaseUrl(providerKey)
|
|
502
|
-
let effectiveModelId = primaryModel.modelId
|
|
503
|
-
|
|
504
|
-
if (connectionMode === 'proxy') {
|
|
505
|
-
// 📖 Read stable proxy settings from config for daemon-compatible env files
|
|
506
|
-
try {
|
|
507
|
-
const cfg = loadConfig()
|
|
508
|
-
const proxySettings = getProxySettings(cfg)
|
|
509
|
-
effectiveApiKey = proxySettings.stableToken || apiKey
|
|
510
|
-
const port = proxySettings.preferredPort || 18045
|
|
511
|
-
effectiveBaseUrl = `http://127.0.0.1:${port}/v1`
|
|
512
|
-
} catch { /* fallback to direct values */ }
|
|
513
|
-
}
|
|
489
|
+
const effectiveApiKey = apiKey
|
|
490
|
+
const effectiveBaseUrl = resolveProviderBaseUrl(providerKey)
|
|
491
|
+
const effectiveModelId = primaryModel.modelId
|
|
514
492
|
|
|
515
493
|
const envLines = [
|
|
516
494
|
'# 📖 Managed by free-coding-models — source this file before launching the tool',
|
|
517
495
|
`# 📖 Provider: ${getProviderLabel(providerKey)} (${models.length} models)`,
|
|
518
|
-
|
|
496
|
+
'# 📖 Connection: Direct provider',
|
|
519
497
|
`export OPENAI_API_KEY="${effectiveApiKey}"`,
|
|
520
498
|
`export OPENAI_BASE_URL="${effectiveBaseUrl}"`,
|
|
521
499
|
`export OPENAI_MODEL="${effectiveModelId}"`,
|
|
@@ -524,19 +502,6 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, c
|
|
|
524
502
|
`export LLM_MODEL="openai/${effectiveModelId}"`,
|
|
525
503
|
]
|
|
526
504
|
|
|
527
|
-
// 📖 Claude Code: Anthropic-specific env vars pointing to proxy /v1/messages endpoint
|
|
528
|
-
if (toolMode === 'claude-code') {
|
|
529
|
-
if (connectionMode === 'proxy') {
|
|
530
|
-
// 📖 Point to proxy base (not /v1) — Claude Code adds /v1/messages itself
|
|
531
|
-
const proxyBase = effectiveBaseUrl.replace(/\/v1$/, '')
|
|
532
|
-
envLines.push(`export ANTHROPIC_AUTH_TOKEN="${effectiveApiKey}"`)
|
|
533
|
-
envLines.push(`export ANTHROPIC_BASE_URL="${proxyBase}"`)
|
|
534
|
-
} else {
|
|
535
|
-
envLines.push(`export ANTHROPIC_AUTH_TOKEN="${effectiveApiKey}"`)
|
|
536
|
-
envLines.push(`export ANTHROPIC_BASE_URL="${effectiveBaseUrl}"`)
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
505
|
ensureDirFor(envFilePath)
|
|
541
506
|
const backupPath = backupIfExists(envFilePath)
|
|
542
507
|
writeFileSync(envFilePath, envLines.join('\n') + '\n')
|
|
@@ -545,7 +510,6 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, c
|
|
|
545
510
|
|
|
546
511
|
export function installProviderEndpoints(config, providerKey, toolMode, options = {}) {
|
|
547
512
|
const canonicalToolMode = canonicalizeToolMode(toolMode)
|
|
548
|
-
const connectionMode = options.connectionMode || 'direct'
|
|
549
513
|
const support = getDirectInstallSupport(providerKey)
|
|
550
514
|
if (!support.supported) {
|
|
551
515
|
throw new Error(support.reason || 'Direct install is not supported for this provider')
|
|
@@ -575,12 +539,10 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
575
539
|
installResult = installIntoAider(providerKey, models, apiKey, paths)
|
|
576
540
|
} else if (canonicalToolMode === 'amp') {
|
|
577
541
|
installResult = installIntoAmp(providerKey, models, apiKey, paths)
|
|
578
|
-
} else if (canonicalToolMode === 'gemini') {
|
|
579
|
-
installResult = installIntoGemini(providerKey, models, apiKey, paths)
|
|
580
542
|
} else if (canonicalToolMode === 'qwen') {
|
|
581
543
|
installResult = installIntoQwen(providerKey, models, apiKey, paths)
|
|
582
|
-
} else if (canonicalToolMode === '
|
|
583
|
-
installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths
|
|
544
|
+
} else if (canonicalToolMode === 'openhands') {
|
|
545
|
+
installResult = installIntoEnvBasedTool(providerKey, models, apiKey, canonicalToolMode, paths)
|
|
584
546
|
} else {
|
|
585
547
|
throw new Error(`Unsupported install target: ${toolMode}`)
|
|
586
548
|
}
|
|
@@ -597,7 +559,7 @@ export function installProviderEndpoints(config, providerKey, toolMode, options
|
|
|
597
559
|
providerKey,
|
|
598
560
|
providerLabel: getProviderLabel(providerKey),
|
|
599
561
|
scope,
|
|
600
|
-
connectionMode,
|
|
562
|
+
connectionMode: 'direct',
|
|
601
563
|
autoRefreshEnabled: true,
|
|
602
564
|
models,
|
|
603
565
|
}
|