free-coding-models 0.5.95 → 0.5.97
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 +15 -11
- package/bin/free-coding-models.js +2 -0
- package/changelog/v0.5.96.md +22 -0
- package/changelog/v0.5.97.md +24 -0
- package/package.json +1 -1
- package/sources.js +197 -91
- package/src/core/config.js +28 -3
- 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 +11 -2
- package/src/core/quota-capabilities.js +2 -0
- package/src/core/router-daemon.js +58 -8
- package/src/core/router-dashboard.js +15 -14
- package/src/core/router-v2/tui-dashboard.js +27 -26
- package/src/core/shell-env.js +6 -0
- package/src/core/utils.js +22 -0
- 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-C2lIYp0O.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
|
@@ -49,6 +49,7 @@ import { discoverRouterV2Port, testModelViaRouter, testSetViaRouter } from './be
|
|
|
49
49
|
// 📖 After the merge, the v2 engine lives in the MAIN router daemon: all
|
|
50
50
|
// discovery and lifecycle calls target the historical daemon paths/ports.
|
|
51
51
|
import { getRouterPidPath, getRouterPortPath, getRouterPortRange } from '../router-daemon.js'
|
|
52
|
+
import { t } from '../i18n/index.js'
|
|
52
53
|
|
|
53
54
|
export const ROUTER_V2_DASHBOARD_POLL_INTERVAL_MS = 2000
|
|
54
55
|
export const ROUTER_V2_DASHBOARD_FETCH_TIMEOUT_MS = 1500
|
|
@@ -429,15 +430,15 @@ function formatDurationV2(seconds) {
|
|
|
429
430
|
|
|
430
431
|
function stateBadgeV2(modelState) {
|
|
431
432
|
switch (modelState) {
|
|
432
|
-
case 'CLOSED': return { text:
|
|
433
|
-
case 'DEGRADED': return { text:
|
|
434
|
-
case 'HALF_OPEN': return { text:
|
|
435
|
-
case 'OPEN': return { text:
|
|
436
|
-
case 'AUTH_ERROR': return { text:
|
|
437
|
-
case 'QUOTA_PAUSED': return { text:
|
|
438
|
-
case 'STALE': return { text:
|
|
439
|
-
case 'UNSUPPORTED': return { text:
|
|
440
|
-
default: return { text:
|
|
433
|
+
case 'CLOSED': return { text: `✅ ${t('dashboard.working').toUpperCase()}`, color: themeColors.success }
|
|
434
|
+
case 'DEGRADED': return { text: `🟠 ${t('router.circuit.degraded').toUpperCase()}`, color: themeColors.warningBold }
|
|
435
|
+
case 'HALF_OPEN': return { text: `🔁 ${t('router.circuit.probing').toUpperCase()}`, color: themeColors.warning }
|
|
436
|
+
case 'OPEN': return { text: `⛔ ${t('router.circuit.down').toUpperCase()}`, color: themeColors.error }
|
|
437
|
+
case 'AUTH_ERROR': return { text: `🔐 ${t('router.circuit.authError').toUpperCase()}`, color: themeColors.errorBold }
|
|
438
|
+
case 'QUOTA_PAUSED': return { text: `🔥 ${t('router.circuit.quotaPaused').toUpperCase()}`, color: themeColors.warningBold }
|
|
439
|
+
case 'STALE': return { text: `👻 ${t('router.circuit.deprecated').toUpperCase()}`, color: themeColors.dim }
|
|
440
|
+
case 'UNSUPPORTED': return { text: `🚫 ${t('router.circuit.unsupported').toUpperCase()}`, color: themeColors.dim }
|
|
441
|
+
default: return { text: `⏳ ${t('dashboard.pending').toUpperCase()}`, color: themeColors.dim }
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
|
|
@@ -489,17 +490,17 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
489
490
|
// ── Quick Setup ─────────────────────────────────────────────────────────────
|
|
490
491
|
const { defaultPort } = getRouterPortRange()
|
|
491
492
|
const port = state.routerV2Port || defaultPort
|
|
492
|
-
lines.push(` ${themeColors.textBold('
|
|
493
|
+
lines.push(` ${themeColors.textBold(t('router.quickSetup'))} ${themeColors.dim(`(${t('router.v2.beta')})`)} ${themeColors.dim(`— ${t('router.v2.pointToolAtV2')}`)}`)
|
|
493
494
|
lines.push(` ${themeColors.dim('URL')} ${themeColors.infoBold(`http://localhost:${port}/v1`)} ${themeColors.dim('Anthropic:')} ${themeColors.infoBold(`http://localhost:${port}`)} ${themeColors.dim('(POST /v1/messages)')}`)
|
|
494
|
-
lines.push(` ${themeColors.dim('
|
|
495
|
+
lines.push(` ${themeColors.dim(t('router.model'))} ${themeColors.infoBold('fcm')} ${themeColors.dim(t('router.v2.orPinOne'))} ${themeColors.infoBold('fcm:@provider/model')}`)
|
|
495
496
|
lines.push(` ${themeColors.dim('API Key')} ${themeColors.infoBold('fcm-local')}`)
|
|
496
497
|
if (isRunning) {
|
|
497
|
-
lines.push(` ${themeColors.dim('
|
|
498
|
+
lines.push(` ${themeColors.dim(t('router.uptime'))} ${themeColors.success(formatDurationV2(toFiniteNumber(stats?.uptimeSeconds, 0)))} ${themeColors.dim(`${t('router.requests')}:`)} ${themeColors.info(String(toFiniteNumber(stats?.requestsRouted, 0)))} ${themeColors.dim(`${t('router.v2.failoverRate')}:`)} ${themeColors.info(`${Math.round(toFiniteNumber(stats?.history?.failover_rate, 0) * 100)}%`)}`)
|
|
498
499
|
}
|
|
499
500
|
lines.push(` ${separator}`)
|
|
500
501
|
|
|
501
502
|
// ── Fallback chain with live breaker states ─────────────────────────────────
|
|
502
|
-
lines.push(` ${themeColors.textBold('
|
|
503
|
+
lines.push(` ${themeColors.textBold(t('router.v2.fallbackChain'))} ${themeColors.dim(`— ${t('router.v2.exactOrder')}`)}`)
|
|
503
504
|
const routingOrder = Array.isArray(stats?.routingOrder) ? stats.routingOrder : []
|
|
504
505
|
const models = Array.isArray(stats?.models) ? stats.models : []
|
|
505
506
|
const healthByKey = new Map(models.map((m) => [m.key, m]))
|
|
@@ -507,11 +508,11 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
507
508
|
const cursor = state.routerV2CursorIndex ?? 0
|
|
508
509
|
|
|
509
510
|
if (!isRunning) {
|
|
510
|
-
lines.push(` ${themeColors.dim('
|
|
511
|
+
lines.push(` ${themeColors.dim(t('router.v2.startToSeeChain'))}`)
|
|
511
512
|
} else if (routingOrder.length === 0) {
|
|
512
|
-
lines.push(` ${themeColors.warning('
|
|
513
|
+
lines.push(` ${themeColors.warning(t('router.v2.emptyCandidates'))}`)
|
|
513
514
|
} else {
|
|
514
|
-
lines.push(` ${themeColors.dim(padEndDisplay('PRI', 4))} ${themeColors.dim(padEndDisplay('
|
|
515
|
+
lines.push(` ${themeColors.dim(padEndDisplay('PRI', 4))} ${themeColors.dim(padEndDisplay(t('router.model'), 44))} ${themeColors.dim(padEndDisplay(t('router.state'), 15))} ${themeColors.dim(padEndDisplay(t('router.uptime'), 7))} ${themeColors.dim(padEndDisplay('V2 ' + t('router.v2.testViaRouter'), 12))} ${themeColors.dim(t('router.v2.lastError'))}`)
|
|
515
516
|
const maxRows = Math.max(1, routingOrder.length)
|
|
516
517
|
routingOrder.forEach((entry, i) => {
|
|
517
518
|
const health = healthByKey.get(entry.key)
|
|
@@ -535,21 +536,21 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
535
536
|
|
|
536
537
|
// ── Recent requests WITH fallback chains (the v1 gap) ───────────────────────
|
|
537
538
|
lines.push('')
|
|
538
|
-
lines.push(` ${themeColors.textBold('
|
|
539
|
+
lines.push(` ${themeColors.textBold(t('router.v2.requestChains'))} ${themeColors.dim(`— ${t('router.v2.attemptsHint')}`)}`)
|
|
539
540
|
const historyEntries = Array.isArray(state.routerV2History?.entries) ? state.routerV2History.entries : []
|
|
540
541
|
if (!isRunning) {
|
|
541
|
-
lines.push(` ${themeColors.dim('
|
|
542
|
+
lines.push(` ${themeColors.dim(t('router.v2.noHistory'))}`)
|
|
542
543
|
} else if (historyEntries.length === 0) {
|
|
543
|
-
lines.push(` ${themeColors.dim('
|
|
544
|
+
lines.push(` ${themeColors.dim(t('router.noRequests'))}`)
|
|
544
545
|
} else {
|
|
545
546
|
for (const entry of historyEntries.slice(0, 6)) {
|
|
546
547
|
const atMs = Date.parse(entry.at)
|
|
547
548
|
const time = Number.isFinite(atMs) ? new Date(atMs).toLocaleTimeString() : '-'
|
|
548
549
|
const outcome = entry.outcome === 'served'
|
|
549
|
-
? themeColors.success('served')
|
|
550
|
+
? themeColors.success(t('router.v2.outcome.served'))
|
|
550
551
|
: entry.outcome === 'client_aborted'
|
|
551
|
-
? themeColors.dim('aborted')
|
|
552
|
-
: themeColors.error(
|
|
552
|
+
? themeColors.dim(t('router.v2.outcome.aborted'))
|
|
553
|
+
: themeColors.error(t('router.v2.outcome.failed'))
|
|
553
554
|
const chain = attemptChainLabel(entry)
|
|
554
555
|
const skips = Array.isArray(entry.skipped) && entry.skipped.length > 0
|
|
555
556
|
? ` ${themeColors.dim(`[skips: ${entry.skipped.map((s) => s.reason).join(', ')}]`)}`
|
|
@@ -572,7 +573,7 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
572
573
|
`🔐 auth ${stateCounts.AUTH_ERROR ?? 0}`,
|
|
573
574
|
`🔥 quota ${stateCounts.QUOTA_PAUSED ?? 0}`,
|
|
574
575
|
]
|
|
575
|
-
lines.push(` ${themeColors.textBold('
|
|
576
|
+
lines.push(` ${themeColors.textBold(`${t('router.models')}:`)} ${chips.map((c) => themeColors.dim(c)).join(' ')}`)
|
|
576
577
|
const pauses = Array.isArray(stats?.quotaPauses) ? stats.quotaPauses : []
|
|
577
578
|
if (pauses.length > 0) {
|
|
578
579
|
for (const pause of pauses.slice(0, 3)) {
|
|
@@ -592,7 +593,7 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
592
593
|
const isStopped = !isRunning && !isLoading
|
|
593
594
|
const cursorBase = Math.max(1, routingOrder.length)
|
|
594
595
|
const startBtnCursor = cursorBase
|
|
595
|
-
const startBtnText = isStopped ? '
|
|
596
|
+
const startBtnText = isStopped ? `▶ ${t('router.start')} (v2)` : `⏹ ${t('router.stop')} (v2)`
|
|
596
597
|
const startBtnRow = ` [ ${startBtnText} ]`
|
|
597
598
|
lines.push(cursor === startBtnCursor
|
|
598
599
|
? themeColors.bgCursor(startBtnRow + ' '.repeat(Math.max(0, width - displayWidth(startBtnRow) - 3)))
|
|
@@ -616,7 +617,7 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
616
617
|
lines.push('')
|
|
617
618
|
lines.push(` ${separator}`)
|
|
618
619
|
const probeMode = stats?.probeMode || 'balanced'
|
|
619
|
-
lines.push(` ${themeColors.hotkey('↑↓')} ${themeColors.dim('
|
|
620
|
+
lines.push(` ${themeColors.hotkey('↑↓')} ${themeColors.dim(t('settings.navigate'))} ${themeColors.dim('•')} ${themeColors.hotkey('T')} ${themeColors.dim(t('router.v2.testViaRouter'))} ${themeColors.dim('•')} ${themeColors.hotkey('S')} ${themeColors.dim(isStopped ? t('router.start') : t('router.stop'))} ${themeColors.dim('•')} ${themeColors.hotkey('I')} ${themeColors.dim(`${t('router.v2.probes')}: ${probeMode}`)} ${themeColors.dim('•')} ${themeColors.hotkey('C')} ${themeColors.dim(t('router.clearHistory'))} ${themeColors.dim('•')} ${themeColors.hotkey('Esc')} ${themeColors.dim(t('common.back'))}`)
|
|
620
621
|
lines.push(` ${themeColors.dim('BETA: the v2 engine now powers the main router daemon. Ctrl+T tests the selected table model, Ctrl+Shift+T tests all visible models through the router.')}`)
|
|
621
622
|
|
|
622
623
|
const { visible, offset } = sliceOverlayLines(lines, state.routerV2ScrollOffset || 0, state.terminalRows || 24)
|
|
@@ -628,5 +629,5 @@ export function renderRouterV2Dashboard(state, deps = {}) {
|
|
|
628
629
|
function compactTextV2(value, width) {
|
|
629
630
|
const text = String(value ?? '')
|
|
630
631
|
if (displayWidth(text) <= width) return themeColors.dim(text)
|
|
631
|
-
return themeColors.dim(
|
|
632
|
+
return themeColors.dim(truncateAnsiWidth(text, width, { ellipsis: '…' }))
|
|
632
633
|
}
|
package/src/core/shell-env.js
CHANGED
|
@@ -41,6 +41,7 @@ import { join } from 'node:path'
|
|
|
41
41
|
import * as readline from 'node:readline'
|
|
42
42
|
import chalk from 'chalk'
|
|
43
43
|
import { ENV_VAR_NAMES } from './provider-metadata.js'
|
|
44
|
+
import { isPureEnvPlaceholder } from './utils.js'
|
|
44
45
|
import { saveConfig } from './config.js'
|
|
45
46
|
|
|
46
47
|
// 📖 Unique marker used to identify the source line we inject into shell rc files.
|
|
@@ -126,6 +127,11 @@ export function buildEnvContent(config, shell) {
|
|
|
126
127
|
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue
|
|
127
128
|
if (!value || typeof value !== 'string') continue
|
|
128
129
|
|
|
130
|
+
// 📖 Issue #196: a config value of "${MY_KEY}" means the key comes from the
|
|
131
|
+
// 📖 user's own environment. Never export the literal placeholder into the
|
|
132
|
+
// 📖 rc/env file, it would clobber the real variable on every shell start.
|
|
133
|
+
if (isPureEnvPlaceholder(value)) continue
|
|
134
|
+
|
|
129
135
|
const safeValue = value.replace(/'/g, "'\\''")
|
|
130
136
|
|
|
131
137
|
if (isFish) {
|
package/src/core/utils.js
CHANGED
|
@@ -1168,3 +1168,25 @@ export function formatResultsAsJSON(results, sortBy = 'avg', limit = 0) {
|
|
|
1168
1168
|
|
|
1169
1169
|
return JSON.stringify(formatted, null, 2)
|
|
1170
1170
|
}
|
|
1171
|
+
|
|
1172
|
+
// 📖 Issue #196: ${VAR} placeholder support inside config apiKeys values.
|
|
1173
|
+
// 📖 The config file on disk keeps the literal placeholder, so saveConfig never
|
|
1174
|
+
// 📖 writes the resolved secret back; expansion happens only in memory, at the
|
|
1175
|
+
// 📖 moment a consumer reads the key (getApiKey, tool env sync). Only the
|
|
1176
|
+
// 📖 explicit ${NAME} form is recognized, so a literal key containing a bare
|
|
1177
|
+
// 📖 "$" is never mangled. An unset variable leaves the placeholder untouched:
|
|
1178
|
+
// 📖 probes will fail auth, which is the honest signal that something is off.
|
|
1179
|
+
export function resolveEnvPlaceholderValue(value) {
|
|
1180
|
+
if (typeof value !== 'string') return value
|
|
1181
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (placeholder, name) => {
|
|
1182
|
+
const resolved = process.env[name]
|
|
1183
|
+
return typeof resolved === 'string' ? resolved : placeholder
|
|
1184
|
+
})
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// 📖 True when the value is nothing but one ${VAR} reference. Used by the
|
|
1188
|
+
// 📖 shell rc/env generator to skip entries the user's own environment already
|
|
1189
|
+
// 📖 provides (exporting the literal placeholder would clobber the real var).
|
|
1190
|
+
export function isPureEnvPlaceholder(value) {
|
|
1191
|
+
return typeof value === 'string' && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value.trim())
|
|
1192
|
+
}
|
package/src/tui/app.js
CHANGED
|
@@ -95,6 +95,7 @@ import { randomUUID } from 'crypto'
|
|
|
95
95
|
import { MODELS, sources } from '../../sources.js'
|
|
96
96
|
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatResultsAsJSON } from '../core/utils.js'
|
|
97
97
|
import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../core/config.js'
|
|
98
|
+
import { setLocale } from '../core/i18n/index.js'
|
|
98
99
|
import { buildMergedModels } from '../core/model-merger.js'
|
|
99
100
|
import { loadOpenCodeConfig, saveOpenCodeConfig } from '../core/opencode-config.js'
|
|
100
101
|
import { usageForRow as _usageForRow } from '../core/usage-reader.js'
|
|
@@ -256,6 +257,7 @@ const LOCAL_VERSION = pkg.version
|
|
|
256
257
|
// 📖 OpenCode helpers are imported from ../src/opencode.js
|
|
257
258
|
|
|
258
259
|
export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
260
|
+
setLocale(config.settings?.language || 'en')
|
|
259
261
|
// 📖 t4: prime the sync extended-benchmark stats so the footer chip is
|
|
260
262
|
// 📖 accurate on the first render (no async, no network — just a sync read).
|
|
261
263
|
ensureBenchStatsLoaded()
|
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
|