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.
@@ -23,8 +23,14 @@
23
23
  * "scaleway": "scw-xxx",
24
24
  * "googleai": "AIza...",
25
25
  * "cloudflare": "cf-xxx",
26
- * "zai": "zai-xxx"
26
+ * "zai": "zai-xxx",
27
+ * "onomeo": "${ONOMEO_API_KEY}"
27
28
  * },
29
+ *
30
+ * 📖 ${VAR} values (issue #196): an apiKeys entry of the form "${MY_VAR}" is
31
+ * 📖 resolved from the environment at read time. The file on disk keeps the
32
+ * 📖 placeholder so secrets never land on disk; saveConfig never writes the
33
+ * 📖 resolved value back. See getApiKey() and resolveEnvPlaceholderValue().
28
34
  * "providers": {
29
35
  * "nvidia": { "enabled": true },
30
36
  * "groq": { "enabled": true },
@@ -105,6 +111,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
105
111
  import { homedir } from 'node:os'
106
112
  import { join, resolve, dirname } from 'node:path'
107
113
  import { syncShellEnv } from './shell-env.js'
114
+ import { DEFAULT_LOCALE, normalizeLocale } from './i18n/index.js'
115
+ import { resolveEnvPlaceholderValue } from './utils.js'
108
116
 
109
117
  // 📖 Expand leading ~/ or bare ~ to homedir so --config-dir ~/.config/... works.
110
118
  // 📖 resolve() alone does NOT expand tilde, it would treat ~ as a literal folder.
@@ -188,6 +196,7 @@ const ENV_VARS = {
188
196
  requesty: 'REQUESTY_API_KEY',
189
197
  orcarouter: 'ORCAROUTER_API_KEY',
190
198
  'vercel-gateway': ['VERCEL_AI_GATEWAY_API_KEY', 'AI_GATEWAY_API_KEY'],
199
+ onomeo: 'ONOMEO_API_KEY',
191
200
  }
192
201
 
193
202
  // 📖 Smart Router defaults are intentionally conservative: balanced probing,
@@ -331,6 +340,7 @@ function normalizeSettingsSection(settings) {
331
340
  delete safeSettings.footerHidden
332
341
  return {
333
342
  ...safeSettings,
343
+ language: normalizeLocale(safeSettings.language),
334
344
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
335
345
  favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
336
346
  runAiSpeedTestOnStartup: typeof safeSettings.runAiSpeedTestOnStartup === 'boolean' ? safeSettings.runAiSpeedTestOnStartup : false,
@@ -456,10 +466,22 @@ function normalizeRouterFailover(failover, routerFallback = {}) {
456
466
  const rawStreamStall = numberOrNull(safeFailover.streamStallTimeoutMs) ?? numberOrNull(routerFallback.streamStallTimeoutMs)
457
467
  const rawRequestTimeout = numberOrNull(safeFailover.requestTimeoutMs) ?? numberOrNull(routerFallback.requestTimeoutMs)
458
468
 
469
+ // 📖 router-v2 documented failover fields (lastResortModel,
470
+ // bodyReadTimeoutMs, totalBudgetMs, contentValidation — docs/router-v2.md)
471
+ // are read by the daemon at runtime but are not v1 knobs normalized here.
472
+ // Carry any unknown keys through so they survive load()/save() round-trips
473
+ // instead of being silently dropped from the on-disk config.
474
+ const v1FailoverKeys = new Set(['maxRetries', 'streamStallTimeoutMs', 'requestTimeoutMs'])
475
+ const extras = {}
476
+ for (const key of Object.keys(safeFailover)) {
477
+ if (!v1FailoverKeys.has(key)) extras[key] = safeFailover[key]
478
+ }
479
+
459
480
  return {
460
481
  maxRetries: normalizePositiveInteger(rawMaxRetries, DEFAULT_ROUTER_SETTINGS.failover.maxRetries, { min: 1, max: 20 }),
461
482
  streamStallTimeoutMs: normalizePositiveInteger(rawStreamStall, DEFAULT_ROUTER_SETTINGS.failover.streamStallTimeoutMs, { min: 1000, max: 120000 }),
462
483
  requestTimeoutMs: normalizePositiveInteger(rawRequestTimeout, DEFAULT_ROUTER_SETTINGS.failover.requestTimeoutMs, { min: 1000, max: 300000 }),
484
+ ...extras,
463
485
  }
464
486
  }
465
487
 
@@ -1142,12 +1164,14 @@ export function getApiKey(config, providerKey) {
1142
1164
  // 📖 but every consumer (ping Authorization header, tool env vars, daemon) needs
1143
1165
  // 📖 a single string: an array would stringify to "key1,key2" and get a
1144
1166
  // 📖 guaranteed 401. Return the first non-empty entry instead.
1167
+ // 📖 ${VAR} references (issue #196) are expanded here so the file on disk
1168
+ // 📖 can stay secret-free while every consumer gets the real value.
1145
1169
  const key = config?.apiKeys?.[providerKey]
1146
1170
  if (Array.isArray(key)) {
1147
1171
  const firstUsable = key.find(k => typeof k === 'string' && k.length > 0)
1148
- return firstUsable ?? null
1172
+ return firstUsable !== undefined ? resolveEnvPlaceholderValue(firstUsable) : null
1149
1173
  }
1150
- if (key) return key
1174
+ if (key) return resolveEnvPlaceholderValue(key)
1151
1175
 
1152
1176
  return null
1153
1177
  }
@@ -1295,6 +1319,7 @@ export function isProviderEnabled(config, providerKey) {
1295
1319
  */
1296
1320
  export function _emptyProfileSettings() {
1297
1321
  return {
1322
+ language: DEFAULT_LOCALE,
1298
1323
  tierFilter: null, // 📖 null = show all tiers, or 'S'|'A'|'B'|'C'|'D'
1299
1324
  sortColumn: 'avg', // 📖 default sort column
1300
1325
  sortAsc: true, // 📖 true = ascending (fastest first for latency)
@@ -0,0 +1,49 @@
1
+ import en from './locales/en.js'
2
+ import zhCN from './locales/zh-CN.js'
3
+
4
+ export const DEFAULT_LOCALE = 'en'
5
+ export const SUPPORTED_LOCALES = Object.freeze(['en', 'zh-CN'])
6
+ export const LOCALE_OPTIONS = Object.freeze([
7
+ Object.freeze({ value: 'en', labelKey: 'common.language.english' }),
8
+ Object.freeze({ value: 'zh-CN', labelKey: 'common.language.simplifiedChinese' }),
9
+ ])
10
+
11
+ const dictionaries = Object.freeze({ en, 'zh-CN': zhCN })
12
+ let activeLocale = DEFAULT_LOCALE
13
+
14
+ export function normalizeLocale(locale) {
15
+ return SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE
16
+ }
17
+
18
+ export function getLocale() {
19
+ return activeLocale
20
+ }
21
+
22
+ export function setLocale(locale) {
23
+ activeLocale = normalizeLocale(locale)
24
+ return activeLocale
25
+ }
26
+
27
+ export function createTranslator(resources = dictionaries) {
28
+ return (locale, key, values = {}) => {
29
+ if (typeof key !== 'string') return ''
30
+ const active = normalizeLocale(locale)
31
+ const localized = resources[active]?.[key]
32
+ const fallback = resources[DEFAULT_LOCALE]?.[key]
33
+ const template = typeof localized === 'string' ? localized : typeof fallback === 'string' ? fallback : key
34
+ return template.replace(/\{([\w.]+)\}/g, (match, name) => {
35
+ const value = values && Object.prototype.hasOwnProperty.call(values, name) ? values[name] : undefined
36
+ return value === null || value === undefined ? match : String(value)
37
+ })
38
+ }
39
+ }
40
+
41
+ const translate = createTranslator()
42
+
43
+ export function t(key, values = {}) {
44
+ return translate(activeLocale, key, values)
45
+ }
46
+
47
+ export function hasTranslation(locale, key) {
48
+ return typeof dictionaries[normalizeLocale(locale)]?.[key] === 'string'
49
+ }