free-coding-models 0.2.17 → 0.3.1
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 +71 -0
- package/README.md +118 -44
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +146 -37
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +440 -0
- package/src/cli-help.js +108 -0
- package/src/config.js +25 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +45 -19
- package/src/key-handler.js +324 -148
- package/src/opencode.js +47 -44
- package/src/overlays.js +282 -207
- package/src/proxy-server.js +746 -10
- package/src/proxy-sync.js +564 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +56 -49
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +343 -26
- package/src/utils.js +31 -8
package/src/opencode.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Launch OpenCode CLI or Desktop
|
|
9
9
|
* - Manage ZAI proxy bridge for non-standard API paths
|
|
10
10
|
* - Start/stop the multi-account proxy server (fcm-proxy)
|
|
11
|
-
* - Auto-start proxy when
|
|
11
|
+
* - Auto-start proxy when the current tool is configured for proxy auto-sync
|
|
12
12
|
*
|
|
13
13
|
* 🎯 Key features:
|
|
14
14
|
* - Provider-aware config setup for OpenCode (NIM, Groq, Cerebras, etc.)
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* - `startOpenCode` — Launch OpenCode CLI with selected model
|
|
22
22
|
* - `startOpenCodeDesktop` — Set model and open Desktop app
|
|
23
23
|
* - `startProxyAndLaunch` — Start fcm-proxy then launch OpenCode
|
|
24
|
-
* - `autoStartProxyIfSynced` — Auto-start proxy
|
|
24
|
+
* - `autoStartProxyIfSynced` — Auto-start proxy and sync the current tool when enabled
|
|
25
25
|
* - `ensureProxyRunning` — Ensure proxy is running (start or reuse)
|
|
26
26
|
* - `isProxyEnabledForConfig` — Check whether proxy mode is opted in
|
|
27
27
|
*
|
|
@@ -39,12 +39,15 @@ import { join } from 'path'
|
|
|
39
39
|
import { copyFileSync, existsSync } from 'fs'
|
|
40
40
|
import { sources } from '../sources.js'
|
|
41
41
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
42
|
-
import { resolveCloudflareUrl } from './ping.js'
|
|
43
42
|
import { ProxyServer } from './proxy-server.js'
|
|
44
|
-
import { loadOpenCodeConfig, saveOpenCodeConfig
|
|
45
|
-
import { getApiKey, getProxySettings
|
|
43
|
+
import { loadOpenCodeConfig, saveOpenCodeConfig } from './opencode-sync.js'
|
|
44
|
+
import { getApiKey, getProxySettings } from './config.js'
|
|
46
45
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
|
|
47
46
|
import { setActiveProxy } from './render-table.js'
|
|
47
|
+
import { buildProxyTopologyFromConfig as _buildTopology } from './proxy-topology.js'
|
|
48
|
+
import { isDaemonRunning, getDaemonInfo } from './daemon-manager.js'
|
|
49
|
+
import { syncProxyToTool, resolveProxySyncToolMode } from './proxy-sync.js'
|
|
50
|
+
import { getToolMeta } from './tool-metadata.js'
|
|
48
51
|
|
|
49
52
|
// 📖 OpenCode config location: ~/.config/opencode/opencode.json on ALL platforms.
|
|
50
53
|
// 📖 OpenCode uses xdg-basedir which resolves to %USERPROFILE%\.config on Windows.
|
|
@@ -527,6 +530,7 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
527
530
|
// ─── Proxy lifecycle ─────────────────────────────────────────────────────────
|
|
528
531
|
|
|
529
532
|
async function cleanupProxy() {
|
|
533
|
+
// 📖 Only clean up in-process proxy. If using daemon, it stays alive.
|
|
530
534
|
if (proxyCleanedUp || !activeProxy) return
|
|
531
535
|
proxyCleanedUp = true
|
|
532
536
|
const proxy = activeProxy
|
|
@@ -546,35 +550,10 @@ function registerExitHandlers() {
|
|
|
546
550
|
process.once('exit', cleanup)
|
|
547
551
|
}
|
|
548
552
|
|
|
553
|
+
// 📖 Thin wrapper that passes module-level mergedModelsRef to the shared topology builder.
|
|
554
|
+
// 📖 The standalone daemon calls _buildTopology() directly with its own merged models.
|
|
549
555
|
export function buildProxyTopologyFromConfig(fcmConfig) {
|
|
550
|
-
|
|
551
|
-
const proxyModels = {}
|
|
552
|
-
|
|
553
|
-
for (const merged of mergedModelsRef) {
|
|
554
|
-
proxyModels[merged.slug] = { name: merged.label }
|
|
555
|
-
|
|
556
|
-
for (const providerEntry of merged.providers) {
|
|
557
|
-
const keys = resolveApiKeys(fcmConfig, providerEntry.providerKey)
|
|
558
|
-
const providerSource = sources[providerEntry.providerKey]
|
|
559
|
-
if (!providerSource) continue
|
|
560
|
-
|
|
561
|
-
const rawUrl = resolveCloudflareUrl(providerSource.url)
|
|
562
|
-
const baseUrl = rawUrl.replace(/\/chat\/completions$/, '')
|
|
563
|
-
|
|
564
|
-
keys.forEach((apiKey, keyIdx) => {
|
|
565
|
-
accounts.push({
|
|
566
|
-
id: `${providerEntry.providerKey}/${merged.slug}/${keyIdx}`,
|
|
567
|
-
providerKey: providerEntry.providerKey,
|
|
568
|
-
proxyModelId: merged.slug,
|
|
569
|
-
modelId: providerEntry.modelId,
|
|
570
|
-
url: baseUrl,
|
|
571
|
-
apiKey,
|
|
572
|
-
})
|
|
573
|
-
})
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return { accounts, proxyModels }
|
|
556
|
+
return _buildTopology(fcmConfig, mergedModelsRef, sources)
|
|
578
557
|
}
|
|
579
558
|
|
|
580
559
|
/**
|
|
@@ -596,6 +575,26 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
|
|
|
596
575
|
throw new Error('Proxy mode is disabled in Settings')
|
|
597
576
|
}
|
|
598
577
|
|
|
578
|
+
// 📖 Phase 1: Check if background daemon is running — delegate instead of starting in-process
|
|
579
|
+
if (!forceRestart) {
|
|
580
|
+
try {
|
|
581
|
+
const daemonRunning = await isDaemonRunning()
|
|
582
|
+
if (daemonRunning) {
|
|
583
|
+
const info = getDaemonInfo()
|
|
584
|
+
if (info) {
|
|
585
|
+
return {
|
|
586
|
+
port: info.port,
|
|
587
|
+
accountCount: info.accountCount || 0,
|
|
588
|
+
proxyToken: info.token,
|
|
589
|
+
proxyModels: null,
|
|
590
|
+
availableModelSlugs: new Set(), // 📖 daemon handles model discovery
|
|
591
|
+
isDaemon: true,
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch { /* daemon check failed — fall through to in-process */ }
|
|
596
|
+
}
|
|
597
|
+
|
|
599
598
|
if (forceRestart && activeProxy) {
|
|
600
599
|
await cleanupProxy()
|
|
601
600
|
}
|
|
@@ -619,8 +618,9 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
|
|
|
619
618
|
throw new Error('No API keys found for proxy-capable models')
|
|
620
619
|
}
|
|
621
620
|
|
|
622
|
-
|
|
621
|
+
// 📖 Use stable token from config so env files / tool configs survive restarts
|
|
623
622
|
const proxySettings = getProxySettings(fcmConfig)
|
|
623
|
+
const proxyToken = proxySettings.stableToken || `fcm_${randomUUID().replace(/-/g, '')}`
|
|
624
624
|
const preferredPort = Number.isInteger(proxySettings.preferredPort) ? proxySettings.preferredPort : 0
|
|
625
625
|
const proxy = new ProxyServer({ port: preferredPort, accounts, proxyApiKey: proxyToken })
|
|
626
626
|
const { port } = await proxy.start()
|
|
@@ -635,24 +635,27 @@ export async function autoStartProxyIfSynced(fcmConfig, state) {
|
|
|
635
635
|
try {
|
|
636
636
|
const proxySettings = getProxySettings(fcmConfig)
|
|
637
637
|
if (!proxySettings.enabled || !proxySettings.syncToOpenCode) return
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
if (!
|
|
638
|
+
const currentToolMode = state?.mode || 'opencode'
|
|
639
|
+
const syncTarget = resolveProxySyncToolMode(currentToolMode)
|
|
640
|
+
if (!syncTarget) return
|
|
641
641
|
|
|
642
642
|
state.proxyStartupStatus = { phase: 'starting' }
|
|
643
643
|
|
|
644
644
|
const started = await ensureProxyRunning(fcmConfig)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
645
|
+
const syncResult = syncProxyToTool(syncTarget, {
|
|
646
|
+
baseUrl: `http://127.0.0.1:${started.port}/v1`,
|
|
647
|
+
token: started.proxyToken,
|
|
648
|
+
}, mergedModelsRef)
|
|
649
|
+
if (!syncResult.success) {
|
|
650
|
+
throw new Error(syncResult.error || `Proxy sync failed for ${syncTarget}`)
|
|
651
|
+
}
|
|
651
652
|
|
|
652
653
|
state.proxyStartupStatus = {
|
|
653
654
|
phase: 'running',
|
|
654
655
|
port: started.port,
|
|
655
656
|
accountCount: started.accountCount,
|
|
657
|
+
tool: getToolMeta(syncTarget).label,
|
|
658
|
+
path: syncResult.path || null,
|
|
656
659
|
}
|
|
657
660
|
} catch (err) {
|
|
658
661
|
state.proxyStartupStatus = {
|
|
@@ -692,7 +695,7 @@ async function startOpenCodeWithProxy(model, port, proxyModelId, proxyModels, fc
|
|
|
692
695
|
|
|
693
696
|
config.provider['fcm-proxy'] = {
|
|
694
697
|
npm: '@ai-sdk/openai-compatible',
|
|
695
|
-
name: 'FCM Proxy',
|
|
698
|
+
name: 'FCM Proxy V2',
|
|
696
699
|
options: {
|
|
697
700
|
baseURL: `http://127.0.0.1:${port}/v1`,
|
|
698
701
|
apiKey: proxyToken
|