free-coding-models 0.5.26 → 0.5.28

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
- Track ~146 models across 15 trusted free or free-limited AI providers in real time<br><br>
9
+ Track ~191 models across 20 trusted free or free-limited AI providers in real time<br><br>
10
10
  <strong>Install Free API endpoints to your favorite AI coding tools:</strong><br>
11
11
  OpenCode CLI / Desktop / WebUI, OpenClaw, Crush, Goose, Aider, Kilo CLI, Qwen Code, OpenHands, Amp, Hermes, Continue, Cline, Xcode, Pi and more...<br><br>
12
12
  <strong>Use Kimi K2, DeepSeek V3, GPT-OSS, Qwen3, MiniMax M3, GLM, Llama 4, Gemma 4, Devstral and more — for free</strong>
@@ -16,8 +16,8 @@
16
16
  <img src="https://img.shields.io/npm/v/free-coding-models?color=3d6b00&label=npm&logo=npm" alt="npm version" width="200"><br>
17
17
  <img src="https://img.shields.io/node/v/free-coding-models?color=3d6b00&logo=node.js" alt="node version" width="200"><br>
18
18
  <img src="https://img.shields.io/npm/l/free-coding-models?color=3d6b00" alt="license" width="200"><br>
19
- <img src="https://img.shields.io/badge/models-146-3d6b00?logo=nvidia" alt="models count" width="200"><br>
20
- <img src="https://img.shields.io/badge/providers-15-1a56db" alt="providers count" width="200">
19
+ <img src="https://img.shields.io/badge/models-191-3d6b00?logo=nvidia" alt="models count" width="200"><br>
20
+ <img src="https://img.shields.io/badge/providers-20-1a56db" alt="providers count" width="200">
21
21
  </p>
22
22
 
