free-coding-models 0.5.49 → 0.5.51

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
@@ -539,7 +539,7 @@ Cerebras free-tier API has a strict **~8k total token limit** (prompt + tools +
539
539
 
540
540
  ### Full documentation
541
541
 
542
- See [`pi-extension/README.md`](./pi-extension/README.md) for the complete architecture diagram, config format, and troubleshooting guide.
542
+ The adapter lives in [`packages/fcm-pi`](./packages/fcm-pi); `pi-extension/` is kept as a thin compat wrapper so existing local-path installs keep loading. See [`packages/fcm-pi/README.md`](./packages/fcm-pi/README.md) for the complete architecture, config format, and the one-time `packages/` self-link setup.
543
543
 
544
544
  ---
545
545
 
@@ -551,10 +551,12 @@ See [`pi-extension/README.md`](./pi-extension/README.md) for the complete archit
551
551
 
552
552
  ```bash
553
553
  mkdir -p ~/.config/opencode/plugins
554
- ln -sf /Users/vava/Documents/GitHub/free-coding-models/opencode-plugin/index.js \
554
+ ln -sf /Users/vava/Documents/GitHub/free-coding-models/packages/fcm-opencode/index.js \
555
555
  ~/.config/opencode/plugins/fcm-opencode.js
556
556
  ```
557
557
 
558
+ (`opencode-plugin/` at the repo root is kept as a thin compat wrapper, so existing symlinks keep working.)
559
+
558
560
  ### Commands
559
561
 
560
562
  | Command | Description |
@@ -568,7 +570,31 @@ ln -sf /Users/vava/Documents/GitHub/free-coding-models/opencode-plugin/index.js
568
570
 
569
571
  Startup is intentionally light: fresh cache first, daemon second, **no direct scan** unless you run `/fcm`.
570
572
 
571
- See [`opencode-plugin/README.md`](./opencode-plugin/README.md) for details and limitations.
573
+ See [`packages/fcm-opencode/README.md`](./packages/fcm-opencode/README.md) for details and limitations.
574
+
575
+ ---
576
+
577
+ ## 🧩 Agent extensions architecture
578
+
579
+ Both adapters share one core so scan/rank/cache/daemon/API-key/provider logic lives in exactly one place:
580
+
581
+ ```
582
+ packages/
583
+ ├── fcm-agent-core/ ← shared core (scan, rank, cache, daemon, keys, provider descriptors; no rendering)
584
+ ├── fcm-pi/ ← Pi adapter (hooks, commands, status-bar renderer, ~/.pi/agent disk writer)
585
+ └── fcm-opencode/ ← OpenCode adapter (config mutation, commands, toasts, shell.env)
586
+ ```
587
+
588
+ - The core emits **structured progress events**; each adapter renders them its own way (Pi status bar, OpenCode toast).
589
+ - API keys are never inlined into OpenCode config — they are referenced via `{env:FCM_<PROVIDER>_API_KEY}`.
590
+ - The cross-tool cache means a scan done in Pi benefits OpenCode (and vice-versa).
591
+
592
+ > The `packages/` tree needs a one-time self-link so `free-coding-models` resolves by name during local-path use:
593
+ > ```bash
594
+ > cd packages && mkdir -p node_modules && ln -s ../../ node_modules/free-coding-models
595
+ > ```
596
+
597
+ See [`packages/fcm-agent-core/README.md`](./packages/fcm-agent-core/README.md) for the public API and rationale. Tracked in [`.kandown/tasks/t2.md`](./.kandown/tasks/t2.md).
572
598
 
573
599
  ---
574
600
 
@@ -783,6 +809,7 @@ Telemetry is enabled by default and can be disabled with any of the following:
783
809
  <td align="center" width="120"><a href="https://github.com/serajbaltu"><img src="https://avatars.githubusercontent.com/u/90699173?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="serajbaltu"></a></td>
784
810
  <td align="center" width="120"><a href="https://github.com/stgreenb"><img src="https://avatars.githubusercontent.com/u/18483964?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="stgreenb"></a></td>
785
811
  <td align="center" width="120"><a href="https://github.com/MoriDanWork"><img src="https://avatars.githubusercontent.com/u/55363096?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="MoriDanWork"></a></td>
812
+ <td align="center" width="120"><a href="https://github.com/fan92rus"><img src="https://avatars.githubusercontent.com/u/13201333?v=4&s=80" width="80" height="80" style="border-radius:50%" alt="fan92rus"></a></td>
786
813
  </tr>
787
814
  <tr>
788
815
  <td align="center"><a href="https://github.com/vava-nessa"><sub><b>vava-nessa</b></sub></a></td>
@@ -794,6 +821,10 @@ Telemetry is enabled by default and can be disabled with any of the following:
794
821
  <td align="center"><a href="https://github.com/serajbaltu"><sub><b>serajbaltu</b></sub></a></td>
795
822
  <td align="center"><a href="https://github.com/stgreenb"><sub><b>stgreenb</b></sub></a></td>
796
823
  <td align="center"><a href="https://github.com/MoriDanWork"><sub><b>MoriDanWork</b></sub></a></td>
824
+ <td align="center"><a href="https://github.com/fan92rus"><sub><b>fan92rus</b></sub></a></td>
825
+ </tr>
826
+ <tr>
827
+ <td align="center" colspan="10"><sub>🛡️ <b>fan92rus</b> — Windows path traversal fix (<code>path.sep</code>)</sub></td>
797
828
  </tr>
798
829
  </table>
799
830
 
@@ -0,0 +1,25 @@
1
+ # Changelog v0.5.48 - 2026-07-15
2
+
3
+ ### Added
4
+ - **Quota column across TUI and Web Dashboard** — users can now see remaining free quota directly in the model table when a provider exposes reliable telemetry. The column shows a percentage for monitorable providers, `N/A` for providers where quota cannot be monitored safely, and `-` when the provider is not configured.
5
+ - **Groq, Cerebras, and OpenRouter quota monitoring** — Groq and Cerebras are parsed from official rate-limit response headers, while OpenRouter continues to use its official key endpoint. Cerebras support includes both daily request headers and token-per-minute headers.
6
+ - **Shared `fcm-agent-core` package** (`packages/fcm-agent-core/`) — all scan, ranking, cache, daemon, API-key, and provider-descriptor logic now lives in one reusable core consumed by both the Pi and OpenCode adapters. The core emits *structured* progress events (`{ phase, percent, completed, total, activeModels, message }`) and contains **no rendering and no chalk** — each adapter renders its own way. Public API: `scanBestFcmModel`, `directScan`, `rankModels`, `createCacheStore`, `buildPiProviderDescriptor`, `buildOpenCodeProviderDescriptor`, `buildSmartRouterDescriptor`, and more.
7
+ - **Canonical `fcm-pi` adapter** (`packages/fcm-pi/`) and **canonical `fcm-opencode` adapter** (`packages/fcm-opencode/`) — the Pi and OpenCode host adapters, now containing only host-specific concerns (Pi status-bar renderer + `~/.pi/agent` disk writer; OpenCode config mutation + toasts + `shell.env`).
8
+ - **21 new unit tests** for the core (`test/fcm-agent-core.test.js`): SWE parsing, composite scoring + ranking, context-window parsing and the 16k agent safety floor, base-URL normalization, provider-ID prefixing, the Pi/OpenCode/router descriptor shapes, a **no-API-key-leak assertion** for OpenCode (keys are referenced via `{env:FCM_*_API_KEY}`), and the namespaced cache store (round-trip, TTL expiry, legacy fallback, context re-filtering).
9
+ - **Quota parser tests** for Groq and Cerebras header variants, covering request/day and token/minute quota shapes.
10
+
11
+ ### Changed
12
+ - **Three-package layout under `packages/`.** The Pi adapter, OpenCode adapter, and shared core are now cleanly separated. Provider config shapes that were previously duplicated (the Pi provider shape existed both in runtime registration and in disk writes) are now centralized in `buildPiProviderDescriptor` / `buildOpenCodeProviderDescriptor`.
13
+ - **Cross-tool cache.** Cache is now a shared `createCacheStore({ filePath, legacyPaths, ttlMs })`; the OpenCode adapter falls back to the Pi cache location, so a scan done in one tool benefits the other.
14
+ - **Rendering decoupled from scanning.** The direct scanner no longer imports chalk or builds ANSI. The branded `> free-coding-models` Pi footer badge (exact TUI header green/white-on-black) now lives in the Pi renderer (`packages/fcm-pi/lib/pi-progress-renderer.js`), where it belongs.
15
+ - **`pi-extension/` and `opencode-plugin/` are now thin compat wrappers** that re-export from `packages/fcm-pi` and `packages/fcm-opencode` respectively, so existing local-path/symlink installs keep loading unchanged. The moved `lib/` code was deleted (now in the core + adapters).
16
+ - README updated with the new **Agent extensions architecture** section and corrected package links.
17
+ - `pnpm test` now runs `test/fcm-agent-core.test.js` alongside `test/test.js`.
18
+ - Provider quota capability metadata now only marks providers as percent-monitorable when they have verified telemetry, preventing misleading quota values for NVIDIA, OpenCode Zen, Routeway, and other unknown providers.
19
+
20
+ ### Fixed
21
+ - Eliminates the provider-shape duplication flagged in `.kandown/tasks/t2.md`: Pi runtime registration and `~/.pi/agent/models.json` disk writes now share one descriptor builder, so they can never drift apart.
22
+
23
+ ### Notes
24
+ - The adapters remain **BETA** and are still installed via local path / symlink (not via the npm `free-coding-models` package). Local use needs a one-time self-link: `cd packages && mkdir -p node_modules && ln -s ../../ node_modules/free-coding-models` (documented in `packages/fcm-agent-core/README.md`; same trick the legacy `pi-extension/` already used).
25
+ - Task `t2` (*« Mutualiser FCM-Pi et créer fcm-opencode »*) is now **Done** — all acceptance criteria and the definition of done are met. Deliberately deferred to v1.1: npm publishing of the sub-packages, OpenCode custom `tool` definitions (v1 uses command hooks), TypeScript build for `fcm-opencode` (kept JS + JSDoc), and unifying `src/core/endpoint-installer.js` onto the shared descriptor builders.
@@ -0,0 +1,21 @@
1
+ # Changelog v0.5.50 - 2026-07-18
2
+
3
+ ### Changed
4
+
5
+ - **Full model catalog audit** — verified all 20 providers against their official docs and APIs (2026-07-18)
6
+ - **Auto-deprecation support** — models with a `deprecatedAfter` date in the tuple are now automatically filtered from `MODELS` after that date, without requiring a code change
7
+ - **Cerebras** — `zai-glm-4.7` marked as `⚠️ DEPRECATED shutdown 2026-08-17` per official Cerebras notice; will disappear automatically after that date
8
+
9
+ ### Fixed
10
+
11
+ - **NVIDIA NIM** — removed `magistral-small-2506` (not found on NIM) and `qwen3-coder-480b` (deprecated free endpoint); fixed context windows: `mistral-medium-3.5-128b` 128k→256k, `nemotron-3-nano-omni` 128k→256k, `mistral-nemotron` -→128k; added `deepseek-r1` at A tier
12
+ - **Codestral** — fixed `codestral-2508` context window 128k→256k
13
+ - **OpenRouter** — removed `gpt-oss-120b:free` and `gpt-oss-20b:free` (no longer available on free tier)
14
+ - **Ollama Cloud** — full catalog refresh: 19 outdated models removed, 10 new models added (kimi-k2.6, kimi-k2.7-code, kimi-k2.5, deepseek-v4-flash, deepseek-v4-pro, glm-5.1, glm-5.2, minimax-m2.7, and more); catalog now verified against official ollama.com/search?c=cloud
15
+ - **Novita AI** — removed stale models (qwen3.6-plus, nex-agi/nex-n2-pro, minimax/m2-her); added tencent/hy3
16
+ - **LLM7** — removed devstral-small-2:24b; added minimax-m2.7 (S+) and gpt-oss:20b (A+)
17
+ - **OpenCode Zen** — added hy3-free (Tencent Hy3 Free)
18
+
19
+ ### Confirmed (no changes)
20
+
21
+ - groq, googleai, github-models, mistral, cloudflare, sambanova, ovhcloud, zai, scaleway, qwen, kilo, routeway — all models verified current
@@ -0,0 +1,15 @@
1
+ # Changelog v0.5.51 - 2026-07-25
2
+
3
+ ### Fixed
4
+ - 🛡️ **Windows path traversal protection** — `serveWebStaticFile()` in `src/core/router-daemon.js` now uses `path.sep` instead of a hardcoded `/` for the path traversal guard. On Windows, `path.resolve()` returns backslash separators, which made the previous `candidate.startsWith(WEB_DIST_DIR + '/')` check always fail and serve `path_traversal_blocked` for every root request. On Linux/macOS, `path.sep === '/'` so behaviour is unchanged. *(Thanks @fan92rus! First contribution 🎉)*
5
+
6
+ ### Changed
7
+ - 📦 **Deps:** bump `@tanstack/react-virtual` from `3.14.3` → `3.14.5`
8
+ - 📦 **DevDeps:** bump `@vitejs/plugin-react` from `6.0.2` → `6.0.3`
9
+
10
+ ### Maintenance
11
+ - 🧹 **kandown housekeeping:** drop stale `AGENT.md` / `AGENT_KANDOWN.md` copies, repair install, refresh agent ref
12
+ - 🧹 **fcm-agent fixes:** `direct-scanner.js`, `ranker.js`, `scan-orchestrator.js`, `pi-progress-renderer.js`, and `extensions/index.js` refinements (312+/46-)
13
+
14
+ ### Contributors
15
+ - **@fan92rus** — Windows path traversal fix (`path.sep`). Welcome aboard! 🛡️🎉
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.5.49",
3
+ "version": "0.5.51",
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
@@ -51,16 +51,15 @@ export const nvidiaNim = [
51
51
  // ── S tier — SWE-bench Verified 60–70% ──
52
52
  ['openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '62.4%', '128k'],
53
53
  ['meta/llama-4-maverick-17b-128e-instruct', 'Llama 4 Maverick', 'S+', '74.8%', '1M'],
54
- ['mistralai/mistral-medium-3.5-128b', 'Mistral Medium 3.5', 'S+', '77.6%', '128k'],
54
+ ['mistralai/mistral-medium-3.5-128b', 'Mistral Medium 3.5', 'S+', '77.6%', '256k'],
55
55
  ['mistralai/mistral-small-4-119b-2603', 'Mistral Small 4', 'S', '60.0%', '256k'],
56
56
  ['minimaxai/minimax-m3', 'MiniMax M3', 'S+', '78.4%', '1M'],
57
- ['qwen/qwen3-coder-480b-a35b-instruct', 'Qwen3 Coder 480B', 'S', '69.6%', '262k'],
58
- ['nvidia/mistral-nemotron', 'Mistral Nemotron', 'S', '-', '-'],
57
+ ['nvidia/mistral-nemotron', 'Mistral Nemotron', 'S', '-', '128k'],
59
58
  ['deepseek-ai/deepseek-v3.2', 'DeepSeek V3.2', 'S+', '70.0%', '160k'],
60
59
  // ── A+ tier — SWE-bench Verified 50–60% ──
61
60
  ['mistralai/mistral-large-3-675b-instruct-2512', 'Mistral Large 675B', 'A+', '58.0%', '256k'],
62
61
  ['nvidia/nemotron-3-super-120b-a12b', 'Nemotron 3 Super', 'S', '60.5%', '128k'],
63
- ['nvidia/nemotron-3-nano-omni-30b-a3b-reasoning', 'Nemotron 3 Omni', 'A+', '52.0%', '128k'],
62
+ ['nvidia/nemotron-3-nano-omni-30b-a3b-reasoning', 'Nemotron 3 Omni', 'A+', '52.0%', '256k'],
64
63
  ['meta-llama/llama-4-scout-17b-16e-instruct', 'Llama 4 Scout', 'B', '28.0%', '10M'],
65
64
  ['nvidia/llama-3.3-nemotron-super-49b-v1.5', 'Llama 3.3 Nemotron Super 49B v1.5', 'A+', '-', '128k'],
66
65
  // ── A tier — SWE-bench Verified 40–50% ──
@@ -68,7 +67,7 @@ export const nvidiaNim = [
68
67
  ['openai/gpt-oss-20b', 'GPT OSS 20B', 'A+', '50.3%', '128k'],
69
68
  ['google/gemma-4-31b-it', 'Gemma 4 31B', 'A+', '52.0%', '256k'],
70
69
  ['qwen/qwen2.5-coder-32b-instruct', 'Qwen2.5 Coder 32B', 'A', '47.0%', '128k'],
71
- ['mistralai/magistral-small-2506', 'Magistral Small 2506', 'A', '45.0%', '128k'],
70
+ ['deepseek-ai/deepseek-r1', 'DeepSeek R1', 'A', '49.2%', '128k'],
72
71
  ['nvidia/nemotron-3-nano', 'Nemotron 3 Nano', 'A-', '38.8%', '256k'],
73
72
  // ── A- tier — SWE-bench Verified 35–40% ──
74
73
  ['bytedance/seed-oss-36b-instruct', 'Seed OSS 36B', 'A+', '56.0%', '32k'],
@@ -97,7 +96,8 @@ export const groq = [
97
96
  // 📖 Free API keys available at https://cloud.cerebras.ai
98
97
  export const cerebras = [
99
98
  // ── S+ tier — SWE-bench Verified ≥70% ──
100
- ['zai-glm-4.7', 'GLM 4.7', 'S+', '73.8%', '128k'],
99
+ // ⚠️ DEPRECATED — shutdown 2026-08-17 (Cerebras official notice)
100
+ ['zai-glm-4.7', 'GLM 4.7', 'S+', '73.8%', '128k', null, '2026-08-17'],
101
101
  // ── S tier — SWE-bench Verified 60–70% ──
102
102
  ['gpt-oss-120b', 'GPT OSS 120B', 'S', '62.4%', '128k'],
103
103
  // ── A tier — SWE-bench Verified 40–50% ──
@@ -138,7 +138,6 @@ export const openrouter = [
138
138
  ['poolside/laguna-xs.2:free', 'Poolside Laguna XS.2', 'S', '68.2%', '262k'],
139
139
  ['poolside/laguna-xs-2.1:free', 'Poolside Laguna XS 2.1', 'S+', '70.9%', '262k'],
140
140
  // ── S tier — SWE-bench Verified 60–70% ──
141
- ['openai/gpt-oss-120b:free', 'GPT OSS 120B', 'S', '62.4%', '131k'],
142
141
  ['cohere/north-mini-code:free', 'North Mini Code', 'S', '-', '256k'],
143
142
  ['tencent/hy3:free', 'Tencent Hy3', 'S', '-', '262k'],
144
143
  ['qwen/qwen3-coder:free', 'Qwen3 Coder', 'S', '69.6%', '1M'],
@@ -146,7 +145,6 @@ export const openrouter = [
146
145
  ['nvidia/nemotron-3-super-120b-a12b:free', 'Nemotron 3 Super', 'S', '60.5%', '1M'],
147
146
  ['nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', 'Nemotron 3 Omni', 'A+', '52.0%', '256k'],
148
147
  // ── A tier — SWE-bench Verified 40–50% ──
149
- ['openai/gpt-oss-20b:free', 'GPT OSS 20B', 'A+', '50.3%', '131k'],
150
148
  ['nvidia/nemotron-3-nano-30b-a3b:free', 'Nemotron Nano 30B', 'A-', '38.8%', '256k'],
151
149
  ['nvidia/nemotron-nano-12b-v2-vl:free', 'Nemotron Nano 12B VL', 'A', '20.0%', '128k'],
152
150
  ['google/gemma-4-31b-it:free', 'Gemma 4 31B', 'A+', '52.0%', '262k'],
@@ -235,8 +233,8 @@ export const mistral = [
235
233
  // 📖 Free coding model — 30 req/min, 2000/day (phone number required for key)
236
234
  // 📖 API keys now use the Mistral platform key format; CODESTRAL_API_KEY remains supported as an alias.
237
235
  export const codestral = [
238
- // ── B+ tier — SWE-bench Verified 3035% ──
239
- ['codestral-2508', 'Codestral', 'A', '40.0%', '128k'],
236
+ // ── A tier — SWE-bench Verified 4050% ──
237
+ ['codestral-2508', 'Codestral', 'A', '40.0%', '256k'],
240
238
  ['codestral-2', 'Codestral 2', 'B+', '-', '128k'],
241
239
  ]
242
240
 
@@ -387,8 +385,9 @@ export const opencodeZen = [
387
385
  ['big-pickle', 'Big Pickle', 'S+', '72.0%', '200k'],
388
386
  ['deepseek-v4-flash-free', 'DeepSeek V4 Flash Free', 'S+', '79.0%', '200k'],
389
387
  ['mimo-v2.5-free', 'MiMo-V2.5 Free', 'S+', '-', '200k'],
390
- ['nemotron-3-ultra-free', 'Nemotron 3 Ultra Free', 'S+', '71.9%', '200k'],
388
+ ['nemotron-3-ultra-free', 'Nemotron 3 Ultra Free', 'S+', '71.9%', '200k'],
391
389
  ['north-mini-code-free', 'North Mini Code Free', 'B+', '-', '200k'],
390
+ ['hy3-free', 'Tencent Hy3 Free', 'S', '-', '200k'],
392
391
  ]
393
392
 
394
393
  // 📖 Kilo source - https://api.kilo.ai/api/gateway
@@ -403,8 +402,10 @@ export const kilo = [
403
402
  // 📖 Pro-tagged models from /v1/models are intentionally excluded.
404
403
  export const llm7 = [
405
404
  // 📖 LLM7 live /v1/models: only `turbo` tier is free (noKeyNeeded). All `pro` models are usage-based paid.
406
- // ── S tier — SWE-bench Verified 60–70% ──
407
- ['devstral-small-2:24b', 'Devstral Small 2', 'S', '68.0%', '255k'],
405
+ // ── S+ tier — SWE-bench Verified 70% ──
406
+ ['minimax-m2.7', 'MiniMax M2.7', 'S+', '78.0%', '180k'],
407
+ // ── A+ tier — SWE-bench Verified 50–60% ──
408
+ ['gpt-oss:20b', 'GPT OSS 20B', 'A+', '50.3%', '128k'],
408
409
  // ── A tier — SWE-bench Verified 40–50% ──
409
410
  ['codestral-latest', 'Codestral Latest', 'A', '40.0%', '32k'],
410
411
  ]
@@ -440,49 +441,34 @@ export const routeway = [
440
441
  // 📖 Test/dev/placeholder zero-price IDs were intentionally excluded.
441
442
  export const novita = [
442
443
  // ── S+ tier — SWE-bench Verified ≥70% ──
443
- ['qwen/qwen3.6-plus', 'Qwen3.6 Plus', 'S+', '78.8%', '1M'],
444
- // ── S tier — SWE-bench Verified 60–70% ──
445
444
  ['qwen/qwen3.5-plus', 'Qwen3.5 Plus', 'S+', '80.0%', '1M'],
446
- ['nex-agi/nex-n2-pro', 'Nex N2 Pro', 'S', '-', '262k'],
447
- ['minimax/m2-her', 'MiniMax M2 HER', 'S', '-', '66k'],
445
+ // ── S tier SWE-bench Verified 60–70% ──
446
+ ['tencent/hy3', 'Tencent Hy3', 'S', '-', '262k'],
448
447
  ]
449
448
 
450
- // 📖 Ollama Cloud source - https://ollama.com/pricing and https://ollama.com/v1/models
449
+ // 📖 Ollama Cloud source - https://ollama.com/pricing and https://ollama.com/search?c=cloud
451
450
  // 📖 Free plan includes cloud model access with session/weekly limits. This list keeps coding-relevant cloud models only.
451
+ // 📖 Catalog verified 2026-07-18 against official Ollama cloud model search page.
452
452
  export const ollamaCloud = [
453
453
  // ── S+ tier — SWE-bench Verified ≥70% ──
454
- ['devstral-2:123b', 'Devstral 2 123B', 'S+', '72.2%', '256k'],
455
- ['qwen3-coder:480b', 'Qwen3 Coder 480B', 'S', '69.6%', '256k'],
456
454
  ['nemotron-3-ultra', 'Nemotron 3 Ultra', 'S+', '71.9%', '256k'],
457
- ['glm-4.7', 'GLM 4.7', 'S+', '73.8%', '200k'],
455
+ ['glm-5.1', 'GLM 5.1', 'S+', '82.8%', '128k'],
456
+ ['glm-5.2', 'GLM 5.2', 'S+', '82.8%', '128k'],
457
+ ['minimax-m2.7', 'MiniMax M2.7', 'S+', '78.0%', '200k'],
458
+ ['minimax-m3', 'MiniMax M3', 'S+', '78.4%', '512k'],
459
+ ['minimax-m2.5', 'MiniMax M2.5', 'S+', '80.2%', '200k'],
460
+ ['kimi-k2.6', 'Kimi K2.6', 'S+', '80.2%', '262k'],
461
+ ['deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '79.0%', '256k'],
462
+ ['deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '80.6%', '256k'],
458
463
  // ── S tier — SWE-bench Verified 60–70% ──
459
- ['qwen3-coder-next', 'Qwen3 Coder Next', 'S+', '70.6%', '256k'],
464
+ ['kimi-k2.7-code', 'Kimi K2.7 Code', 'S', '60.4%', '262k'],
460
465
  ['gpt-oss:120b', 'GPT OSS 120B', 'S', '62.4%', '128k'],
461
- ['minimax-m3', 'MiniMax M3', 'S+', '78.4%', '512k'],
462
- // ── A+ tier — SWE-bench Verified 50–60% ──
463
466
  ['nemotron-3-super', 'Nemotron 3 Super', 'S', '60.5%', '256k'],
464
- ['cogito-2.1:671b', 'Cogito 2.1 671B', 'A+', '-', '160k'],
465
- // ── A tier — SWE-bench Verified 40–50% ──
467
+ // ── A+ tier — SWE-bench Verified 50–60% ──
468
+ ['kimi-k2.5', 'Kimi K2.5', 'A+', '-', '200k'],
466
469
  ['gemma4:31b', 'Gemma 4 31B', 'A+', '52.0%', '256k'],
467
470
  ['gpt-oss:20b', 'GPT OSS 20B', 'A+', '50.3%', '128k'],
468
- ['glm-4.6', 'GLM 4.6', 'A', '-', '200k'],
469
- ['qwen3-next:80b', 'Qwen3 Next 80B', 'S+', '70.6%', '256k'],
470
- ['qwen3-vl:235b', 'Qwen3 VL 235B', 'A', '-', '256k'],
471
- ['qwen3-vl:235b-instruct', 'Qwen3 VL 235B Instruct', 'A', '-', '256k'],
472
- // ── A- tier — SWE-bench Verified 35–40% ──
473
- ['devstral-small-2:24b', 'Devstral Small 2 24B', 'S', '68.0%', '256k'],
474
- ['gemma3:27b', 'Gemma 3 27B', 'B', '22.0%', '128k'],
475
- ['minimax-m2', 'MiniMax M2', 'S', '69.4%', '200k'],
476
- ['minimax-m2.1', 'MiniMax M2.1', 'S+', '74.0%', '200k'],
477
- ['minimax-m2.5', 'MiniMax M2.5', 'S+', '80.2%', '200k'],
478
- ['ministral-3:8b', 'Ministral 3 8B', 'A-', '-', '256k'],
479
- ['ministral-3:14b', 'Ministral 3 14B', 'A-', '-', '256k'],
480
471
  ['nemotron-3-nano:30b', 'Nemotron 3 Nano 30B', 'A-', '38.8%', '1M'],
481
- // ── B+ tier — SWE-bench Verified 30–35% ──
482
- ['gemma3:4b', 'Gemma 3 4B', 'B+', '-', '128k'],
483
- ['gemma3:12b', 'Gemma 3 12B', 'B+', '-', '128k'],
484
- ['ministral-3:3b', 'Ministral 3 3B', 'B+', '-', '256k'],
485
- ['rnj-1:8b', 'RNJ 1 8B', 'B+', '-', '32k'],
486
472
  ]
487
473
 
488
474
  // 📖 All sources combined - used by the main script
@@ -598,11 +584,14 @@ export const sources = {
598
584
 
599
585
  // 📖 Flatten all models from all sources — each entry includes providerKey as 6th element
600
586
  // 📖 providerKey lets the main CLI know which API key and URL to use per model
587
+ // 📖 Models with a deprecatedAfter date (7th tuple element) are auto-filtered after that date
601
588
  export const MODELS = [];
589
+ const _today = new Date().toISOString().split('T')[0];
602
590
  for (const [sourceKey, sourceData] of Object.entries(sources)) {
603
591
  if (!sourceData || !sourceData.models) continue
604
592
  for (const model of sourceData.models) {
605
- const [modelId, label, tier, sweScore, ctx, addedDate] = model
593
+ const [modelId, label, tier, sweScore, ctx, addedDate, deprecatedAfter] = model
594
+ if (deprecatedAfter && _today > deprecatedAfter) continue
606
595
  MODELS.push([modelId, label, tier, sweScore, ctx, sourceKey, addedDate || null])
607
596
  }
608
597
  }
package/src/core/ping.js CHANGED
@@ -205,6 +205,9 @@ export function extractQuotaPercent(headers) {
205
205
  const variants = [
206
206
  ['x-ratelimit-remaining', 'x-ratelimit-limit'],
207
207
  ['x-ratelimit-remaining-requests', 'x-ratelimit-limit-requests'],
208
+ ['x-ratelimit-remaining-requests-day', 'x-ratelimit-limit-requests-day'],
209
+ ['x-ratelimit-remaining-tokens', 'x-ratelimit-limit-tokens'],
210
+ ['x-ratelimit-remaining-tokens-minute', 'x-ratelimit-limit-tokens-minute'],
208
211
  ['ratelimit-remaining', 'ratelimit-limit'],
209
212
  ['ratelimit-remaining-requests', 'ratelimit-limit-requests'],
210
213
  ]
@@ -34,18 +34,20 @@
34
34
 
35
35
  /** @type {Record<string, ProviderCapability>} */
36
36
  export const PROVIDER_CAPABILITIES = {
37
- // Providers that return x-ratelimit-remaining / x-ratelimit-limit headers
38
- nvidia: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
37
+ // Providers with verified quota telemetry from official docs or live API behavior.
38
+ nvidia: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
39
39
  groq: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
40
- cerebras: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
41
- sambanova: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
42
- 'github-models': { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
43
- mistral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'monthly' },
44
- scaleway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
45
- googleai: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
46
- codestral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
47
- qwen: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
48
- ovhcloud: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
40
+ cerebras: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
41
+
42
+ // Providers not yet proven to expose reliable remaining-quota telemetry.
43
+ sambanova: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
44
+ 'github-models': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
45
+ mistral: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'monthly' },
46
+ scaleway: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
47
+ googleai: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
48
+ codestral: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
49
+ qwen: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
50
+ ovhcloud: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
49
51
 
50
52
  // Providers that have a dedicated usage/credits endpoint
51
53
  openrouter: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'percent', resetCadence: 'unknown' },
@@ -56,7 +58,7 @@ export const PROVIDER_CAPABILITIES = {
56
58
  'opencode-zen': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
57
59
  kilo: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
58
60
  llm7: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'rolling' },
59
- routeway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
61
+ routeway: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
60
62
  novita: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
61
63
  'ollama-cloud': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'rolling' },
62
64
  }
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { createServer } from 'node:http'
33
33
  import { existsSync, readFileSync } from 'node:fs'
34
- import { dirname, join, resolve as resolvePath } from 'node:path'
34
+ import { dirname, join, resolve as resolvePath, sep as pathSep } from 'node:path'
35
35
  import { fork } from 'node:child_process'
36
36
  import { randomUUID } from 'node:crypto'
37
37
  import { appendFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
@@ -434,7 +434,7 @@ function serveWebStaticFile(res, pathname, requestId) {
434
434
  // 📖 Without this, `pathname` like `/../../etc/passwd` escapes the dist root.
435
435
  const requested = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '')
436
436
  const candidate = resolvePath(WEB_DIST_DIR, requested)
437
- if (candidate !== WEB_DIST_DIR && !candidate.startsWith(WEB_DIST_DIR + '/')) {
437
+ if (candidate !== WEB_DIST_DIR && !candidate.startsWith(WEB_DIST_DIR + pathSep)) {
438
438
  sendError(res, 403, 'Forbidden', 'invalid_request_error', 'path_traversal_blocked', requestId)
439
439
  return
440
440
  }
@@ -454,7 +454,7 @@ function serveWebStaticFile(res, pathname, requestId) {
454
454
 
455
455
  if (stats.isDirectory()) {
456
456
  const dirIndex = resolvePath(candidate, 'index.html')
457
- if (dirIndex.startsWith(WEB_DIST_DIR + '/') && existsSync(dirIndex)) {
457
+ if (dirIndex.startsWith(WEB_DIST_DIR + pathSep) && existsSync(dirIndex)) {
458
458
  serveStaticFromDist(res, dirIndex)
459
459
  return
460
460
  }
package/src/tui/app.js CHANGED
@@ -313,6 +313,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
313
313
  status: 'pending',
314
314
  pings: [], // 📖 All ping results (ms or 'TIMEOUT')
315
315
  httpCode: null,
316
+ hasApiKey: !!getApiKey(config, providerKey),
316
317
  isPinging: false, // 📖 Per-row live flag so Last Ping can keep last value and show a spinner during refresh.
317
318
  hidden: false, // 📖 Simple flag to hide/show models
318
319
  }))
@@ -431,6 +432,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
431
432
 
432
433
  try {
433
434
  const providerApiKey = getApiKey(state.config, r.providerKey) ?? null
435
+ r.hasApiKey = !!providerApiKey
434
436
  const providerUrl = sources[r.providerKey]?.url ?? sources.nvidia.url
435
437
  let { code, ms, quotaPercent } = await ping(providerApiKey, r.modelId, r.providerKey, providerUrl)
436
438
 
@@ -28,7 +28,7 @@
28
28
  * - ../src/constants.js: PING_INTERVAL, FRAMES
29
29
  * - ../src/tier-colors.js: TIER_COLOR
30
30
  * - ../src/utils.js: getAvg, getVerdict, getUptime, getStabilityScore
31
- * - ../src/ping.js: usagePlaceholderForProvider
31
+ * - ../src/quota-capabilities.js: supportsUsagePercent
32
32
  * - ../src/render-helpers.js: calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, fadedRow
33
33
  *
34
34
  * @see bin/free-coding-models.js — main entry point that calls renderTable
@@ -50,7 +50,7 @@ import {
50
50
  import { themeColors, currentPalette, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme, THEME_BG_RGB } from './theme.js'
51
51
  import { TIER_COLOR } from './tier-colors.js'
52
52
  import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo, isNewModel } from '../core/utils.js'
53
- import { usagePlaceholderForProvider } from '../core/ping.js'
53
+ import { supportsUsagePercent } from '../core/quota-capabilities.js'
54
54
  import { formatBenchmarkLatency, formatBenchmarkTps } from '../core/benchmark.js'
55
55
  import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi, fadedRow } from './render-helpers.js'
56
56
  import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from '../core/tool-metadata.js'
@@ -96,6 +96,7 @@ const COLUMN_SORT_MAP = {
96
96
  uptime: 'uptime',
97
97
  aiLatency: 'aiLatency',
98
98
  tps: 'tps',
99
+ quota: 'usage',
99
100
  }
100
101
  export { COLUMN_SORT_MAP }
101
102
 
@@ -284,7 +285,7 @@ export function renderTable({
284
285
  const W_RANK = 6
285
286
  const W_TIER = 5
286
287
  const W_CTX = 4
287
- const W_SOURCE = 14
288
+ const W_SOURCE = 11
288
289
  const W_MODEL = 26
289
290
  const W_SWE = 5
290
291
  const W_STATUS = 17
@@ -292,9 +293,9 @@ export function renderTable({
292
293
  const W_UPTIME = 6
293
294
  const W_AI_LATENCY = 17
294
295
  const W_TPS = 5
296
+ const W_QUOTA = 6
295
297
 
296
298
  // const W_TOKENS = 7 // Used column removed
297
- // const W_USAGE = 7 // Usage column removed
298
299
  const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
299
300
 
300
301
  // 📖 Responsive column visibility: progressively hide least-useful columns
@@ -308,6 +309,7 @@ export function renderTable({
308
309
  let wAvg = 9
309
310
  let wStab = 11
310
311
  let wSource = W_SOURCE
312
+ let wModel = W_MODEL
311
313
  let wStatus = W_STATUS
312
314
  let wAiLatency = W_AI_LATENCY
313
315
  let showRank = true
@@ -324,10 +326,11 @@ export function renderTable({
324
326
  cols.push(W_MOOD)
325
327
  if (showRank) cols.push(W_RANK)
326
328
  if (showTier) cols.push(W_TIER)
327
- cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
329
+ cols.push(W_SWE, W_CTX, wModel, wSource, wPing, wAvg, wStatus, W_VERDICT)
328
330
  if (showStability) cols.push(wStab)
329
331
  if (showUptime) cols.push(W_UPTIME)
330
332
  if (showBenchmarkColumns) cols.push(wAiLatency, W_TPS)
333
+ cols.push(W_QUOTA)
331
334
  return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
332
335
  }
333
336
 
@@ -338,8 +341,9 @@ export function renderTable({
338
341
  wAvg = 9 // 'Avg Ping' stays aligned with Last Ping
339
342
  wStab = 8 // 'StaB.' instead of 'Stability'
340
343
  wSource = 7 // Provider truncated to 4 chars + '…', 7 cols total
344
+ wModel = 21 // Keep compact tables wide enough for Rank + Quota
341
345
  wStatus = 13 // Health truncated after 6 chars + '…'
342
- wAiLatency = 13 // Mirror compact Health text when health is not good
346
+ wAiLatency = 9 // Keep compact tables wide enough for Rank + Quota
343
347
  }
344
348
  // 📖 Steps 2–6: Progressive column hiding (least useful first)
345
349
  if (calcWidth() > terminalCols) showRank = false
@@ -359,7 +363,7 @@ export function renderTable({
359
363
  if (showTier) colDefs.push({ name: 'tier', width: W_TIER })
360
364
  colDefs.push({ name: 'swe', width: W_SWE })
361
365
  colDefs.push({ name: 'ctx', width: W_CTX })
362
- colDefs.push({ name: 'model', width: W_MODEL })
366
+ colDefs.push({ name: 'model', width: wModel })
363
367
  colDefs.push({ name: 'source', width: wSource })
364
368
  colDefs.push({ name: 'ping', width: wPing })
365
369
  colDefs.push({ name: 'avg', width: wAvg })
@@ -371,6 +375,7 @@ export function renderTable({
371
375
  colDefs.push({ name: 'aiLatency', width: wAiLatency })
372
376
  colDefs.push({ name: 'tps', width: W_TPS })
373
377
  }
378
+ colDefs.push({ name: 'quota', width: W_QUOTA })
374
379
  let x = ROW_MARGIN + 1 // 📖 1-based: first column starts after the 2-char left margin
375
380
  const columns = []
376
381
  for (let i = 0; i < colDefs.length; i++) {
@@ -517,7 +522,7 @@ export function renderTable({
517
522
  if (headerFlashColumn === 'tier') return flashHeader(tierLabel, W_TIER)
518
523
  return colorFirst(tierLabel, W_TIER)
519
524
  })()
520
- const modelH_c = headerStyle('model', modelLabel, W_MODEL)
525
+ const modelH_c = headerStyle('model', modelLabel, wModel)
521
526
  const sweH_c = headerStyle('swe', sweLabel, W_SWE)
522
527
  const ctxH_c = headerStyle('ctx', ctxLabel, W_CTX)
523
528
  const pingH_c = headerStyle('ping', pingLabel, wPing)
@@ -580,8 +585,15 @@ export function renderTable({
580
585
  const padding = ' '.repeat(Math.max(0, W_TPS - plain.length))
581
586
  return themeColors.dim(plain + padding)
582
587
  })()
588
+ const quotaH_c = (() => {
589
+ if (headerFlashColumn === 'quota' || headerFlashColumn === 'usage') {
590
+ const ft = (dir + ' Quota').length <= W_QUOTA ? dir + ' Quota' : 'Quota' + dir
591
+ return flashHeader(ft, W_QUOTA)
592
+ }
593
+ if (sortColumn === 'usage') return sortActiveHeader('Quota', W_QUOTA)
594
+ return themeColors.dim('Quota'.padEnd(W_QUOTA))
595
+ })()
583
596
 
584
- // 📖 Usage column removed from UI – no header or separator for it.
585
597
  // 📖 Header row: conditionally include columns based on responsive visibility
586
598
  const headerParts = [moodH_c]
587
599
  if (showRank) headerParts.push(rankH_c)
@@ -590,6 +602,7 @@ export function renderTable({
590
602
  if (showStability) headerParts.push(stabH_c)
591
603
  if (showUptime) headerParts.push(uptimeH_c)
592
604
  if (showBenchmarkColumns) headerParts.push(aiLatencyH_c, tpsH_c)
605
+ headerParts.push(quotaH_c)
593
606
  lines.push(' ' + headerParts.join(COL_SEP))
594
607
 
595
608
  // 📖 Mouse support: the column header row is the last line we just pushed.
@@ -666,7 +679,7 @@ export function renderTable({
666
679
  favoritePrefix = '🆕 '
667
680
  }
668
681
  const prefixDisplayWidth = displayWidth(favoritePrefix)
669
- const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
682
+ const nameWidth = Math.max(0, wModel - prefixDisplayWidth)
670
683
  const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
671
684
  const sweScore = r.sweScore ?? '—'
672
685
  // 📖 SWE% colorized on the same gradient as Tier:
@@ -901,9 +914,23 @@ export function renderTable({
901
914
  // 📖 Check if this model is incompatible with the active tool mode
902
915
  const isIncompatible = !isModelCompatibleWithTool(r.providerKey, mode)
903
916
 
904
- // 📖 Usage column removed from UI no usage data displayed.
905
- // (We keep the logic but do not render it.)
906
- const usageCell = ''
917
+ // 📖 Quota column: provider telemetry if available, N/A when impossible,
918
+ // 📖 and '-' when the provider is not configured.
919
+ let quotaCell
920
+ if (!r.hasApiKey) {
921
+ quotaCell = themeColors.dim('-'.padEnd(W_QUOTA))
922
+ } else if (!supportsUsagePercent(r.providerKey)) {
923
+ quotaCell = themeColors.dim('N/A'.padEnd(W_QUOTA))
924
+ } else if (typeof r.usagePercent === 'number' && Number.isFinite(r.usagePercent)) {
925
+ const quotaText = `${Math.round(r.usagePercent)}%`.padEnd(W_QUOTA)
926
+ quotaCell = r.usagePercent >= 50
927
+ ? themeColors.metricGood(quotaText)
928
+ : r.usagePercent >= 20
929
+ ? themeColors.metricWarn(quotaText)
930
+ : themeColors.metricBad(quotaText)
931
+ } else {
932
+ quotaCell = themeColors.dim('…'.padEnd(W_QUOTA))
933
+ }
907
934
 
908
935
  // 📖 AI Latency + TPS columns — same benchmark result, split into two readable metrics.
909
936
  // 📖 Benchmark results are shown regardless of health status (up/timeout/down/429/noauth).
@@ -960,6 +987,7 @@ export function renderTable({
960
987
  if (showStability) rowParts.push(stabCell)
961
988
  if (showUptime) rowParts.push(uptimeCell)
962
989
  if (showBenchmarkColumns) rowParts.push(latencyCell, tpsCell)
990
+ rowParts.push(quotaCell)
963
991
  const row = ' ' + rowParts.join(COL_SEP)
964
992
 
965
993
  // 📖 "Unusable" models (no key / bad key) are visually de-emphasized at the