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 +34 -3
- package/changelog/v0.5.48.md +25 -0
- package/changelog/v0.5.50.md +21 -0
- package/changelog/v0.5.51.md +15 -0
- package/package.json +1 -1
- package/sources.js +33 -44
- package/src/core/ping.js +3 -0
- package/src/core/quota-capabilities.js +14 -12
- package/src/core/router-daemon.js +3 -3
- package/src/tui/app.js +2 -0
- package/src/tui/render-table.js +41 -13
- package/web/dist/assets/index-BkK1gdJN.css +1 -0
- package/web/dist/assets/index-DhqlYW96.js +40 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +15 -1
- package/web/src/components/dashboard/ModelTable.jsx +21 -1
- package/web/src/components/dashboard/ModelTable.module.css +11 -0
- package/web/src/hooks/useFilter.js +2 -0
- package/web/dist/assets/index-Cv5m3BUX.js +0 -40
- package/web/dist/assets/index-r8niexgJ.css +0 -1
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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%', '
|
|
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
|
-
['
|
|
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%', '
|
|
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
|
-
['
|
|
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
|
-
|
|
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
|
-
// ──
|
|
239
|
-
['codestral-2508', 'Codestral', 'A', '40.0%', '
|
|
236
|
+
// ── A tier — SWE-bench Verified 40–50% ──
|
|
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%',
|
|
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
|
|
407
|
-
['
|
|
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
|
-
|
|
447
|
-
['
|
|
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/
|
|
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-
|
|
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
|
-
['
|
|
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
|
-
|
|
465
|
-
|
|
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
|
|
38
|
-
nvidia: { telemetryType: '
|
|
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: '
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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: '
|
|
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 +
|
|
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
|
|
package/src/tui/render-table.js
CHANGED
|
@@ -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/
|
|
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 {
|
|
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 =
|
|
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,
|
|
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 =
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
// 📖
|
|
905
|
-
//
|
|
906
|
-
|
|
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
|