23
23
  ```bash
@@ -59,7 +59,7 @@ free-coding-models
59
59
 
60
60
  ## 💡 Why this tool?
61
61
 
62
- There are **~153 cataloged free or free-limited coding models** across 16 vetted providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
62
+ There are **~191 cataloged free or free-limited coding models** across 20 vetted providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
63
63
 
64
64
  This CLI pings them all in parallel, shows live latency, and calculates a **live Stability Score (0-100)**. Average latency alone is misleading if a model randomly spikes to 6 seconds; the stability score measures true reliability by combining **p95 latency** (30%), **jitter/variance** (30%), **spike rate** (20%), and **uptime** (20%).
65
65
 
@@ -71,9 +71,9 @@ It then writes the model you pick directly into your coding tool's config — so
71
71
 
72
72
  ### 🟢 List of Free AI Providers
73
73
 
74
- Create a free account on one provider below to get started:
74
+ Create a free account on one provider below to get started. A few providers (`Kilo`, `LLM7`, OVHcloud sandbox) can also answer without a key, with tighter shared limits.
75
75
 
76
- **~153 coding models** across 16 active providers, ranked by practical free-tier usefulness.
76
+ **~191 coding models** across 20 active providers, ranked by practical free-tier usefulness.
77
77
 
78
78
  | # | Provider | Models | Tier range | Free tier | Env var |
79
79
  |---|----------|--------|-----------|-----------|--------|
@@ -83,7 +83,7 @@ Create a free account on one provider below to get started:
83
83
  | 4 | [Google AI Studio](https://aistudio.google.com/apikey) | 7 | S+ → A | Gemini free quotas vary by model/region | `GOOGLE_API_KEY` |
84
84
  | 5 | [GitHub Models](https://models.github.ai) | 15 | S+ → C | Quota depends on GitHub/Copilot tier | `GITHUB_TOKEN` |
85
85
  | 6 | [Mistral La Plateforme](https://console.mistral.ai/api-keys) | 5 | S+ → A | Experiment plan, free evaluation tier | `MISTRAL_API_KEY` |
86
- | 7 | [Cloudflare Workers AI](https://dash.cloudflare.com) | 13 | S+ → B | 10K neurons/day, 300 RPM (no credit card) | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
86
+ | 7 | [Cloudflare Workers AI](https://dash.cloudflare.com) | 16 | S+ → B | 10K neurons/day, 300 RPM (no credit card) | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
87
87
  | 8 | [OpenRouter](https://openrouter.ai/keys) | 24 | S+ → C | 50 req/day free, 1K/day with $10 spend | `OPENROUTER_API_KEY` |
88
88
  | 9 | [SambaNova](https://cloud.sambanova.ai/apis) | 7 | S+ → B+ | Small developer quota, useful for light usage | `SAMBANOVA_API_KEY` |
89
89
  | 10 | [OVHcloud AI Endpoints](https://endpoints.ai.cloud.ovh.net) | 10 | S → B | 2 req/min/IP free, 400 RPM with key | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
@@ -91,7 +91,12 @@ Create a free account on one provider below to get started:
91
91
  | 12 | [ZAI](https://z.ai) | 2 | S | Free Flash models only | `ZAI_API_KEY` |
92
92
  | 13 | [Scaleway](https://console.scaleway.com/iam/api-keys) | 10 | S+ → B | 1M free tokens | `SCALEWAY_API_KEY` |
93
93
  | 14 | [Alibaba DashScope](https://modelstudio.console.alibabacloud.com) | 11 | S+ → A+ | 1M free tokens/model, Singapore, 90 days | `DASHSCOPE_API_KEY` |
94
- | 15 | [OpenCode Zen](https://opencode.ai/zen) | 6 | S+ → A | Free with OpenCode account | Zen models ✨ |
94
+ | 15 | [OpenCode Zen](https://opencode.ai/zen) | 5 | S+ → A | Free with OpenCode account | Zen models ✨ |
95
+ | 16 | [Kilo](https://kilo.ai) | 1 | A+ | Free auto-router works without a key | optional `KILO_API_KEY` |
96
+ | 17 | [LLM7](https://llm7.io) | 4 | S+ → B+ | Shared free tier, optional free token | optional `LLM7_API_KEY` |
97
+ | 18 | [Routeway](https://routeway.ai) | 15 | S+ → C | Explicit `:free` zero-price models | `ROUTEWAY_API_KEY` |
98
+ | 19 | [Novita AI](https://novita.ai) | 4 | S+ → S | Only zero-price live chat models included | `NOVITA_API_KEY` |
99
+ | 20 | [Ollama Cloud](https://ollama.com/pricing) | 17 | S+ → A | Free cloud usage with session/weekly limits | `OLLAMA_API_KEY` |
95
100
 
96
101
  > 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
97
102
 
@@ -513,7 +518,7 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
513
518
 
514
519
  ## ✨ Features
515
520
 
516
- - **Parallel pings** — all ~150 API/Zen-callable models tested simultaneously via native `fetch` (~152 total cataloged models)
521
+ - **Parallel pings** — all ~191 API/Zen-callable models tested simultaneously via native `fetch`
517
522
  - **AI benchmark columns** — `Ctrl+A` benchmarks the selected model, `Ctrl+U` benchmarks visible models, and results split cleanly into **AI Latency** plus **TPS**. Settings includes an opt-in **Startup AI Speed Scan** toggle to run the global benchmark automatically after launch.
518
523
  - **Tiny verdict indicator** — the first `❔` column mirrors the full Verdict as a compact emoji (`🟩`, `🟢`, `🟡`, `🟠`, etc.) and sorts by the same verdict order.
519
524
  - **Adaptive monitoring** — 2s burst for 60s → 10s normal → 30s idle
@@ -0,0 +1,17 @@
1
+ # Changelog v0.5.27 - 2026-06-11
2
+
3
+ ### Added
4
+ - Added five newly audited free/free-limited providers discovered from `apmantza/pi-free`: Kilo, LLM7, Routeway, Novita AI, and Ollama Cloud.
5
+ - Added 41 coding-capable API models from those providers, keeping only no-key/free-router models, explicit `:free` zero-price Routeway models, zero-price Novita chat models, and coding-relevant Ollama Cloud models.
6
+ - Added provider metadata, environment-variable support, quota behavior, Settings key-test endpoints, no-key provider handling, and TUI colors for the new providers.
7
+ - Added 💰 billing/credits warnings in TUI Settings, first-run setup, and Web Settings for providers that require paid credits or trial-credit-style accounts despite exposing free/free-tagged models.
8
+
9
+ ### Changed
10
+ - Updated README model/provider counts to 191 models across 20 providers and documented the new providers in the main provider table.
11
+ - Improved first-run setup wording from “Free key” to “Signup/key page” so paid/trial-credit providers are not misleading.
12
+ - Exposed provider billing metadata through the Web Dashboard config API so the desktop/web surface can render the same paid-provider warnings as the TUI.
13
+
14
+ ### Fixed
15
+ - Fixed first-run usability for keyless providers by allowing Kilo and LLM7 to count as usable providers without forcing the API key wizard.
16
+ - Fixed Docker dashboard 404s by serving the Web Router Dashboard aliases (`/api/router/status`, `/api/router/stats`, `/api/router/sets`, `/api/router/tokens`, `/api/router/quick-setup`) and `/api/changelog` directly from the router daemon.
17
+ - Fixed confusing "pending" status for models not in the active router set. Models outside the set now show a distinct "NOT IN SET" label with a dim dot and faded row, instead of the animated yellow "wait" indicator. Applies to Web Dashboard (both Docker daemon and local dev mode).
@@ -0,0 +1,5 @@
1
+ # Changelog v0.5.28 - 2026-06-11
2
+
3
+ ### Fixed
4
+ - Fixed Docker dashboard 404s — the router daemon now serves 10 web dashboard API aliases (`/api/router/status`, `/api/router/stats`, `/api/router/sets`, `/api/router/tokens`, `/api/router/quick-setup`, `/api/router/start`, `/api/router/stop`, `/api/router/probe-mode`, `/api/router/sets/:name/*`, `/api/changelog`) directly, so the React frontend works identically in Docker mode and local dev. Cross-origin guards protect mutating endpoints. (Fixes #116, reported by @stgreenb)
5
+ - Fixed confusing "pending" status for models not in the active router set — models outside the set now show a distinct "NOT IN SET" label with a dim gray dot and faded row opacity, instead of the animated yellow "wait" indicator. The `inRouterSet` field is now exposed by both the Docker daemon and the local web server. (Fixes #117, reported by @stgreenb)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
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 — 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 — 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]
@@ -363,6 +363,76 @@ export const opencodeZen = [
363
363
  // Added (2026-06-01): minimax-m3-free — MiniMax M3, 1M context, MSA attention (SWE-Bench Pro 59.0%)
364
364
  ]
365
365
 
366
+ // 📖 Kilo source - https://api.kilo.ai/api/gateway
367
+ // 📖 OpenAI-compatible gateway. `kilo-auto/free` works without a key and routes to Kilo's current free model pool.
368
+ // 📖 Keep only the stable router model here; individual promo `:free` models churn too quickly.
369
+ export const kilo = [
370
+ ['kilo-auto/free', 'Kilo Auto Free', 'A+', '-', '256k'],
371
+ ]
372
+
373
+ // 📖 LLM7 source - https://api.llm7.io/v1
374
+ // 📖 Free unauthenticated tier works with tight shared limits; optional free token at https://token.llm7.io
375
+ // 📖 Pro-tagged models from /v1/models are intentionally excluded.
376
+ export const llm7 = [
377
+ ['qwen3-235b', 'Qwen3 235B', 'S+', '70.0%', '240k'],
378
+ ['mistral-small-3.2', 'Mistral Small 3.2', 'B+', '34.0%', '128k'],
379
+ ['codestral-latest', 'Codestral Latest', 'B+', '34.0%', '32k'],
380
+ ['devstral-small-2:24b', 'Devstral Small 2', 'A', '-', '128k'],
381
+ ]
382
+
383
+ // 📖 Routeway source - https://api.routeway.ai/v1/models
384
+ // 📖 OpenAI-compatible gateway with explicit zero-price `:free` chat models.
385
+ // 📖 Live catalog checked 2026-06-11; only chat-completions models with free pricing are listed.
386
+ export const routeway = [
387
+ ['deepseek-v4-flash:free', 'DeepSeek V4 Flash', 'S+', '72.0%', '1M'],
388
+ ['step-3.5-flash:free', 'Step 3.5 Flash', 'S+', '74.4%', '256k'],
389
+ ['ling-2.6-flash:free', 'Ling 2.6 Flash', 'S', '-', '262k'],
390
+ ['gpt-oss-120b:free', 'GPT OSS 120B', 'S', '60.0%', '131k'],
391
+ ['minimax-m2:free', 'MiniMax M2', 'S', '-', '197k'],
392
+ ['laguna-m.1:free', 'Poolside Laguna M.1', 'S+', '-', '131k'],
393
+ ['laguna-xs.2:free', 'Poolside Laguna XS.2','S+', '-', '131k'],
394
+ ['gemma-4-31b-it:free', 'Gemma 4 31B', 'A', '45.0%', '262k'],
395
+ ['nemotron-3-nano-30b-a3b:free', 'Nemotron Nano 30B', 'A', '43.0%', '256k'],
396
+ ['llama-3.3-70b-instruct:free', 'Llama 3.3 70B', 'A-', '39.5%', '131k'],
397
+ ['mistral-nemo-instruct:free', 'Mistral Nemo', 'B+', '30.0%', '16k'],
398
+ ['nemotron-nano-9b-v2:free', 'Nemotron Nano 9B', 'B+', '18.0%', '128k'],
399
+ ['llama-3.1-8b-instruct:free', 'Llama 3.1 8B', 'B', '28.8%', '16k'],
400
+ ['llama-3.2-3b-instruct:free', 'Llama 3.2 3B', 'B', '20.0%', '16k'],
401
+ ['llama-3.2-1b-instruct:free', 'Llama 3.2 1B', 'C', '-', '16k'],
402
+ ]
403
+
404
+ // 📖 Novita AI source - https://api.novita.ai/openai/v1/models
405
+ // 📖 Novita is mostly paid/trial-credit, so this catalog only includes live chat models reporting 0 input/output price.
406
+ // 📖 Test/dev/placeholder zero-price IDs were intentionally excluded.
407
+ export const novita = [
408
+ ['qwen/qwen3.6-plus', 'Qwen3.6 Plus', 'S+', '72.0%', '1M'],
409
+ ['qwen/qwen3.5-plus', 'Qwen3.5 Plus', 'S', '68.0%', '1M'],
410
+ ['nex-agi/nex-n2-pro', 'Nex N2 Pro', 'S', '-', '262k'],
411
+ ['minimax/m2-her', 'MiniMax M2 HER', 'S', '-', '32k'],
412
+ ]
413
+
414
+ // 📖 Ollama Cloud source - https://ollama.com/pricing and https://ollama.com/v1/models
415
+ // 📖 Free plan includes cloud model access with session/weekly limits. This list keeps coding-relevant cloud models only.
416
+ export const ollamaCloud = [
417
+ ['minimax-m2.7', 'MiniMax M2.7', 'S+', '80.2%', '200k'],
418
+ ['glm-5.1', 'GLM 5.1', 'S+', '77.8%', '203k'],
419
+ ['kimi-k2.6', 'Kimi K2.6', 'S+', '76.8%', '131k'],
420
+ ['deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '73.1%', '1M'],
421
+ ['deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '72.0%', '1M'],
422
+ ['devstral-2:123b', 'Devstral 2 123B', 'S+', '72.2%', '200k'],
423
+ ['qwen3-coder:480b', 'Qwen3 Coder 480B', 'S+', '70.6%', '256k'],
424
+ ['nemotron-3-ultra', 'Nemotron 3 Ultra', 'S+', '-', '1M'],
425
+ ['glm-4.7', 'GLM 4.7', 'S+', '73.8%', '128k'],
426
+ ['qwen3.5:397b', 'Qwen3.5 397B', 'S', '68.0%', '128k'],
427
+ ['qwen3-coder-next', 'Qwen3 Coder Next', 'S', '65.0%', '256k'],
428
+ ['gpt-oss:120b', 'GPT OSS 120B', 'S', '60.0%', '128k'],
429
+ ['minimax-m3', 'MiniMax M3', 'S', '59.0%', '1M'],
430
+ ['mistral-large-3:675b', 'Mistral Large 3', 'A+', '58.0%', '256k'],
431
+ ['nemotron-3-super', 'Nemotron 3 Super', 'A+', '56.0%', '128k'],
432
+ ['gemma4:31b', 'Gemma 4 31B', 'A', '45.0%', '256k'],
433
+ ['gpt-oss:20b', 'GPT OSS 20B', 'A', '42.0%', '128k'],
434
+ ]
435
+
366
436
  // 📖 All sources combined - used by the main script
367
437
  // 📖 Each source has: name (display), url (API endpoint), models (array of model tuples)
368
438
  // 📖 Providers ordered by generosity of free tier (most generous first)
@@ -445,6 +515,33 @@ export const sources = {
445
515
  models: opencodeZen,
446
516
  zenOnly: true,
447
517
  },
518
+ kilo: {
519
+ name: 'Kilo',
520
+ url: 'https://api.kilo.ai/api/gateway/chat/completions',
521
+ models: kilo,
522
+ noKeyNeeded: true,
523
+ },
524
+ llm7: {
525
+ name: 'LLM7',
526
+ url: 'https://api.llm7.io/v1/chat/completions',
527
+ models: llm7,
528
+ noKeyNeeded: true,
529
+ },
530
+ routeway: {
531
+ name: 'Routeway',
532
+ url: 'https://api.routeway.ai/v1/chat/completions',
533
+ models: routeway,
534
+ },
535
+ novita: {
536
+ name: 'Novita AI',
537
+ url: 'https://api.novita.ai/openai/v1/chat/completions',
538
+ models: novita,
539
+ },
540
+ 'ollama-cloud': {
541
+ name: 'Ollama Cloud',
542
+ url: 'https://ollama.com/v1/chat/completions',
543
+ models: ollamaCloud,
544
+ },
448
545
  }
449
546
 
450
547
  // 📖 Flatten all models from all sources — each entry includes providerKey as 6th element
@@ -134,6 +134,11 @@ const ENV_VARS = {
134
134
  zai: 'ZAI_API_KEY',
135
135
  ovhcloud: 'OVH_AI_ENDPOINTS_ACCESS_TOKEN',
136
136
  'opencode-zen': 'OPENCODE_ZEN_API_KEY',
137
+ kilo: ['KILO_API_KEY', 'KILO_TOKEN'],
138
+ llm7: 'LLM7_API_KEY',
139
+ routeway: 'ROUTEWAY_API_KEY',
140
+ novita: 'NOVITA_API_KEY',
141
+ 'ollama-cloud': 'OLLAMA_API_KEY',
137
142
  }
138
143
 
139
144
  // 📖 Smart Router defaults are intentionally conservative: balanced probing,
@@ -205,6 +210,18 @@ function normalizeFavoriteList(favorites) {
205
210
  return normalized
206
211
  }
207
212
 
213
+ // 📖 normalizeStringSet: Convert an array (from JSON) or existing Set into a Set<string>.
214
+ // 📖 Used for hiddenModels and similar set-shaped config fields.
215
+ function normalizeStringSet(value) {
216
+ if (value instanceof Set) return value
217
+ if (!Array.isArray(value)) return new Set()
218
+ const result = new Set()
219
+ for (const entry of value) {
220
+ if (typeof entry === 'string' && entry.trim()) result.add(entry.trim())
221
+ }
222
+ return result
223
+ }
224
+
208
225
  function normalizeApiKeyValue(value) {
209
226
  if (Array.isArray(value)) {
210
227
  const normalized = []
@@ -258,6 +275,7 @@ function normalizeSettingsSection(settings) {
258
275
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
259
276
  favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
260
277
  runAiSpeedTestOnStartup: typeof safeSettings.runAiSpeedTestOnStartup === 'boolean' ? safeSettings.runAiSpeedTestOnStartup : false,
278
+ autoHideBrokenModels: typeof safeSettings.autoHideBrokenModels === 'boolean' ? safeSettings.autoHideBrokenModels : true,
261
279
  theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
262
280
  }
263
281
  }
@@ -466,8 +484,9 @@ function normalizeConfigShape(config) {
466
484
  favorites: normalizeFavoriteList(safeConfig.favorites),
467
485
  telemetry: normalizeTelemetrySection(safeConfig.telemetry),
468
486
  endpointInstalls: normalizeEndpointInstalls(safeConfig.endpointInstalls),
469
-
470
-
487
+ // 📖 hiddenModels: Set of "provider/modelId" keys auto-hidden by the 404 probe (Ctrl+Shift+P).
488
+ // 📖 Only populated when settings.autoHideBrokenModels is true (default).
489
+ hiddenModels: normalizeStringSet(safeConfig.hiddenModels),
471
490
  }
472
491
  const normalizedRouter = normalizeRouterConfig(safeConfig.router)
473
492
  if (normalizedRouter) normalized.router = normalizedRouter
@@ -679,8 +698,12 @@ export function saveConfig(config, options = {}) {
679
698
 
680
699
  try {
681
700
  const persistedConfig = buildPersistedConfig(config, readStoredConfigSnapshot(), options)
682
- const json = JSON.stringify(persistedConfig, null, 2)
683
- writeFileSync(tempPath, json, { mode: 0o600 })
701
+ // 📖 Serialize Sets to arrays for JSON compatibility (e.g. hiddenModels)
702
+ const jsonSafe = JSON.stringify(persistedConfig, (key, value) => {
703
+ if (value instanceof Set) return [...value]
704
+ return value
705
+ }, 2)
706
+ writeFileSync(tempPath, jsonSafe, { mode: 0o600 })
684
707
  renameSync(tempPath, CONFIG_PATH)
685
708
 
686
709
  // 📖 Verify the write succeeded by reading back and validating
@@ -1077,6 +1100,7 @@ export function _emptyProfileSettings() {
1077
1100
  hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
1078
1101
  favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
1079
1102
  runAiSpeedTestOnStartup: false, // 📖 opt-in: automatically fire the Ctrl+U global AI Speed Test after startup.
1103
+ autoHideBrokenModels: true, // 📖 opt-out: auto-hide models that return 404/410 from probe (Ctrl+Shift+P).
1080
1104
  preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
1081
1105
  theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
1082
1106
  }
@@ -1126,5 +1150,6 @@ function _emptyConfig() {
1126
1150
  telemetry: { enabled: null, consentVersion: 0, anonymousId: null },
1127
1151
  endpointInstalls: [],
1128
1152
  settings: _emptyProfileSettings(),
1153
+ hiddenModels: new Set(),
1129
1154
  }
1130
1155
  }
@@ -8,7 +8,8 @@
8
8
  * This module owns three separate concerns that all relate to "knowing about providers":
9
9
  *
10
10
  * 1. `PROVIDER_METADATA` — human-readable display info (label, colour, signup URL, rate limits)
11
- * used in the setup wizard (`promptApiKey`) and the Settings overlay.
11
+ * used in the setup wizard (`promptApiKey`) and the Settings overlay. Providers that need
12
+ * credits/billing can expose `paidProviderNote`, which Settings renders as a 💰 warning.
12
13
  *
13
14
  * 2. `ENV_VAR_NAMES` — maps providerKey → the environment variable name that carries the API key.
14
15
  * Used when spawning OpenCode child processes so that keys stored only in
@@ -23,6 +24,7 @@
23
24
  *
24
25
  * @exports
25
26
  * PROVIDER_METADATA, ENV_VAR_NAMES, OPENCODE_MODEL_MAP,
27
+ * getProviderBillingNote, getProviderLabelWithBilling,
26
28
  * isWindows, isMac, isLinux
27
29
  *
28
30
  * @see bin/free-coding-models.js — consumes all exports from this module
@@ -55,6 +57,11 @@ export const ENV_VAR_NAMES = {
55
57
  ovhcloud: 'OVH_AI_ENDPOINTS_ACCESS_TOKEN',
56
58
  qwen: 'DASHSCOPE_API_KEY',
57
59
  'opencode-zen': 'OPENCODE_ZEN_API_KEY',
60
+ kilo: 'KILO_API_KEY',
61
+ llm7: 'LLM7_API_KEY',
62
+ routeway: 'ROUTEWAY_API_KEY',
63
+ novita: 'NOVITA_API_KEY',
64
+ 'ollama-cloud': 'OLLAMA_API_KEY',
58
65
  }
59
66
 
60
67
  // 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
@@ -70,6 +77,7 @@ export const OPENCODE_MODEL_MAP = {
70
77
  // 📖 `color` is a chalk function for visual distinction in the TUI.
71
78
  // 📖 `signupUrl` / `signupHint` guide users through first-time key generation.
72
79
  // 📖 `rateLimits` gives a quick reminder of the free-tier quota without opening a browser.
80
+ // 📖 `paidProviderNote` marks providers that require credits/billing despite exposing free/trial/free-tagged models.
73
81
  export const PROVIDER_METADATA = {
74
82
  nvidia: {
75
83
  label: 'NVIDIA NIM',
@@ -143,6 +151,7 @@ export const PROVIDER_METADATA = {
143
151
  signupUrl: 'https://deepinfra.com/login',
144
152
  signupHint: 'Login → API keys',
145
153
  rateLimits: 'Free tier: 200 concurrent requests (default)',
154
+ paidProviderNote: 'trial credit provider',
146
155
  },
147
156
  fireworks: {
148
157
  label: 'Fireworks AI',
@@ -157,6 +166,7 @@ export const PROVIDER_METADATA = {
157
166
  signupUrl: 'https://console.mistral.ai/api-keys',
158
167
  signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY; CODESTRAL_API_KEY also works)',
159
168
  rateLimits: 'Codestral free access: 30 req/min, 2000/day',
169
+ paidProviderNote: 'paid - free Experiment plan',
160
170
  },
161
171
  hyperbolic: {
162
172
  label: 'Hyperbolic',
@@ -192,6 +202,7 @@ export const PROVIDER_METADATA = {
192
202
  signupUrl: 'https://api.together.ai/settings/api-keys',
193
203
  signupHint: 'Settings → API keys',
194
204
  rateLimits: 'Credits/promos vary by account (check console)',
205
+ paidProviderNote: 'trial credit provider',
195
206
  },
196
207
  cloudflare: {
197
208
  label: 'Cloudflare Workers AI',
@@ -257,4 +268,64 @@ export const PROVIDER_METADATA = {
257
268
  signupHint: 'Manager → Public Cloud → AI Endpoints → API keys (optional: sandbox works without key)',
258
269
  rateLimits: 'Free sandbox: 2 req/min per IP per model (no key). With API key: 400 RPM',
259
270
  },
271
+ kilo: {
272
+ label: 'Kilo',
273
+ color: chalk.rgb(120, 255, 190),
274
+ signupUrl: 'https://kilo.ai',
275
+ signupHint: 'No key needed for kilo-auto/free; optional OAuth/API token unlocks more models',
276
+ rateLimits: 'Free router model works without a key; limits are managed by Kilo',
277
+ noKeyNeeded: true,
278
+ },
279
+ llm7: {
280
+ label: 'LLM7',
281
+ color: chalk.rgb(180, 255, 140),
282
+ signupUrl: 'https://token.llm7.io',
283
+ signupHint: 'Optional: sign in at token.llm7.io for a free token',
284
+ rateLimits: 'Free shared tier without key; optional free token improves quota',
285
+ noKeyNeeded: true,
286
+ },
287
+ routeway: {
288
+ label: 'Routeway',
289
+ color: chalk.rgb(130, 210, 255),
290
+ signupUrl: 'https://routeway.ai',
291
+ signupHint: 'Create account → API key',
292
+ rateLimits: 'Free :free models with an API key; paid models excluded here',
293
+ paidProviderNote: 'paid — has :free models',
294
+ },
295
+ novita: {
296
+ label: 'Novita AI',
297
+ color: chalk.rgb(255, 185, 120),
298
+ signupUrl: 'https://novita.ai/settings/key-management',
299
+ signupHint: 'Settings → Key Management → Create API key',
300
+ rateLimits: 'Only zero-price live chat models are listed; other Novita models are paid/trial-credit',
301
+ paidProviderNote: 'paid — 3 free models',
302
+ },
303
+ 'ollama-cloud': {
304
+ label: 'Ollama Cloud',
305
+ color: chalk.rgb(230, 230, 230),
306
+ signupUrl: 'https://ollama.com/settings/keys',
307
+ signupHint: 'Settings → Keys → Create API key',
308
+ rateLimits: 'Free plan includes cloud access with session + weekly limits',
309
+ },
310
+ }
311
+
312
+ /**
313
+ * 📖 Return the short paid/billing warning note for a provider, formatted for UI display.
314
+ * @param {string} providerKey
315
+ * @returns {string}
316
+ */
317
+ export function getProviderBillingNote(providerKey) {
318
+ const note = PROVIDER_METADATA[providerKey]?.paidProviderNote
319
+ return typeof note === 'string' && note.trim() ? `(${note.trim()})` : ''
320
+ }
321
+
322
+ /**
323
+ * 📖 Return a provider label with a small money marker when credits/billing are required.
324
+ * @param {string} providerKey
325
+ * @param {string} fallbackLabel
326
+ * @returns {string}
327
+ */
328
+ export function getProviderLabelWithBilling(providerKey, fallbackLabel) {
329
+ const label = PROVIDER_METADATA[providerKey]?.label || fallbackLabel || providerKey
330
+ return getProviderBillingNote(providerKey) ? `${label} 💰` : label
260
331
  }
