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/opencode.js
CHANGED
|
@@ -1,53 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file opencode.js
|
|
3
|
-
* @description OpenCode integration and
|
|
3
|
+
* @description OpenCode integration helpers for direct launches and Desktop setup.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module owns all OpenCode-related behavior:
|
|
7
7
|
* - Configure opencode.json with selected models/providers
|
|
8
8
|
* - Launch OpenCode CLI or Desktop
|
|
9
9
|
* - Manage ZAI proxy bridge for non-standard API paths
|
|
10
|
-
* - Start/stop the multi-account proxy server (fcm-proxy)
|
|
11
|
-
* - Auto-start proxy when the current tool is configured for proxy auto-sync
|
|
12
10
|
*
|
|
13
11
|
* π― Key features:
|
|
14
12
|
* - Provider-aware config setup for OpenCode (NIM, Groq, Cerebras, etc.)
|
|
15
13
|
* - ZAI proxy bridge to rewrite /v1/* β /api/coding/paas/v4/*
|
|
16
14
|
* - Auto-pick tmux port for OpenCode sub-agents
|
|
17
|
-
* - Multi-account proxy with rotation + auto-sync to opencode.json
|
|
18
15
|
*
|
|
19
16
|
* β Functions:
|
|
20
|
-
* - `setOpenCodeModelData` β
|
|
17
|
+
* - `setOpenCodeModelData` β Keep shared merged model references available
|
|
21
18
|
* - `startOpenCode` β Launch OpenCode CLI with selected model
|
|
22
19
|
* - `startOpenCodeDesktop` β Set model and open Desktop app
|
|
23
|
-
* - `startProxyAndLaunch` β Start fcm-proxy then launch OpenCode
|
|
24
|
-
* - `autoStartProxyIfSynced` β Auto-start proxy and sync the current tool when enabled
|
|
25
|
-
* - `ensureProxyRunning` β Ensure proxy is running (start or reuse)
|
|
26
|
-
* - `isProxyEnabledForConfig` β Check whether proxy mode is opted in
|
|
27
20
|
*
|
|
28
|
-
* @see src/opencode-
|
|
29
|
-
* @see src/proxy-server.js β ProxyServer implementation
|
|
21
|
+
* @see src/opencode-config.js β shared OpenCode config read/write helpers
|
|
30
22
|
*/
|
|
31
23
|
|
|
32
24
|
import chalk from 'chalk'
|
|
33
25
|
import { createServer } from 'net'
|
|
34
26
|
import { createServer as createHttpServer } from 'http'
|
|
35
27
|
import { request as httpsRequest } from 'https'
|
|
36
|
-
import { randomUUID } from 'crypto'
|
|
37
28
|
import { homedir } from 'os'
|
|
38
29
|
import { join } from 'path'
|
|
39
30
|
import { copyFileSync, existsSync } from 'fs'
|
|
40
|
-
import { sources } from '../sources.js'
|
|
41
31
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import { getApiKey, getProxySettings } from './config.js'
|
|
32
|
+
import { loadOpenCodeConfig, saveOpenCodeConfig } from './opencode-config.js'
|
|
33
|
+
import { getApiKey } from './config.js'
|
|
45
34
|
import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
|
|
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'
|
|
51
35
|
|
|
52
36
|
// π OpenCode config location: ~/.config/opencode/opencode.json on ALL platforms.
|
|
53
37
|
// π OpenCode uses xdg-basedir which resolves to %USERPROFILE%\.config on Windows.
|
|
@@ -55,12 +39,7 @@ const OPENCODE_CONFIG = join(homedir(), '.config', 'opencode', 'opencode.json')
|
|
|
55
39
|
const OPENCODE_PORT_RANGE_START = 4096
|
|
56
40
|
const OPENCODE_PORT_RANGE_END = 5096
|
|
57
41
|
|
|
58
|
-
// π
|
|
59
|
-
let activeProxy = null
|
|
60
|
-
let proxyCleanedUp = false
|
|
61
|
-
let exitHandlersRegistered = false
|
|
62
|
-
|
|
63
|
-
// π Merged model references for proxy topology.
|
|
42
|
+
// π Keep merged model references available for future OpenCode-related features.
|
|
64
43
|
let mergedModelsRef = []
|
|
65
44
|
let mergedModelByLabelRef = new Map()
|
|
66
45
|
|
|
@@ -70,19 +49,6 @@ export function setOpenCodeModelData(mergedModels, mergedModelByLabel) {
|
|
|
70
49
|
mergedModelByLabelRef = mergedModelByLabel instanceof Map ? mergedModelByLabel : new Map()
|
|
71
50
|
}
|
|
72
51
|
|
|
73
|
-
/**
|
|
74
|
-
* π resolveProxyModelId maps a selected provider-specific model to the shared
|
|
75
|
-
* π proxy catalog slug used by `fcm-proxy`. The proxy exposes merged slugs, not
|
|
76
|
-
* π upstream provider ids, so every launcher that targets the proxy must use this.
|
|
77
|
-
*
|
|
78
|
-
* @param {{ label?: string, modelId?: string }} model
|
|
79
|
-
* @returns {string}
|
|
80
|
-
*/
|
|
81
|
-
export function resolveProxyModelId(model) {
|
|
82
|
-
const merged = mergedModelByLabelRef.get(model?.label)
|
|
83
|
-
return merged?.slug ?? model?.modelId ?? ''
|
|
84
|
-
}
|
|
85
|
-
|
|
86
52
|
// π isTcpPortAvailable: checks if a local TCP port is free for OpenCode.
|
|
87
53
|
// π Used to avoid tmux sub-agent port conflicts when multiple projects run in parallel.
|
|
88
54
|
function isTcpPortAvailable(port) {
|
|
@@ -527,214 +493,6 @@ export async function startOpenCode(model, fcmConfig) {
|
|
|
527
493
|
await spawnOpenCode(['--model', modelRef], providerKey, fcmConfig)
|
|
528
494
|
}
|
|
529
495
|
|
|
530
|
-
// βββ Proxy lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
531
|
-
|
|
532
|
-
async function cleanupProxy() {
|
|
533
|
-
// π Only clean up in-process proxy. If using daemon, it stays alive.
|
|
534
|
-
if (proxyCleanedUp || !activeProxy) return
|
|
535
|
-
proxyCleanedUp = true
|
|
536
|
-
const proxy = activeProxy
|
|
537
|
-
activeProxy = null
|
|
538
|
-
setActiveProxy(activeProxy)
|
|
539
|
-
try {
|
|
540
|
-
await proxy.stop()
|
|
541
|
-
} catch { /* best-effort */ }
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function registerExitHandlers() {
|
|
545
|
-
if (exitHandlersRegistered) return
|
|
546
|
-
exitHandlersRegistered = true
|
|
547
|
-
const cleanup = () => { cleanupProxy().catch(() => {}) }
|
|
548
|
-
process.once('SIGINT', cleanup)
|
|
549
|
-
process.once('SIGTERM', cleanup)
|
|
550
|
-
process.once('exit', cleanup)
|
|
551
|
-
}
|
|
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.
|
|
555
|
-
export function buildProxyTopologyFromConfig(fcmConfig) {
|
|
556
|
-
return _buildTopology(fcmConfig, mergedModelsRef, sources)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* π Proxy mode is opt-in. Both launch-time proxying and persistent sync rely on
|
|
561
|
-
* π this single helper so settings/profile changes behave consistently.
|
|
562
|
-
*
|
|
563
|
-
* @param {object} fcmConfig
|
|
564
|
-
* @returns {boolean}
|
|
565
|
-
*/
|
|
566
|
-
export function isProxyEnabledForConfig(fcmConfig) {
|
|
567
|
-
return getProxySettings(fcmConfig).enabled === true
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {}) {
|
|
571
|
-
registerExitHandlers()
|
|
572
|
-
proxyCleanedUp = false
|
|
573
|
-
|
|
574
|
-
if (!isProxyEnabledForConfig(fcmConfig)) {
|
|
575
|
-
throw new Error('Proxy mode is disabled in Settings')
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// π Always prefer the background daemon when it is available. Launcher code
|
|
579
|
-
// π can update config and let the daemon hot-reload, which is closer to the
|
|
580
|
-
// π Claude proxy model than spinning up tool-specific local proxies.
|
|
581
|
-
try {
|
|
582
|
-
const daemonRunning = await isDaemonRunning()
|
|
583
|
-
if (daemonRunning) {
|
|
584
|
-
const info = getDaemonInfo()
|
|
585
|
-
if (info) {
|
|
586
|
-
return {
|
|
587
|
-
port: info.port,
|
|
588
|
-
accountCount: info.accountCount || 0,
|
|
589
|
-
proxyToken: info.token,
|
|
590
|
-
proxyModels: null,
|
|
591
|
-
availableModelSlugs: new Set(), // π daemon handles model discovery
|
|
592
|
-
isDaemon: true,
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
} catch { /* daemon check failed β fall through to in-process */ }
|
|
597
|
-
|
|
598
|
-
if (forceRestart && activeProxy) {
|
|
599
|
-
await cleanupProxy()
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const existingStatus = activeProxy?.getStatus?.()
|
|
603
|
-
if (existingStatus?.running === true) {
|
|
604
|
-
const availableModelSlugs = new Set(
|
|
605
|
-
(activeProxy._accounts || []).map(a => a.proxyModelId).filter(Boolean)
|
|
606
|
-
)
|
|
607
|
-
return {
|
|
608
|
-
port: existingStatus.port,
|
|
609
|
-
accountCount: existingStatus.accountCount,
|
|
610
|
-
proxyToken: activeProxy?._proxyApiKey,
|
|
611
|
-
proxyModels: null,
|
|
612
|
-
availableModelSlugs,
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const { accounts, proxyModels, anthropicRouting } = buildProxyTopologyFromConfig(fcmConfig)
|
|
617
|
-
if (accounts.length === 0) {
|
|
618
|
-
throw new Error('No API keys found for proxy-capable models')
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// π Use stable token from config so env files / tool configs survive restarts
|
|
622
|
-
const proxySettings = getProxySettings(fcmConfig)
|
|
623
|
-
const proxyToken = proxySettings.stableToken || `fcm_${randomUUID().replace(/-/g, '')}`
|
|
624
|
-
const preferredPort = Number.isInteger(proxySettings.preferredPort) ? proxySettings.preferredPort : 0
|
|
625
|
-
const proxy = new ProxyServer({ port: preferredPort, accounts, proxyApiKey: proxyToken, anthropicRouting })
|
|
626
|
-
const { port } = await proxy.start()
|
|
627
|
-
activeProxy = proxy
|
|
628
|
-
setActiveProxy(activeProxy)
|
|
629
|
-
|
|
630
|
-
const availableModelSlugs = new Set(accounts.map(a => a.proxyModelId).filter(Boolean))
|
|
631
|
-
return { port, accountCount: accounts.length, proxyToken, proxyModels, availableModelSlugs }
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
export async function autoStartProxyIfSynced(fcmConfig, state) {
|
|
635
|
-
try {
|
|
636
|
-
const proxySettings = getProxySettings(fcmConfig)
|
|
637
|
-
if (!proxySettings.enabled || !proxySettings.syncToOpenCode) return
|
|
638
|
-
const currentToolMode = state?.mode || 'opencode'
|
|
639
|
-
const syncTarget = resolveProxySyncToolMode(currentToolMode)
|
|
640
|
-
if (!syncTarget) return
|
|
641
|
-
|
|
642
|
-
state.proxyStartupStatus = { phase: 'starting' }
|
|
643
|
-
|
|
644
|
-
const started = await ensureProxyRunning(fcmConfig)
|
|
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
|
-
}
|
|
652
|
-
|
|
653
|
-
state.proxyStartupStatus = {
|
|
654
|
-
phase: 'running',
|
|
655
|
-
port: started.port,
|
|
656
|
-
accountCount: started.accountCount,
|
|
657
|
-
tool: getToolMeta(syncTarget).label,
|
|
658
|
-
path: syncResult.path || null,
|
|
659
|
-
}
|
|
660
|
-
} catch (err) {
|
|
661
|
-
state.proxyStartupStatus = {
|
|
662
|
-
phase: 'failed',
|
|
663
|
-
reason: err?.message ?? String(err),
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export async function startProxyAndLaunch(model, fcmConfig) {
|
|
669
|
-
try {
|
|
670
|
-
const started = await ensureProxyRunning(fcmConfig, { forceRestart: true })
|
|
671
|
-
const defaultProxyModelId = resolveProxyModelId(model)
|
|
672
|
-
|
|
673
|
-
if (!started.proxyModels || Object.keys(started.proxyModels).length === 0) {
|
|
674
|
-
throw new Error('Proxy model catalog is empty')
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
console.log(chalk.dim(` π Multi-account proxy listening on port ${started.port} (${started.accountCount} accounts)`))
|
|
678
|
-
await startOpenCodeWithProxy(model, started.port, defaultProxyModelId, started.proxyModels, fcmConfig, started.proxyToken)
|
|
679
|
-
} catch (err) {
|
|
680
|
-
console.error(chalk.red(` β Proxy failed to start: ${err.message}`))
|
|
681
|
-
console.log(chalk.dim(' Falling back to direct single-account flowβ¦'))
|
|
682
|
-
await cleanupProxy()
|
|
683
|
-
await startOpenCode(model, fcmConfig)
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async function startOpenCodeWithProxy(model, port, proxyModelId, proxyModels, fcmConfig, proxyToken) {
|
|
688
|
-
const config = loadOpenCodeConfig()
|
|
689
|
-
if (!config.provider) config.provider = {}
|
|
690
|
-
const previousProxyProvider = config.provider['fcm-proxy']
|
|
691
|
-
const previousModel = config.model
|
|
692
|
-
|
|
693
|
-
const fallbackModelId = Object.keys(proxyModels)[0]
|
|
694
|
-
const selectedProxyModelId = proxyModels[proxyModelId] ? proxyModelId : fallbackModelId
|
|
695
|
-
|
|
696
|
-
config.provider['fcm-proxy'] = {
|
|
697
|
-
npm: '@ai-sdk/openai-compatible',
|
|
698
|
-
name: 'FCM Proxy V2',
|
|
699
|
-
options: {
|
|
700
|
-
baseURL: `http://127.0.0.1:${port}/v1`,
|
|
701
|
-
apiKey: proxyToken
|
|
702
|
-
},
|
|
703
|
-
models: proxyModels
|
|
704
|
-
}
|
|
705
|
-
config.model = `fcm-proxy/${selectedProxyModelId}`
|
|
706
|
-
saveOpenCodeConfig(config)
|
|
707
|
-
|
|
708
|
-
console.log(chalk.green(` Setting ${chalk.bold(model.label)} via proxy as default for OpenCodeβ¦`))
|
|
709
|
-
console.log(chalk.dim(` Model: fcm-proxy/${selectedProxyModelId} β’ Proxy: http://127.0.0.1:${port}/v1`))
|
|
710
|
-
console.log(chalk.dim(` Catalog: ${Object.keys(proxyModels).length} models available via fcm-proxy`))
|
|
711
|
-
console.log()
|
|
712
|
-
|
|
713
|
-
try {
|
|
714
|
-
await spawnOpenCode(['--model', `fcm-proxy/${selectedProxyModelId}`], 'fcm-proxy', fcmConfig)
|
|
715
|
-
} finally {
|
|
716
|
-
try {
|
|
717
|
-
const savedCfg = loadOpenCodeConfig()
|
|
718
|
-
if (!savedCfg.provider) savedCfg.provider = {}
|
|
719
|
-
|
|
720
|
-
if (previousProxyProvider) {
|
|
721
|
-
savedCfg.provider['fcm-proxy'] = previousProxyProvider
|
|
722
|
-
} else if (savedCfg.provider['fcm-proxy']) {
|
|
723
|
-
delete savedCfg.provider['fcm-proxy']
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (typeof previousModel === 'string' && previousModel.length > 0) {
|
|
727
|
-
savedCfg.model = previousModel
|
|
728
|
-
} else if (typeof savedCfg.model === 'string' && savedCfg.model.startsWith('fcm-proxy/')) {
|
|
729
|
-
delete savedCfg.model
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
saveOpenCodeConfig(savedCfg)
|
|
733
|
-
} catch { /* best-effort */ }
|
|
734
|
-
await cleanupProxy()
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
496
|
// βββ Start OpenCode Desktop βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
739
497
|
|
|
740
498
|
export async function startOpenCodeDesktop(model, fcmConfig) {
|