free-coding-models 0.3.11 β†’ 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/src/opencode.js CHANGED
@@ -1,53 +1,37 @@
1
1
  /**
2
2
  * @file opencode.js
3
- * @description OpenCode integration and multi-account proxy lifecycle.
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` β€” Provide merged model lists for proxy topology
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-sync.js β€” syncToOpenCode/load/save utilities
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 { ProxyServer } from './proxy-server.js'
43
- import { loadOpenCodeConfig, saveOpenCodeConfig } from './opencode-sync.js'
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
- // πŸ“– Module-level proxy state β€” shared between startProxyAndLaunch and cleanup.
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) {