@@ -54,6 +54,11 @@ export const PROVIDER_CAPABILITIES = {
54
54
  cloudflare: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
55
55
  zai: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
56
56
  'opencode-zen': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
57
+ kilo: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
58
+ llm7: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'rolling' },
59
+ routeway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
60
+ novita: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
61
+ 'ollama-cloud': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'rolling' },
57
62
  }
58
63
 
59
64
  /** Fallback for unrecognized providers */
@@ -49,6 +49,7 @@ import {
49
49
  } from './config.js'
50
50
  import { buildChatCompletionPingBody, ping, resolveCloudflareUrl, shouldUseDisabledThinkingForProvider } from './ping.js'
51
51
  import { benchmarkModel, BENCHMARK_TIMEOUT_MS } from './benchmark.js'
52
+ import { loadChangelog } from './changelog-loader.js'
52
53
  import { sendUsageTelemetry } from './telemetry.js'
53
54
 
54
55
  export const ROUTER_DEFAULT_PORT = 19280
@@ -2397,8 +2398,8 @@ class RouterRuntime {
2397
2398
  * 📖 with only the ones that come back 2xx. Returns the new set + a
2398
2399
  * 📖 sample of probe results so the UI can show "what changed".
2399
2400
  */
2400
- async handleSyncSetRequest(req, res, requestId) {
2401
- const url = req.url ? new URL(req.url, 'http://localhost') : null
2401
+ async handleSyncSetRequest(req, res, requestId, routeUrl = null) {
2402
+ const url = routeUrl || (req.url ? new URL(req.url, 'http://localhost') : null)
2402
2403
  const pathname = url ? url.pathname : ''
2403
2404
  const setSyncMatch = pathname.match(/^\/sets\/([^/]+)\/sync$/)
2404
2405
  if (!setSyncMatch) {
@@ -2524,6 +2525,87 @@ class RouterRuntime {
2524
2525
  await this.handleProbeModeRequest(req, res, requestId)
2525
2526
  return
2526
2527
  }
2528
+
2529
+ // 📖 Docker mode serves the built Web Dashboard directly from the daemon
2530
+ // 📖 on :19280. The React app uses the same `/api/router/*` routes as
2531
+ // 📖 local dev (`web/server.js`), so the daemon must expose aliases for
2532
+ // 📖 its canonical `/health`, `/stats`, and `/sets` APIs instead of
2533
+ // 📖 forcing the frontend to special-case Docker.
2534
+ if (req.method === 'GET' && url.pathname === '/api/router/status') {
2535
+ sendJson(res, 200, this.statusPayload(), { 'x-request-id': requestId })
2536
+ return
2537
+ }
2538
+ if (req.method === 'GET' && url.pathname === '/api/router/stats') {
2539
+ sendJson(res, 200, this.statsPayload(), { 'x-request-id': requestId })
2540
+ return
2541
+ }
2542
+ if (req.method === 'GET' && url.pathname === '/api/router/tokens') {
2543
+ sendJson(res, 200, this.tokenTracker.summary(), { 'x-request-id': requestId })
2544
+ return
2545
+ }
2546
+ if (req.method === 'GET' && url.pathname === '/api/router/quick-setup') {
2547
+ const router = this.routerConfig()
2548
+ sendJson(res, 200, {
2549
+ running: true,
2550
+ port: this.port,
2551
+ baseUrl: `http://127.0.0.1:${this.port}/v1`,
2552
+ model: 'fcm',
2553
+ activeSet: router.activeSet || DEFAULT_ROUTER_SETTINGS.activeSet,
2554
+ apiKey: 'not-needed',
2555
+ }, { 'x-request-id': requestId })
2556
+ return
2557
+ }
2558
+ if (url.pathname === '/api/router/start') {
2559
+ if (req.method !== 'POST') {
2560
+ sendError(res, 405, 'Method not allowed', 'invalid_request_error', 'method_not_allowed', requestId, { allowed: ['POST'] })
2561
+ return
2562
+ }
2563
+ if (!isSameOriginOrLocal(req)) {
2564
+ sendError(res, 403, 'Forbidden cross-origin request', 'invalid_request_error', 'forbidden_origin', requestId)
2565
+ return
2566
+ }
2567
+ sendJson(res, 200, { ...this.statusPayload(), alreadyRunning: true }, { 'x-request-id': requestId })
2568
+ return
2569
+ }
2570
+ if (url.pathname === '/api/router/stop') {
2571
+ if (req.method !== 'POST') {
2572
+ sendError(res, 405, 'Method not allowed', 'invalid_request_error', 'method_not_allowed', requestId, { allowed: ['POST'] })
2573
+ return
2574
+ }
2575
+ if (!isSameOriginOrLocal(req)) {
2576
+ sendError(res, 403, 'Forbidden cross-origin request', 'invalid_request_error', 'forbidden_origin', requestId)
2577
+ return
2578
+ }
2579
+ sendJson(res, 200, { ok: true, stopped: true, message: 'Daemon shutting down' }, { 'x-request-id': requestId })
2580
+ setTimeout(() => this.shutdown(0), 50)
2581
+ return
2582
+ }
2583
+ if (url.pathname === '/api/router/probe-mode' && req.method === 'POST') {
2584
+ if (!isSameOriginOrLocal(req)) {
2585
+ sendError(res, 403, 'Forbidden cross-origin request', 'invalid_request_error', 'forbidden_origin', requestId)
2586
+ return
2587
+ }
2588
+ await this.handleProbeModeRequest(req, res, requestId)
2589
+ return
2590
+ }
2591
+ if (req.method === 'GET' && url.pathname === '/api/changelog') {
2592
+ sendJson(res, 200, loadChangelog(), { 'x-request-id': requestId })
2593
+ return
2594
+ }
2595
+ if (url.pathname === '/api/router/sets' || url.pathname.startsWith('/api/router/sets/')) {
2596
+ if (req.method !== 'GET' && !isSameOriginOrLocal(req)) {
2597
+ sendError(res, 403, 'Forbidden cross-origin request', 'invalid_request_error', 'forbidden_origin', requestId)
2598
+ return
2599
+ }
2600
+ const aliasedUrl = new URL(req.url, `http://localhost:${this.port}`)
2601
+ aliasedUrl.pathname = aliasedUrl.pathname.replace(/^\/api\/router/, '')
2602
+ if (/^\/sets\/[^/]+\/sync$/.test(aliasedUrl.pathname) && req.method === 'POST') {
2603
+ await this.handleSyncSetRequest(req, res, requestId, aliasedUrl)
2604
+ return
2605
+ }
2606
+ await this.handleSetsRequest(req, res, aliasedUrl, requestId)
2607
+ return
2608
+ }
2527
2609
  if (url.pathname === '/sets' || url.pathname.startsWith('/sets/')) {
2528
2610
  // 📖 /sets/:name/sync has a different return type (rebuilds the
2529
2611
  // 📖 set from probes) so it gets its own handler.
package/src/core/setup.js CHANGED
@@ -36,7 +36,7 @@
36
36
  import chalk from 'chalk'
37
37
  import { createRequire } from 'module'
38
38
  import { sources } from '../../sources.js'
39
- import { PROVIDER_METADATA } from './provider-metadata.js'
39
+ import { PROVIDER_METADATA, getProviderBillingNote, getProviderLabelWithBilling } from './provider-metadata.js'
40
40
  import { saveConfig } from './config.js'
41
41
 
42
42
  const require = createRequire(import.meta.url)
@@ -62,7 +62,8 @@ export async function promptApiKey(config) {
62
62
  const meta = PROVIDER_METADATA[key] || {}
63
63
  return {
64
64
  key,
65
- label: meta.label || sources[key]?.name || key,
65
+ label: getProviderLabelWithBilling(key, sources[key]?.name || key),
66
+ billingNote: getProviderBillingNote(key),
66
67
  color: meta.color || chalk.white,
67
68
  url: meta.signupUrl || 'https://example.com',
68
69
  hint: meta.signupHint || 'Create API key',
@@ -77,8 +78,8 @@ export async function promptApiKey(config) {
77
78
  })
78
79
 
79
80
  for (const p of providers) {
80
- console.log(` ${p.color('●')} ${chalk.bold(p.label)}`)
81
- console.log(chalk.dim(` Free key at: `) + chalk.cyanBright(p.url))
81
+ console.log(` ${p.color('●')} ${chalk.bold(p.label)}${p.billingNote ? ' ' + chalk.yellow(p.billingNote) : ''}`)
82
+ console.log(chalk.dim(` Signup/key page: `) + chalk.cyanBright(p.url))
82
83
  console.log(chalk.dim(` ${p.hint}`))
83
84
  const answer = await ask(chalk.dim(` Enter key (or Enter to skip): `))
84
85
  console.log()
package/src/tui/app.js CHANGED
@@ -59,7 +59,7 @@
59
59
  * ⚙️ Configuration:
60
60
  * - API keys stored per-provider in ~/.free-coding-models.json (0600 perms)
61
61
  * - Old ~/.free-coding-models plain-text auto-migrated as nvidia key on first run
62
- * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, GITHUB_TOKEN, MISTRAL_API_KEY, SCALEWAY_API_KEY, GOOGLE_API_KEY, CLOUDFLARE_API_TOKEN, DASHSCOPE_API_KEY, ZAI_API_KEY, etc.
62
+ * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, GITHUB_TOKEN, MISTRAL_API_KEY, SCALEWAY_API_KEY, GOOGLE_API_KEY, CLOUDFLARE_API_TOKEN, DASHSCOPE_API_KEY, ZAI_API_KEY, LLM7_API_KEY, ROUTEWAY_API_KEY, NOVITA_API_KEY, OLLAMA_API_KEY, etc.
63
63
  * - ZAI (z.ai) uses a non-standard base path; cloudflare needs CLOUDFLARE_ACCOUNT_ID in env.
64
64
  * - Cloudflare Workers AI requires both CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and CLOUDFLARE_ACCOUNT_ID
65
65
  * - Models loaded from sources.js — all provider/model definitions are centralized there
@@ -205,10 +205,11 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
205
205
 
206
206
  // 📖 Profile system removed - API keys now persist permanently across all sessions
207
207
 
208
- // 📖 Check if any provider has a key — if not, run the first-time setup wizard
209
- const hasAnyKey = Object.keys(sources).some(pk => !!getApiKey(config, pk))
208
+ // 📖 Check if any provider has a key — if not, run the first-time setup wizard.
209
+ // 📖 Keyless providers (Kilo/LLM7) can still run immediately, so they also count as usable.
210
+ const hasAnyUsableProvider = Object.keys(sources).some(pk => !!getApiKey(config, pk) || PROVIDER_METADATA[pk]?.noKeyNeeded)
210
211
 
211
- if (!hasAnyKey) {
212
+ if (!hasAnyUsableProvider) {
212
213
  const result = await promptApiKey(config)
213
214
  if (!result) {
214
215
  console.log()
@@ -314,6 +315,18 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
314
315
  isPinging: false, // 📖 Per-row live flag so Last Ping can keep last value and show a spinner during refresh.
315
316
  hidden: false, // 📖 Simple flag to hide/show models
316
317
  }))
318
+
319
+ // 📖 Auto-hide models that were previously marked broken by the 404 probe.
320
+ // 📖 Only applies when autoHideBrokenModels setting is enabled (default).
321
+ if (config.settings?.autoHideBrokenModels !== false && config.hiddenModels instanceof Set) {
322
+ for (const r of results) {
323
+ const modelKey = `${r.providerKey}/${r.modelId}`
324
+ if (config.hiddenModels.has(modelKey)) {
325
+ r.hidden = true
326
+ }
327
+ }
328
+ }
329
+
317
330
  syncFavoriteFlags(results, config)
318
331
  // 📖 Garbage-collect favorites that reference models no longer in sources.js,
319
332
  // 📖 so the router dashboard only shows real, launchable models.
@@ -825,6 +838,10 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
825
838
  benchmarkResults: state.benchmarkResults,
826
839
  benchmarkRunning: state.benchmarkRunning,
827
840
  headerFlashColumn: state.headerFlashColumn,
841
+ probeRunning: state.probeRunning,
842
+ probeTotal: state.probeTotal,
843
+ probeCompleted: state.probeCompleted,
844
+ probeHiddenCount: state.probeHiddenCount,
828
845
  }
829
846
  if (state.commandPaletteOpen) {
830
847
  if (!state.commandPaletteFrozenTable) {