free-coding-models 0.5.95 → 0.5.96

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.
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { getToolModeOrder, getToolMeta } from '../core/tool-metadata.js'
22
+ import { t } from '../core/i18n/index.js'
22
23
 
23
24
  const ANALYSIS_FLAGS = [
24
25
  { flag: '--best', description: 'Show only top tiers (A+, S, S+)' },
@@ -71,6 +72,29 @@ const EXAMPLES = [
71
72
  "free-coding-models --json | jq '.[0]'",
72
73
  ]
73
74
 
75
+ const FLAG_DESCRIPTION_KEYS = Object.freeze({
76
+ '--best': 'cli.flag.best', '--fiable': 'cli.flag.fiable', '--json': 'cli.flag.json',
77
+ '--tier <S|A|B|C>': 'cli.flag.tier', '--recommend': 'cli.flag.recommend',
78
+ '--premium': 'cli.flag.premium', '--sort <column>': 'cli.flag.sort',
79
+ '--desc | --asc': 'cli.flag.direction', '--origin <provider>': 'cli.flag.origin',
80
+ '--ping-interval <ms>': 'cli.flag.pingInterval', '--hide-unconfigured': 'cli.flag.hideUnconfigured',
81
+ '--show-unconfigured': 'cli.flag.showUnconfigured', '--reprobe, --no-cache': 'cli.flag.reprobe',
82
+ '--probe-ttl <ms>': 'cli.flag.probeTtl', '--show-broken': 'cli.flag.showBroken',
83
+ '--check-drift': 'cli.flag.checkDrift', '--drift-threshold <N>': 'cli.flag.driftThreshold',
84
+ 'web | --web | --gui': 'cli.flag.web', 'playground | --playground': 'cli.flag.playground',
85
+ '--daemon': 'cli.flag.daemon', '--daemon-bg': 'cli.flag.daemonBg', '--daemon-status': 'cli.flag.daemonStatus',
86
+ '--daemon-stop': 'cli.flag.daemonStop', '--router-v2': 'cli.flag.routerV2', '--router-v2-bg': 'cli.flag.routerV2Bg',
87
+ '--router-v2-status': 'cli.flag.routerV2Status', '--router-v2-stop': 'cli.flag.routerV2Stop',
88
+ '--sync-set [name]': 'cli.flag.syncSet', '--config-dir <dir>': 'cli.flag.configDir',
89
+ '--fix-permissions, --yes, -y': 'cli.flag.fixPermissions', '--no-telemetry': 'cli.flag.noTelemetry',
90
+ '--help, -h': 'cli.flag.help',
91
+ })
92
+
93
+ function entryDescription(entry) {
94
+ const key = FLAG_DESCRIPTION_KEYS[entry.flag]
95
+ return key ? t(key) : entry.description
96
+ }
97
+
74
98
  /**
75
99
  * 📖 buildHowTheRouterWorks — a single-source explanation of the router
76
100
  * 📖 internals (circuit breaker, probe mechanism, pre-prompt) that the
@@ -83,10 +107,10 @@ export function buildHowTheRouterWorksLines({ chalk = null, indent = '' } = {})
83
107
  const body = (text) => `${indent}${paint(chalk, chalk?.dim, text)}`
84
108
  const bullet = (text) => `${indent} • ${text}`
85
109
 
86
- lines.push(header('How the FCM Router Works'))
110
+ lines.push(header(t('cli.routerHowItWorks')))
87
111
  lines.push('')
88
112
 
89
- lines.push(header('1. The smart router daemon'))
113
+ lines.push(header(`1. ${t('cli.routerDaemon')}`))
90
114
  lines.push(body('Point any OpenAI-compatible client at http://localhost:19280/v1'))
91
115
  lines.push(body('with model: "fcm". The daemon picks the healthiest model in'))
92
116
  lines.push(body('your active set and forwards the request — with automatic'))
@@ -95,14 +119,14 @@ export function buildHowTheRouterWorksLines({ chalk = null, indent = '' } = {})
95
119
  lines.push(body('Family failover on the Router Dashboard) before switching family.'))
96
120
  lines.push('')
97
121
 
98
- lines.push(header('2. The pre-prompt (system message)'))
122
+ lines.push(header(`2. ${t('cli.prePrompt')}`))
99
123
  lines.push(body('A first-class system message is injected on every proxied'))
100
124
  lines.push(body('request. The default text introduces the assistant as the FCM'))
101
125
  lines.push(body('routing agent and points the user to the dashboard URL.'))
102
126
  lines.push(body('You can edit it from Settings (Settings → Pre-prompt).'))
103
127
  lines.push('')
104
128
 
105
- lines.push(header('3. The probe mechanism (every 10s/30s/120s)'))
129
+ lines.push(header(`3. ${t('cli.probeMechanism')}`))
106
130
  lines.push(body('The daemon sends a 1-token chat-completion ping to every model'))
107
131
  lines.push(body('in the active set. The probe measures latency + status code, not'))
108
132
  lines.push(body('just URL reachability — so a wrong API key is caught and the'))
@@ -112,7 +136,7 @@ export function buildHowTheRouterWorksLines({ chalk = null, indent = '' } = {})
112
136
  lines.push(bullet('aggressive: probe every 10s (uses more quota)'))
113
137
  lines.push('')
114
138
 
115
- lines.push(header('4. The circuit breaker (per-model state)'))
139
+ lines.push(header(`4. ${t('cli.circuitBreaker')}`))
116
140
  lines.push(body('Each model has a tiny disjoncteur that flips between 3 states.'))
117
141
  lines.push(body('The raw jargon is hidden in the UI — here is what the colors mean:'))
118
142
  lines.push(bullet('Healthy (green) — last probe returned 2xx, route here freely'))
@@ -125,21 +149,21 @@ export function buildHowTheRouterWorksLines({ chalk = null, indent = '' } = {})
125
149
  lines.push(body('then falls through to any provider.'))
126
150
  lines.push('')
127
151
 
128
- lines.push(header('5. Failover order'))
152
+ lines.push(header(`5. ${t('cli.failoverOrder')}`))
129
153
  lines.push(body('Models in the active set are tried in priority order. A model'))
130
154
  lines.push(body('in Recovering/Down/Auth error is skipped — the request goes to'))
131
155
  lines.push(body('the next healthy one. If ALL models fail, you get a 503 with the'))
132
156
  lines.push(body('"models_tried" list in the error body — useful for debugging.'))
133
157
  lines.push('')
134
158
 
135
- lines.push(header('6. Auto-heal (default behavior)'))
159
+ lines.push(header(`6. ${t('cli.autoHeal')}`))
136
160
  lines.push(body('On daemon start, the active set is checked. Any model in Auth'))
137
161
  lines.push(body('error or Deprecated is swapped for a working alternative. The'))
138
162
  lines.push(body('first time you add/remove/reorder a model, auto-heal switches off'))
139
163
  lines.push(body('and your manual choices are preserved.'))
140
164
  lines.push('')
141
165
 
142
- lines.push(header('7. Rate limits (RPD / RPM / TPM)'))
166
+ lines.push(header(`7. ${t('cli.rateLimits')}`))
143
167
  lines.push(body('Each provider has its own quota. Common free-tier limits:'))
144
168
  lines.push(bullet('Groq on-demand: 14 400 RPD, 30 RPM per model'))
145
169
  lines.push(bullet('Mistral La Plateforme: 1 RPS, 1B TPM (experiment plan)'))
@@ -164,35 +188,35 @@ function formatEntry(label, description, { chalk = null, indent = '', labelWidth
164
188
  return `${indent}${coloredLabel} ${coloredDescription}`
165
189
  }
166
190
 
167
- export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help' } = {}) {
191
+ export function buildCliHelpLines({ chalk = null, indent = '', title = null } = {}) {
168
192
  const lines = []
169
193
  const launchFlags = getToolModeOrder()
170
194
  .map((mode) => getToolMeta(mode))
171
195
  .filter((meta) => meta.flag)
172
- .map((meta) => ({ flag: meta.flag, description: `${meta.label} mode` }))
196
+ .map((meta) => ({ flag: meta.flag, description: `${meta.label} ${t('cli.mode')}` }))
173
197
 
174
- lines.push(`${indent}${paint(chalk, chalk?.bold, title)}`)
175
- lines.push(`${indent}${paint(chalk, chalk?.dim, 'Usage: free-coding-models [apiKey] [options]')}`)
198
+ lines.push(`${indent}${paint(chalk, chalk?.bold, title || t('cli.helpTitle'))}`)
199
+ lines.push(`${indent}${paint(chalk, chalk?.dim, `${t('cli.usage')}: free-coding-models [apiKey] [options]`)}`)
176
200
  lines.push('')
177
- lines.push(`${indent}${paint(chalk, chalk?.bold, 'Tool Flags')}`)
201
+ lines.push(`${indent}${paint(chalk, chalk?.bold, t('cli.toolFlags'))}`)
178
202
  for (const entry of launchFlags) {
179
- lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
203
+ lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
180
204
  }
181
205
  lines.push('')
182
- lines.push(`${indent}${paint(chalk, chalk?.bold, 'Analysis Flags')}`)
206
+ lines.push(`${indent}${paint(chalk, chalk?.bold, t('cli.analysisFlags'))}`)
183
207
  for (const entry of ANALYSIS_FLAGS) {
184
- lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
208
+ lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
185
209
  }
186
210
  lines.push('')
187
- lines.push(`${indent}${paint(chalk, chalk?.bold, 'Config & Maintenance')}`)
211
+ lines.push(`${indent}${paint(chalk, chalk?.bold, t('cli.configMaintenance'))}`)
188
212
  for (const entry of CONFIG_FLAGS) {
189
- lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
213
+ lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
190
214
  }
191
215
  lines.push('')
192
- lines.push(`${indent}${paint(chalk, chalk?.dim, 'Default launcher with no tool flag: OpenCode CLI')}`)
193
- lines.push(`${indent}${paint(chalk, chalk?.dim, 'Flags can be combined: --openclaw --tier S --json')}`)
216
+ lines.push(`${indent}${paint(chalk, chalk?.dim, t('cli.defaultLauncher'))}`)
217
+ lines.push(`${indent}${paint(chalk, chalk?.dim, t('cli.combineFlags'))}`)
194
218
  lines.push('')
195
- lines.push(`${indent}${paint(chalk, chalk?.bold, 'Examples')}`)
219
+ lines.push(`${indent}${paint(chalk, chalk?.bold, t('cli.examples'))}`)
196
220
  for (const example of EXAMPLES) {
197
221
  lines.push(`${indent}${paint(chalk, chalk?.cyan, example)}`)
198
222
  }
@@ -18,6 +18,7 @@
18
18
  import { TOOL_METADATA, TOOL_MODE_ORDER } from '../core/tool-metadata.js'
19
19
  import { sources } from '../../sources.js'
20
20
  import { sanitizePingInterval } from './tui-state.js'
21
+ import { getLocale, hasTranslation, t } from '../core/i18n/index.js'
21
22
 
22
23
  // 📖 Max result rows the palette renders (overlays.js slices to this). The
23
24
  // 📖 cursor must never point past the rendered slice, so key-handler clamps
@@ -284,7 +285,24 @@ export function buildCommandPaletteTree(visibleModels = [], config = null) {
284
285
  }
285
286
  }
286
287
 
287
- return tree
288
+ const localize = (nodes) => nodes.map((node) => {
289
+ const labelKey = `palette.command.${node.id}`
290
+ const descriptionKey = `palette.description.${node.id}`
291
+ const isProviderModelsDescription = node.id.startsWith('filter-provider-')
292
+ && node.id !== 'filter-provider-cycle'
293
+ && node.id !== 'filter-provider-all'
294
+ const effectiveDescriptionKey = isProviderModelsDescription
295
+ ? 'palette.description.providerModels'
296
+ : descriptionKey
297
+ if (hasTranslation(getLocale(), labelKey)) node.label = t(labelKey)
298
+ if (hasTranslation(getLocale(), effectiveDescriptionKey)) {
299
+ const provider = isProviderModelsDescription ? node.description?.replace(/ models$/, '') : undefined
300
+ node.description = t(effectiveDescriptionKey, { provider })
301
+ }
302
+ if (Array.isArray(node.children)) node.children = localize(node.children)
303
+ return node
304
+ })
305
+ return localize(tree)
288
306
  }
289
307
 
290
308
  /**
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from '../core/tool-metadata.js'
25
25
  import { loadConfig, saveConfig, replaceConfigContents, getApiKey } from '../core/config.js'
26
+ import { setLocale } from '../core/i18n/index.js'
26
27
  import { sources } from '../../sources.js'
27
28
  import { join, dirname } from 'node:path'
28
29
  import { fileURLToPath } from 'node:url'
@@ -606,6 +607,14 @@ export function createKeyHandler(ctx) {
606
607
  applyThemeSetting(cycleThemeSetting(currentTheme))
607
608
  }
608
609
 
610
+ function cycleLanguage() {
611
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
612
+ const nextLanguage = state.config.settings.language === 'zh-CN' ? 'en' : 'zh-CN'
613
+ state.config.settings.language = nextLanguage
614
+ setLocale(nextLanguage)
615
+ saveConfig(state.config)
616
+ }
617
+
609
618
  function toggleStartupAiSpeedScan() {
610
619
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
611
620
  state.config.settings.runAiSpeedTestOnStartup = state.config.settings.runAiSpeedTestOnStartup !== true
@@ -2607,7 +2616,8 @@ export function createKeyHandler(ctx) {
2607
2616
  const providerKeys = Object.keys(sources)
2608
2617
  const updateRowIdx = providerKeys.length
2609
2618
  const themeRowIdx = updateRowIdx + 1
2610
- const favoritesModeRowIdx = themeRowIdx + 1
2619
+ const languageRowIdx = themeRowIdx + 1
2620
+ const favoritesModeRowIdx = languageRowIdx + 1
2611
2621
  const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
2612
2622
  const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
2613
2623
  const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
@@ -2763,6 +2773,11 @@ export function createKeyHandler(ctx) {
2763
2773
  return
2764
2774
  }
2765
2775
 
2776
+ if (state.settingsCursor === languageRowIdx) {
2777
+ cycleLanguage()
2778
+ return
2779
+ }
2780
+
2766
2781
  if (state.settingsCursor === favoritesModeRowIdx) {
2767
2782
  toggleFavoritesDisplayMode()
2768
2783
  return
@@ -2828,6 +2843,10 @@ export function createKeyHandler(ctx) {
2828
2843
  cycleGlobalTheme()
2829
2844
  return
2830
2845
  }
2846
+ if (state.settingsCursor === languageRowIdx) {
2847
+ cycleLanguage()
2848
+ return
2849
+ }
2831
2850
  if (state.settingsCursor === favoritesModeRowIdx) {
2832
2851
  toggleFavoritesDisplayMode()
2833
2852
  return
@@ -2856,6 +2875,7 @@ export function createKeyHandler(ctx) {
2856
2875
  if (
2857
2876
  state.settingsCursor === updateRowIdx
2858
2877
  || state.settingsCursor === themeRowIdx
2878
+ || state.settingsCursor === languageRowIdx
2859
2879
  || state.settingsCursor === favoritesModeRowIdx
2860
2880
  || state.settingsCursor === startupAiSpeedScanRowIdx
2861
2881
  || state.settingsCursor === autoHideBrokenModelsRowIdx