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.
- package/README.md +12 -8
- package/bin/free-coding-models.js +2 -0
- package/changelog/v0.5.96.md +22 -0
- package/package.json +1 -1
- package/sources.js +50 -7
- package/src/core/config.js +16 -0
- package/src/core/i18n/index.js +49 -0
- package/src/core/i18n/locales/en.js +991 -0
- package/src/core/i18n/locales/zh-CN.js +991 -0
- package/src/core/opencode.js +15 -8
- package/src/core/ping.js +31 -1
- package/src/core/playground.js +20 -19
- package/src/core/provider-key-tester.js +1 -0
- package/src/core/provider-metadata.js +9 -0
- package/src/core/quota-capabilities.js +2 -0
- package/src/core/router-daemon.js +37 -6
- package/src/core/router-dashboard.js +15 -14
- package/src/core/router-v2/tui-dashboard.js +27 -26
- package/src/tui/app.js +2 -0
- package/src/tui/cli-help.js +45 -21
- package/src/tui/command-palette.js +19 -1
- package/src/tui/key-handler.js +21 -1
- package/src/tui/overlays.js +113 -107
- package/src/tui/render-helpers.js +4 -3
- package/src/tui/render-table.js +137 -132
- package/src/tui/theme.js +2 -0
- package/web/dist/assets/index-Cq9X7sN8.js +45 -0
- package/web/dist/assets/index-DZFdxy2N.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +58 -6
- package/web/dist/assets/index-BOpGJzSO.js +0 -45
- package/web/dist/assets/index-CCaxIOti.css +0 -1
package/src/tui/cli-help.js
CHANGED
|
@@ -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('
|
|
110
|
+
lines.push(header(t('cli.routerHowItWorks')))
|
|
87
111
|
lines.push('')
|
|
88
112
|
|
|
89
|
-
lines.push(header(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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, '
|
|
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, '
|
|
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
|
|
203
|
+
lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
|
|
180
204
|
}
|
|
181
205
|
lines.push('')
|
|
182
|
-
lines.push(`${indent}${paint(chalk, chalk?.bold, '
|
|
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
|
|
208
|
+
lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
|
|
185
209
|
}
|
|
186
210
|
lines.push('')
|
|
187
|
-
lines.push(`${indent}${paint(chalk, chalk?.bold, '
|
|
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
|
|
213
|
+
lines.push(formatEntry(entry.flag, entryDescription(entry), { chalk, indent }))
|
|
190
214
|
}
|
|
191
215
|
lines.push('')
|
|
192
|
-
lines.push(`${indent}${paint(chalk, chalk?.dim, '
|
|
193
|
-
lines.push(`${indent}${paint(chalk, chalk?.dim, '
|
|
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, '
|
|
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
|
-
|
|
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
|
/**
|
package/src/tui/key-handler.js
CHANGED
|
@@ -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
|
|
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
|