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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>Find the fastest free coding model in seconds.</strong><br>
9
- Live latency, stability and verdicts for 256 models from 24 free AI providers, then install the one you pick straight into your favorite coding tool.<br><br>
9
+ Live latency, stability and verdicts for 259 models from 25 free AI providers, then install the one you pick straight into your favorite coding tool.<br><br>
10
10
  <strong>Works with:</strong> OpenCode CLI / Desktop / WebUI, OpenClaw, Crush, Goose, Aider, Kilo CLI, Qwen Code, OpenHands, Amp, Hermes, Continue, Cline, Xcode, Pi, ZCode, ForgeCode, Copilot, jcode, Caveman Code and more.
11
11
  </p>
12
12
 
@@ -24,7 +24,7 @@ free-coding-models
24
24
  ```
25
25
 
26
26
  <p align="center">
27
- <sub>Then create a free account on one of the <a href="#-providers">24 providers</a> to grab an API key.</sub>
27
+ <sub>Then create a free account on one of the <a href="#-providers">25 providers</a> to grab an API key.</sub>
28
28
  </p>
29
29
 
30
30
  <p align="center">
@@ -35,7 +35,7 @@ free-coding-models
35
35
 
36
36
  ## ๐Ÿ’ก Why this tool?
37
37
 
38
- There is a large catalog of free and free-limited coding models (**24 providers / 256 live models**, generated from [`sources.js`](./sources.js)). Which one is fastest *right now*? Which one is actually stable, versus just lucky on the last ping?
38
+ There is a large catalog of free and free-limited coding models (**25 providers / 259 live models**, generated from [`sources.js`](./sources.js)). Which one is fastest *right now*? Which one is actually stable, versus just lucky on the last ping?
39
39
 
40
40
  `free-coding-models` (FCM) answers that by pinging every model in parallel, showing live latency, and computing a **live Stability Score (0-100)** combining p95 latency, jitter, spike rate and uptime. Average latency alone is misleading: a model that randomly spikes to 6 seconds is not reliable.
41
41
 
@@ -87,17 +87,17 @@ free-coding-models --fiable # print the single most reliable model
87
87
 
88
88
  ## ๐ŸŸข Providers
89
89
 
90
- **24 active providers / 256 live models**, sorted by live model count. Top 8:
90
+ **25 active providers / 259 live models**, sorted by live model count. Top 8:
91
91
 
92
92
  | Provider | Models | Best tier | Env var |
93
93
  |----------|--------|-----------|---------|
94
94
  | [Alibaba DashScope](https://modelstudio.console.alibabacloud.com) | 29 | S+ | `DASHSCOPE_API_KEY` |
95
- | [Pollinations AI](https://enter.pollinations.ai) | 23 | S+ | `POLLINATIONS_API_KEY` |
95
+ | [Kilo](https://kilo.ai) | 21 | S+ | `KILO_API_KEY` |
96
96
  | [OpenRouter](https://openrouter.ai/keys) | 21 | S+ | `OPENROUTER_API_KEY` |
97
- | [Kilo](https://kilo.ai) | 20 | S+ | `KILO_API_KEY` |
97
+ | [Pollinations AI](https://enter.pollinations.ai) | 21 | S+ | `POLLINATIONS_API_KEY` |
98
98
  | [Ollama Cloud](https://ollama.com/settings/keys) | 20 | S+ | `OLLAMA_API_KEY` |
99
- | [OVHcloud AI](https://endpoints.ai.cloud.ovh.net) | 17 | S+ | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
100
- | [Cloudflare AI](https://dash.cloudflare.com) | 15 | S+ | `CLOUDFLARE_API_TOKEN` |
99
+ | [Cloudflare AI](https://dash.cloudflare.com) | 15 | S | `CLOUDFLARE_API_TOKEN` |
100
+ | [OVHcloud AI](https://endpoints.ai.cloud.ovh.net) | 13 | S+ | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
101
101
  | [NVIDIA NIM](https://build.nvidia.com) | 12 | S+ | `NVIDIA_API_KEY` |
102
102
 
103
103
  > ๐Ÿงพ **What "free" means here:** free is a property of the *(provider, model)* pair, never of the provider as a whole. A row is listed only when that exact model id costs $0 to call through that provider (permanent free tier, `:free` variant, or free plan), verified live at audit time. The same open-weights model can be free on one host and paid on another - paid siblings are deliberately excluded. Full breakdown and badge legend: [`docs/providers.md`](./docs/providers.md).
@@ -129,6 +129,10 @@ The TUI is the heart of FCM: a live, sortable table of every model with real lat
129
129
 
130
130
  Mouse support: click headers to sort, double-click a row to launch, right-click to favorite, scroll to navigate.
131
131
 
132
+ ### Languages
133
+
134
+ The TUI and Web Dashboard support English (default) and Simplified Chinese (`zh-CN`). Change **Language** in Settings; the preference is shared and saved in your local config.
135
+
132
136
  > ๐Ÿ“– **Full key table, mouse reference and workflows:** [`docs/tui.md`](./docs/tui.md) ยท Stability score and columns: [`docs/stability.md`](./docs/stability.md)
133
137
 
134
138
  ---
@@ -32,6 +32,7 @@ if (_configDirIdx !== -1 && process.argv[_configDirIdx + 1] && !process.argv[_co
32
32
  import chalk from 'chalk';
33
33
  import { parseArgs, TIER_LETTER_MAP } from '../src/core/utils.js';
34
34
  import { loadConfig, saveConfig } from '../src/core/config.js';
35
+ import { setLocale } from '../src/core/i18n/index.js';
35
36
  import { ensureTelemetryConfig } from '../src/core/telemetry.js';
36
37
  import { ensureFavoritesConfig } from '../src/core/favorites.js';
37
38
  import { buildCliHelpText } from '../src/tui/cli-help.js';
@@ -69,6 +70,7 @@ async function main() {
69
70
  const cliArgs = parseArgs(process.argv);
70
71
 
71
72
  if (cliArgs.helpMode) {
73
+ setLocale(loadConfig().settings?.language || 'en');
72
74
  console.log();
73
75
  console.log(buildCliHelpText({ chalk, title: 'free-coding-models' }));
74
76
  console.log();
@@ -0,0 +1,22 @@
1
+ # Changelog v0.5.96 - 2026-09-26
2
+
3
+ Community release: 3 issues fixed, 5 pull requests merged (4 new contributors), 1 declined with reason. The catalog moves to **25 providers / 259 models** with the addition of onomeo and 2 Mistral coding models, minus 6 Pollinations models that silently left the free tier.
4
+
5
+ ### Added
6
+
7
+ - **๐Ÿ‡จ๐Ÿ‡ณ Simplified Chinese localization** (PR #193 by @Mavlan): the TUI and the Web Dashboard now share one i18n layer, English stays the default and `zh-CN` is one setting away. The language preference is persisted in the shared config and exposed through the web Settings API. CLI help, settings, router, install, search and command-palette text are all localized, and CJK display-width handling is reused for TUI columns and overlay clipping.
8
+ - **onomeo provider** (PR #187 by @Owaid0723666, who also disclosed operating the service): 9 free coding models behind a daily check-in credit system (50k credits on day 1, up to 200k/day with a streak, no card). Wired end to end: env var `ONOMEO_API_KEY`, key test against `GET /v1/models`, quota badge `limited`, theme colors, Docker env passthrough and provider card on the website. Model ids verified live against the public catalog; premium models deliberately not listed.
9
+ - **Mistral coding model aliases** (PR #192 by @MrTodone): `mistral-code-latest` and `mistral-code-fim-latest` join the Mistral provider (both live-verified aliases of codestral-2508, 256k context), handy for clients that address the `mistral-code-*` family directly.
10
+
11
+ ### Fixed
12
+
13
+ - **Daemon dashboard "[object Object]" toast on every open** (issue #189): the daemon's embedded dashboard only stubbed `GET /api/tool-mode`, so the frontend's POST (URL write-back of `?toolMode=`) fell through to the OpenAI-style 404 catch-all. The daemon now handles GET and POST like `web/server.js`: GET returns the persisted `settings.preferredToolMode` instead of a hardcoded `opencode`, POST validates and persists the mode, and the frontend hook extracts nested `error.message` so any future failure reads as a sentence, not `[object Object]`. Covered by 4 new integration tests.
14
+ - **"Unrecognized flag: --model" when launching OpenCode v2** (issue #194): OpenCode v2 rejects flags its entry points don't know, and `opencode web` never accepted `--model` at all. FCM already wrote your pick into `opencode.json` (`config.model`) before every launch, so the flag was redundant: all launch paths (CLI, Web, Desktop, snap/flatpak) now spawn bare and let the config default apply. Works identically on OpenCode v1 and v2.
15
+ - **6 Pollinations models that quietly became paid** (issue #190, reported by @holakoyad): `minimax-m2.7`, `qwen/qwen3.8-max`, `google/gemini-3.1-pro-preview`, `nvidia/nemotron-3-ultra`, `qwen/qwen3-coder-next` and `gemma-4-31b` answer HTTP 200 with the error text embedded in the completion content on free keys, so status-code probes saw them as healthy. All 6 are removed (free alternatives remain on their original providers), and probes now sniff pollinations completions for the paid-Pollen message and report `402` instead of a fake `200`. Covered by 4 new unit tests.
16
+ - **Router v2 failover settings wiped on restart** (PR #191 by @MrTodone): `lastResortModel`, `bodyReadTimeoutMs`, `totalBudgetMs` and `contentValidation` were silently stripped by the config normalizer on every `load() โ†’ save()` round-trip. Unknown failover keys now survive normalization (the daemon still sanitizes them at read time), with round-trip regression tests.
17
+ - **Web Settings API key deletion could resurrect itself** (PR #195 by @ming076): `/api/settings` saved keys with a plain `saveConfig()` whose on-disk merge could restore a just-deleted key. Key add/update/delete now goes through the provider-scoped `persistApiKeysForProvider()` helper, and persistence failures return a real error instead of a fake success.
18
+
19
+ ### Changed
20
+
21
+ - Provider counts: **24 โ†’ 25 providers, 254 โ†’ 259 models** (minus 6 paid Pollinations, plus 9 onomeo, plus 2 Mistral aliases). `docs/providers.md` regenerated from `sources.js`, README counts and Top-8 table refreshed, website catalog mirror synced.
22
+ - Declined the chalk 5 โ†’ 6 dependabot bump (PR #183): chalk 6 requires Node 22 while FCM supports Node 18+ and CI tests Node 20. It will be revisited when the minimum Node version moves to 22.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.5.95",
3
+ "version": "0.5.96",
4
4
  "description": "Find the fastest coding LLM models in seconds โ€” ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",
package/sources.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * ๐Ÿ“– Secondary: https://swe-rebench.com (independent evals, scores are lower)
30
30
  * ๐Ÿ“– Leaderboard tracker: https://www.marc0.dev/en/leaderboard
31
31
  *
32
- * @exports nvidiaNim, groq, cerebras, sambanova, openrouter, githubModels, mistral, codestral, scaleway, googleai, zai, qwen, cloudflare, ovhcloud, opencodeZen, kilo, llm7, routeway, novita, ollamaCloud, pollinations, siliconflow, requesty, orcarouter, vercelGateway โ€” model arrays per active provider
32
+ * @exports nvidiaNim, groq, cerebras, sambanova, openrouter, githubModels, mistral, codestral, scaleway, googleai, zai, qwen, cloudflare, ovhcloud, opencodeZen, kilo, llm7, routeway, novita, ollamaCloud, pollinations, siliconflow, requesty, orcarouter, vercelGateway, onomeo โ€” model arrays per active provider
33
33
  * @exports sources โ€” map of active free/free-limited providers, each with { name, url, models }
34
34
 
35
35
  * @exports MODELS โ€” flat array of [modelId, label, tier, sweScore, ctx, providerKey]
@@ -227,6 +227,9 @@ export const mistral = [
227
227
  ['labs-leanstral-1-5', 'Leanstral 1.5', 'B', '-', '256k'], // Added (2026-09-22) โ€” free-listed on Mistral LP; targets Lean 4 theorem proving, niche coding use
228
228
  ['ministral-3b-2512', 'Ministral 3 3B', 'B', '-', '128k'], // Fixed (2026-09-16): ministral-3-3b-25-12 โ†’ ministral-3b-2512; ctx 256k โ†’ 128k (max_context_length 131072)
229
229
  // Removed (2026-09-16): mistral-small-creative-25-12 (Mistral Small Creative) โ€” absent from the live catalog
230
+ // โ”€โ”€ Coding models (codestral-2508 aliases, live-verified 2026-09-22 via api.mistral.ai/v1/models) โ”€โ”€
231
+ ['mistral-code-latest', 'Mistral Code', 'A', '-', '256k'], // Added (2026-09-22): aliases codestral-2508 / codestral-latest; ctx 256000
232
+ ['mistral-code-fim-latest', 'Mistral Code FIM', 'A', '-', '256k'], // Added (2026-09-22): FIM alias of codestral-2508; ctx 256000
230
233
  ]
231
234
 
232
235
  // ๐Ÿ“– Mistral Codestral source - https://codestral.mistral.ai
@@ -553,21 +556,25 @@ export const novita = [
553
556
  // ๐Ÿ“– Note (2026-09-21): the anonymous tier (text.pollinations.ai/models) now lists ONLY openai-fast;
554
557
  // ๐Ÿ“– the models below need the free API key + Pollen credits (gen.pollinations.ai). Re-check the Pollen
555
558
  // ๐Ÿ“– free-grant policy at next audit: if grants stop covering these models, this list must shrink to openai-fast.
559
+ // ๐Ÿ“– Note (2026-09-26, issue #190): a subset of models now requires PAID Pollen even with a free key โ€” they
560
+ // ๐Ÿ“– return HTTP 200 with the error embedded in the completion content ("not enough credits / needs paid
561
+ // ๐Ÿ“– Pollen"), so status-code-only probes see them as healthy. 6 such models were removed; when auditing
562
+ // ๐Ÿ“– Pollinations, sniff the completion content for that message, not just the HTTP status.
556
563
  export const pollinations = [
557
564
  // โ”€โ”€ S+ tier โ€” SWE-bench Verified โ‰ฅ70% โ”€โ”€
558
565
  ['laguna', 'Laguna S 2.1', 'S+', '-', '1M'], // Fixed (2026-09-21): alias now resolves to poolside/laguna-s-2.1 (Laguna S 2.1), was Laguna XS.2; score cleared (S 2.1 has no published SWE-bench Verified)
559
- ['minimax-m2.7', 'MiniMax M2.7', 'S+', '78.0%', '200k'],
566
+ // Removed (2026-09-26): minimax-m2.7 (MiniMax M2.7) โ€” now requires paid Pollen: HTTP 200 with an embedded "not enough credits, this model needs paid Pollen" error on free-tier keys (issue #190); replacement: minimax (MiniMax M3, still free via Pollen grants)
560
567
  ['glm-5.3', 'Z.ai GLM-5.3', 'S+', '-', '1M'],
561
568
  ['kimi', 'Moonshot Kimi K2.6', 'S+', '80.2%', '262k'],
562
569
  ['minimax', 'MiniMax M3', 'S+', '80.5%', '524k'],
563
570
  ['moonshotai/kimi-k3', 'Moonshot Kimi K3', 'S+', '76.8%', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models; score follows the Kimi K3 entry on NVIDIA
564
571
  ['deepseek/deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
565
- ['qwen/qwen3.8-max', 'Qwen3.8 Max', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
566
- ['google/gemini-3.1-pro-preview', 'Gemini 3.1 Pro Preview', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id (paid-only on Google AI Studio but free here) // โš ๏ธ status 'down' on live /v1/models 2026-09-22, re-verify next audit
572
+ // Removed (2026-09-26): qwen/qwen3.8-max (Qwen3.8 Max) โ€” requires paid Pollen (embedded credits error on free-tier keys, issue #190); still free on DashScope
573
+ // Removed (2026-09-26): google/gemini-3.1-pro-preview (Gemini 3.1 Pro Preview) โ€” requires paid Pollen (embedded credits error on free-tier keys, issue #190); no free Gemini Pro tier anywhere
567
574
  ['openai/gpt-5.5', 'OpenAI GPT-5.5', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
568
575
  ['openai/gpt-6-astra', 'OpenAI GPT-6 Astra', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
569
576
  ['z-ai/glm-5.3-flash', 'Z.ai GLM-5.3 Flash', 'S+', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
570
- ['nvidia/nemotron-3-ultra', 'NVIDIA Nemotron 3 Ultra', 'S+', '71.9%', '262k'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models; score/scale from the NVIDIA entry
577
+ // Removed (2026-09-26): nvidia/nemotron-3-ultra (NVIDIA Nemotron 3 Ultra) โ€” requires paid Pollen (embedded credits error on free-tier keys, issue #190); still free on NVIDIA NIM
571
578
  ['anthropic/claude-opus-5', 'Claude Opus 5', 'S+', '-', '1M'], // Added (2026-09-22) โ€” healthy on live /v1/models, free via Pollen credits
572
579
  // โ”€โ”€ S tier โ€” SWE-bench Verified 60โ€“70% โ”€โ”€
573
580
  ['anthropic/claude-sonnet-5', 'Claude Sonnet 5', 'S', '-', '1M'], // Added (2026-09-22) โ€” healthy on live /v1/models, free via Pollen credits
@@ -575,12 +582,12 @@ export const pollinations = [
575
582
  ['deepseek', 'DeepSeek V4 Flash', 'S+', '79.0%', '1M'], // Fixed (2026-09-21): alias now resolves to deepseek/deepseek-v4-flash (V4 Flash 0731), was V3; re-scored per the V4 Flash family entry
576
583
  ['kimi-code', 'Kimi K2.7 Code', 'S', '60.4%', '262k'], // Fixed (2026-09-21): alias now resolves to moonshotai/kimi-k2.7-code, was K2 Code
577
584
  ['openai', 'OpenAI GPT-5.4 Nano', 'B+', '-', '400k'], // Fixed (2026-09-21): alias now resolves to openai/gpt-5.4-nano (was a generic GPT alias); re-tiered to the nano class
578
- ['qwen/qwen3-coder-next', 'Qwen3 Coder Next', 'S+', '70.6%', '262k'], // Added (2026-09-21) โ€” canonical id (new on the network, health still warming up); score from the DashScope entry
585
+ // Removed (2026-09-26): qwen/qwen3-coder-next (Qwen3 Coder Next) โ€” requires paid Pollen (embedded credits error on free-tier keys, issue #190); still free on DashScope
579
586
  ['openai/gpt-5.6-luna', 'OpenAI GPT-5.6 Luna', 'S', '-', '1M'], // Added (2026-09-21) โ€” canonical id, healthy on live /v1/models
580
587
  // โ”€โ”€ A+ tier โ€” SWE-bench Verified 50โ€“60% โ”€โ”€
581
588
  ['deepseek/deepseek-v4.1-flash', 'DeepSeek V4.1 Flash', 'A+', '-', '1M'], // Added (2026-09-22) โ€” healthy on live /v1/models, successor to V4 Flash
582
589
  ['meituan/longcat-2.0', 'LongCat 2.0', 'A', '-', '1M'], // Added (2026-09-22) โ€” healthy on live /v1/models, new MoE agentic/coding model on the network
583
- ['gemma-4-31b', 'Gemma 4 31B', 'A+', '52.0%', '262k'],
590
+ // Removed (2026-09-26): gemma-4-31b (Gemma 4 31B) โ€” requires paid Pollen (embedded credits error on free-tier keys, issue #190); the free Gemma route is NVIDIA NIM
584
591
  ['gpt-oss', 'GPT OSS 20B', 'A+', '50.3%', '131k'],
585
592
  ['qwen3.7-flash', 'Qwen3.7 Flash', 'A+', '-', '1M'],
586
593
  // โ”€โ”€ B+ tier โ”€โ”€
@@ -706,6 +713,35 @@ export const ollamaCloud = [
706
713
  ['nemotron-3-nano:30b', 'Nemotron 3 Nano 30B', 'A-', '38.8%', '1M'],
707
714
  ]
708
715
 
716
+ // ๐Ÿ“– onomeo source - https://onomeo.com/docs
717
+ // ๐Ÿ“– OpenAI-compatible gateway: https://onomeo.com/v1/chat/completions (streaming + tool calls)
718
+ // ๐Ÿ“– Public beta: not every feature is guaranteed to work; feedback is welcome at https://onomeo.com/feedback.
719
+ // ๐Ÿ“– Free credits come from a daily check-in, no card: 50,000 on day 1, rising to 200,000/day from
720
+ // ๐Ÿ“– day 7 of a streak (a missed day resets it). Every call spends credits and the cost per reply
721
+ // ๐Ÿ“– varies by model (https://onomeo.com/models), so this is a small allowance.
722
+ // ๐Ÿ“– Premium models are not listed; each account that has not paid can spend up to 50,000 credits/day on them.
723
+ // ๐Ÿ“– Optional: $5 one-time buys 1,000,000 credits. Limits: 12 req/min per key, 60 req per 5 hours
724
+ // ๐Ÿ“– per account, 120 per 5 hours per IP, and a site-wide pool of 450 per 5 hours for accounts
725
+ // ๐Ÿ“– that have not bought credits.
726
+ // ๐Ÿ“– Some upstreams may train on prompts; each model page (https://onomeo.com/models/<id>) says which.
727
+ // ๐Ÿ“– Ids and ctx checked 2026-09-26 against the public https://onomeo.com/api/info
728
+ // ๐Ÿ“– (`models`, `modelFacts`); /v1/models needs a key.
729
+ export const onomeo = [
730
+ // โ”€โ”€ S+ tier โ€” SWE-bench Verified โ‰ฅ70% โ”€โ”€
731
+ ['deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '79.0%', '1M'],
732
+ ['glm-5.2', 'GLM 5.2', 'S+', '82.8%', '1M'],
733
+ // โ”€โ”€ S tier โ€” SWE-bench Verified 60โ€“70% โ”€โ”€
734
+ ['gemini-3.1-flash-lite', 'Gemini 3.1 Flash Lite', 'S', '62.8%', '1M'],
735
+ ['@cf/openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '62.4%', '128k'],
736
+ ['nvidia/nemotron-3-super-120b-a12b', 'Nemotron 3 Super', 'S', '60.5%', '262k'],
737
+ // โ”€โ”€ A+ tier โ€” SWE-bench Verified 50โ€“60% โ”€โ”€
738
+ ['openai/gpt-oss-20b', 'GPT OSS 20B', 'A+', '50.3%', '128k'],
739
+ ['z-ai/glm-5.3-flash-free', 'GLM-5.3 Flash', 'A+', '-', '1M'],
740
+ ['stepfun/step-3.7-flash:free', 'Step 3.7 Flash', 'A+', '-', '256k'],
741
+ // โ”€โ”€ A tier โ€” SWE-bench Verified 40โ€“50% โ”€โ”€
742
+ ['codestral-latest', 'Codestral Latest', 'A', '40.0%', '256k'],
743
+ ]
744
+
709
745
  // ๐Ÿ“– All sources combined - used by the main script
710
746
  // ๐Ÿ“– Each source has: name (display), url (API endpoint), models (array of model tuples)
711
747
  // ๐Ÿ“– Providers ordered by generosity of free tier (most generous first)
@@ -898,6 +934,13 @@ export const sources = {
898
934
  quotaCode: 'free',
899
935
  models: ollamaCloud,
900
936
  },
937
+ onomeo: {
938
+ name: 'onomeo',
939
+ url: 'https://onomeo.com/v1/chat/completions',
940
+ quota: 'Daily check-in credits ยท 12 RPM ยท 60 req/5h',
941
+ quotaCode: 'limited',
942
+ models: onomeo,
943
+ },
901
944
  }
902
945
 
903
946
  // ๐Ÿ“– Flatten all models from all sources โ€” each entry includes providerKey as 6th element
@@ -105,6 +105,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
105
105
  import { homedir } from 'node:os'
106
106
  import { join, resolve, dirname } from 'node:path'
107
107
  import { syncShellEnv } from './shell-env.js'
108
+ import { DEFAULT_LOCALE, normalizeLocale } from './i18n/index.js'
108
109
 
109
110
  // ๐Ÿ“– Expand leading ~/ or bare ~ to homedir so --config-dir ~/.config/... works.
110
111
  // ๐Ÿ“– resolve() alone does NOT expand tilde, it would treat ~ as a literal folder.
@@ -188,6 +189,7 @@ const ENV_VARS = {
188
189
  requesty: 'REQUESTY_API_KEY',
189
190
  orcarouter: 'ORCAROUTER_API_KEY',
190
191
  'vercel-gateway': ['VERCEL_AI_GATEWAY_API_KEY', 'AI_GATEWAY_API_KEY'],
192
+ onomeo: 'ONOMEO_API_KEY',
191
193
  }
192
194
 
193
195
  // ๐Ÿ“– Smart Router defaults are intentionally conservative: balanced probing,
@@ -331,6 +333,7 @@ function normalizeSettingsSection(settings) {
331
333
  delete safeSettings.footerHidden
332
334
  return {
333
335
  ...safeSettings,
336
+ language: normalizeLocale(safeSettings.language),
334
337
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
335
338
  favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
336
339
  runAiSpeedTestOnStartup: typeof safeSettings.runAiSpeedTestOnStartup === 'boolean' ? safeSettings.runAiSpeedTestOnStartup : false,
@@ -456,10 +459,22 @@ function normalizeRouterFailover(failover, routerFallback = {}) {
456
459
  const rawStreamStall = numberOrNull(safeFailover.streamStallTimeoutMs) ?? numberOrNull(routerFallback.streamStallTimeoutMs)
457
460
  const rawRequestTimeout = numberOrNull(safeFailover.requestTimeoutMs) ?? numberOrNull(routerFallback.requestTimeoutMs)
458
461
 
462
+ // ๐Ÿ“– router-v2 documented failover fields (lastResortModel,
463
+ // bodyReadTimeoutMs, totalBudgetMs, contentValidation โ€” docs/router-v2.md)
464
+ // are read by the daemon at runtime but are not v1 knobs normalized here.
465
+ // Carry any unknown keys through so they survive load()/save() round-trips
466
+ // instead of being silently dropped from the on-disk config.
467
+ const v1FailoverKeys = new Set(['maxRetries', 'streamStallTimeoutMs', 'requestTimeoutMs'])
468
+ const extras = {}
469
+ for (const key of Object.keys(safeFailover)) {
470
+ if (!v1FailoverKeys.has(key)) extras[key] = safeFailover[key]
471
+ }
472
+
459
473
  return {
460
474
  maxRetries: normalizePositiveInteger(rawMaxRetries, DEFAULT_ROUTER_SETTINGS.failover.maxRetries, { min: 1, max: 20 }),
461
475
  streamStallTimeoutMs: normalizePositiveInteger(rawStreamStall, DEFAULT_ROUTER_SETTINGS.failover.streamStallTimeoutMs, { min: 1000, max: 120000 }),
462
476
  requestTimeoutMs: normalizePositiveInteger(rawRequestTimeout, DEFAULT_ROUTER_SETTINGS.failover.requestTimeoutMs, { min: 1000, max: 300000 }),
477
+ ...extras,
463
478
  }
464
479
  }
465
480
 
@@ -1295,6 +1310,7 @@ export function isProviderEnabled(config, providerKey) {
1295
1310
  */
1296
1311
  export function _emptyProfileSettings() {
1297
1312
  return {
1313
+ language: DEFAULT_LOCALE,
1298
1314
  tierFilter: null, // ๐Ÿ“– null = show all tiers, or 'S'|'A'|'B'|'C'|'D'
1299
1315
  sortColumn: 'avg', // ๐Ÿ“– default sort column
1300
1316
  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
+ }