free-coding-models 0.3.48 → 0.3.50

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/CHANGELOG.md CHANGED
@@ -1,10 +1,41 @@
1
- ## [0.3.48] - 2026-04-11
1
+ ## [0.3.50] - 2026-04-11
2
2
 
3
- ### Fixed
4
- - **jcode model validation freeze (for real this time)** — jcode has a hardcoded model whitelist that rejects bare model names like `gpt-oss-120b` via `--model` flag. The v0.3.47 fix using `JCODE_MODEL` env var didn't actually work — jcode silently ignores it for the `openai-compatible` provider. The real fix uses two strategies:
5
- 1. **Native providers**: For providers that jcode supports natively (Groq, Cerebras, DeepInfra, Scaleway, Together, Hugging Face, Fireworks, Chutes, OpenRouter, Perplexity, ZAI, Mistral), we now use `--provider groq` instead of `--provider openai-compatible`. Native providers accept namespaced model names like `openai/gpt-oss-120b` without validation issues.
6
- 2. **OpenAI-compatible fallback**: For providers without a native jcode match (NVIDIA NIM, SambaNova, etc.), we keep `--provider openai-compatible` but ensure model IDs always have a namespace prefix (`openai/gpt-oss-120b` instead of `gpt-oss-120b`) and set a placeholder `OPENROUTER_API_KEY` to satisfy jcode's false credential check on namespaced models.
3
+ ### Changed
7
4
 
8
- ### Added
9
- - **`JCODE_NATIVE_PROVIDERS` mapping** — New provider mapping table that routes 12 of our providers to jcode's native provider system, with correct env var names extracted from the jcode binary. This gives better compatibility and avoids the `openai-compatible` provider's model validation bugs.
10
- - **`ensureJcodeModelPrefix()` helper** Ensures model IDs always have a namespace prefix (e.g. `gpt-oss-120b` `openai/gpt-oss-120b`) so jcode's whitelist validation is bypassed.
5
+ - **Providers reordered by generosity of free tier** — All 25 providers are now sorted from most generous to least generous in the README, TUI Settings page, and `D` key filter cycling. No more hunting for the best free option.
6
+
7
+ - **Free tier limits corrected across all providers** Verified and corrected free tier limits for every provider using live web research. Key corrections:
8
+ - **Groq**: 30 RPM, 1K-14.4K req/day (previously listed as "30-50 RPM per model")
9
+ - **Google AI Studio**: 15-60 RPM, 250-1.5K req/day (previously listed as "14.4K req/day, 30/min")
10
+ - **Together AI**: ❌ **No free tier** — requires $5 minimum purchase. Removed from the "free" recommendation.
11
+ - **iFlow**: ⚠️ **Shutting down April 17, 2026** — marked with warning in README and sources.js
12
+
13
+ - **README subtitle updated** — Now says "ranked by generosity of free tier (most generous first)" instead of "ranked by SWE-bench"
14
+
15
+ ### Provider generosity ranking (most generous first)
16
+
17
+ 1. Groq (30 RPM, 1K-14.4K req/day)
18
+ 2. Cerebras (1M tokens/day)
19
+ 3. Google AI Studio (15-60 RPM, 250-1.5K req/day)
20
+ 4. NVIDIA NIM (~40 RPM)
21
+ 5. Cloudflare Workers AI (10K neurons/day)
22
+ 6. OpenRouter (50 req/day free)
23
+ 7. DeepInfra (200 concurrent requests)
24
+ 8. HuggingFace (~$0.10/month)
25
+ 9. Perplexity (~50 RPM tiered)
26
+ 10. SambaNova (generous dev quota)
27
+ 11. Fireworks AI ($1 credits)
28
+ 12. Hyperbolic ($1 credits)
29
+ 13. OVHcloud AI (2 req/min/IP free)
30
+ 14. Replicate (6 req/min free)
31
+ 15. Codestral (30 RPM, 2K req/day)
32
+ 16. ZAI (generous free quota)
33
+ 17. Scaleway (1M tokens)
34
+ 18. Alibaba DashScope (1M tokens/90 days)
35
+ 19. SiliconFlow (100 req/day + $1 credits)
36
+ 20. Rovo Dev CLI (5M tokens/day)
37
+ 21. Gemini CLI (1K req/day)
38
+ 22. Chutes AI (free community GPU)
39
+ 23. OpenCode Zen (free with account)
40
+ 24. Together AI (❌ no free tier)
41
+ 25. iFlow (⚠️ shutting down April 17, 2026)
package/README.md CHANGED
@@ -34,7 +34,7 @@ create a free account on one of the [providers](#-list-of-free-ai-providers)
34
34
  <a href="#-why-this-tool">💡 Why</a> •
35
35
  <a href="#-quick-start">⚡ Quick Start</a> •
36
36
  <a href="#-list-of-free-ai-providers">🟢 Providers</a> •
37
- <a href="#-bonus-free-stuff">🎁 Bonus Free Stuff</a> •
37
+ <a href="#-other-free-ai-resources">🆓 Other Free AI Resources</a> •
38
38
  <a href="#-usage">🚀 Usage</a> •
39
39
  <a href="#-tui-keys">⌨️ TUI Keys</a> •
40
40
  <a href="#-features">✨ Features</a> •
@@ -72,43 +72,43 @@ It then writes the model you pick directly into your coding tool's config — so
72
72
 
73
73
  Create a free account on one provider below to get started:
74
74
 
75
- **238 coding models** across 25 providers, ranked by [SWE-bench Verified](https://www.swebench.com).
76
-
77
- | Provider | Models | Tier range | Free tier | Env var |
78
- |----------|--------|-----------|-----------|--------|
79
- | [NVIDIA NIM](https://build.nvidia.com) | 46 | S+C | 40 req/min (no credit card needed) | `NVIDIA_API_KEY` |
80
- | [OpenRouter](https://openrouter.ai/keys) | 25 | S+ → C | Free on :free: 50/day <$10, 1000/day ≥$10 (20 req/min) | `OPENROUTER_API_KEY` |
81
- | [Cloudflare Workers AI](https://dash.cloudflare.com) | 15 | SB | Free: 10k neurons/day, text-gen 300 RPM | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
82
- | [SambaNova](https://sambanova.ai/developers) | 13 | S+ → B | Dev tier generous quota | `SAMBANOVA_API_KEY` |
83
- | [Hyperbolic](https://app.hyperbolic.ai/settings) | 13 | S+A- | $1 free trial credits | `HYPERBOLIC_API_KEY` |
84
- | [Together AI](https://api.together.ai/settings/api-keys) | 19 | S+ → A- | Credits/promos vary by account (check console) | `TOGETHER_API_KEY` |
85
- | [Scaleway](https://console.scaleway.com/iam/api-keys) | 10 | S+ → B+ | 1M free tokens | `SCALEWAY_API_KEY` |
86
- | [iFlow](https://platform.iflow.cn) | 11 | S+A+ | Free for individuals (no req limits, 7-day key expiry) | `IFLOW_API_KEY` |
87
- | [Alibaba DashScope](https://modelstudio.console.alibabacloud.com) | 11 | S+ → A | 1M free tokens per model (Singapore region, 90 days) | `DASHSCOPE_API_KEY` |
88
- | [Groq](https://console.groq.com/keys) | 8 | S → B | 30‑50 RPM per model (varies by model) | `GROQ_API_KEY` |
89
- | [Rovo Dev CLI](https://www.atlassian.com/rovo) | 5 | S+ | 5M tokens/day (beta) | CLI tool 🦘 |
90
- | [ZAI](https://z.ai) | 7 | S+ → S | Free tier (generous quota) | `ZAI_API_KEY` |
91
- | [OpenCode Zen](https://opencode.ai/zen) | 7 | S+A+ | Free with OpenCode account | Zen models |
92
- | [Google AI Studio](https://aistudio.google.com/apikey) | 6 | B+C | 14.4K req/day, 30/min | `GOOGLE_API_KEY` |
93
- | [SiliconFlow](https://cloud.siliconflow.cn/account/ak) | 6 | S+ → A | Free models: usually 100 RPM, varies by model | `SILICONFLOW_API_KEY` |
94
- | [Cerebras](https://cloud.cerebras.ai) | 4 | S+ → B | Generous free tier (developer tier 10× higher limits) | `CEREBRAS_API_KEY` |
95
- | [Perplexity API](https://www.perplexity.ai/settings/api) | 4 | A+ → B | Tiered limits by spend (default ~50 RPM) | `PERPLEXITY_API_KEY` |
96
- | [OVHcloud AI Endpoints](https://endpoints.ai.cloud.ovh.net) | 8 | S → B | Free sandbox: 2 req/min/IP (no key). 400 RPM with key | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
97
- | [Chutes AI](https://chutes.ai) | 4 | S → A | Free (community GPU-powered, no credit card) | `CHUTES_API_KEY` |
98
- | [DeepInfra](https://deepinfra.com/login) | 4 | A- → B+ | 200 concurrent requests (default) | `DEEPINFRA_API_KEY` |
99
- | [Fireworks AI](https://fireworks.ai) | 4 | S → B+ | $1 credits – 10 req/min without payment | `FIREWORKS_API_KEY` |
100
- | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 3 | S+ → A+ | 1,000 req/day | CLI tool |
101
- | [Hugging Face](https://huggingface.com/settings/tokens) | 2 | S → B | Free monthly credits (~$0.10) | `HUGGINGFACE_API_KEY` |
102
- | [Replicate](https://replicate.com/account/api-tokens) | 2 | A-B | 6 req/min (no payment) up to 3,000 RPM with payment | `REPLICATE_API_TOKEN` |
103
- | [Mistral Codestral](https://codestral.mistral.ai) | 1 | B+ | 30 req/min, 2000/day | `CODESTRAL_API_KEY` |
75
+ **238 coding models** across 25 providers, ranked by generosity of free tier (most generous first).
76
+
77
+ | # | Provider | Models | Tier range | Free tier | Env var |
78
+ |---|----------|--------|-----------|-----------|--------|
79
+ | 1 | [Groq](https://console.groq.com/keys) | 8 | S → B | 30 RPM, 1K‑14.4K req/day (no credit card) | `GROQ_API_KEY` |
80
+ | 2 | [Cerebras](https://cloud.cerebras.ai) | 4 | S+ → B | 30 RPM, 1M tokens/day (no credit card) | `CEREBRAS_API_KEY` |
81
+ | 3 | [Google AI Studio](https://aistudio.google.com/apikey) | 6 | B+C | 15‑60 RPM, 250‑1.5K req/day (no credit card) | `GOOGLE_API_KEY` |
82
+ | 4 | [NVIDIA NIM](https://build.nvidia.com) | 46 | S+ → C | ~40 RPM (no credit card) | `NVIDIA_API_KEY` |
83
+ | 5 | [Cloudflare Workers AI](https://dash.cloudflare.com) | 15 | S → B | 10K neurons/day, 300 RPM (no credit card) | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
84
+ | 6 | [OpenRouter](https://openrouter.ai/keys) | 25 | S+ → C | 50 req/day free, 1K/day with $10 spend | `OPENROUTER_API_KEY` |
85
+ | 7 | [DeepInfra](https://deepinfra.com/login) | 4 | A- → B+ | 200 concurrent requests (no credit card) | `DEEPINFRA_API_KEY` |
86
+ | 8 | [Hugging Face](https://huggingface.com/settings/tokens) | 2 | S → B | ~$0.10/month free credits | `HUGGINGFACE_API_KEY` |
87
+ | 9 | [Perplexity API](https://www.perplexity.ai/settings/api) | 4 | A+ → B | ~50 RPM (tiered by spend) | `PERPLEXITY_API_KEY` |
88
+ | 10 | [SambaNova](https://sambanova.ai/developers) | 13 | S+ → B | Dev tier generous quota (no credit card) | `SAMBANOVA_API_KEY` |
89
+ | 11 | [Fireworks AI](https://fireworks.ai) | 4 | S → B+ | $1 free credits, 10 RPM without payment | `FIREWORKS_API_KEY` |
90
+ | 12 | [Hyperbolic](https://app.hyperbolic.ai/settings) | 13 | S+ → A- | $1 free credits (permanent) | `HYPERBOLIC_API_KEY` |
91
+ | 13 | [OVHcloud AI Endpoints](https://endpoints.ai.cloud.ovh.net) | 8 | S → B | 2 req/min/IP free, 400 RPM with key | `OVH_AI_ENDPOINTS_ACCESS_TOKEN` |
92
+ | 14 | [Replicate](https://replicate.com/account/api-tokens) | 2 | A-B | 6 req/min free, 3K RPM with payment | `REPLICATE_API_TOKEN` |
93
+ | 15 | [Codestral](https://codestral.mistral.ai) | 1 | B+ | 30 RPM, 2K req/day (no credit card) | `CODESTRAL_API_KEY` |
94
+ | 16 | [ZAI](https://z.ai) | 7 | S+ → S | Generous free quota (concurrency limited) | `ZAI_API_KEY` |
95
+ | 17 | [Scaleway](https://console.scaleway.com/iam/api-keys) | 10 | S+ → B+ | 1M free tokens (permanent) | `SCALEWAY_API_KEY` |
96
+ | 18 | [Alibaba DashScope](https://modelstudio.console.alibabacloud.com) | 11 | S+A | 1M free tokens/model (90 days) | `DASHSCOPE_API_KEY` |
97
+ | 19 | [SiliconFlow](https://cloud.siliconflow.cn/account/ak) | 6 | S+ → A | 100 req/day + $1 free credits | `SILICONFLOW_API_KEY` |
98
+ | 20 | [Rovo Dev CLI](https://www.atlassian.com/rovo) | 5 | S+ | 5M tokens/day (beta) | CLI tool 🦘 |
99
+ | 21 | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 3 | S+A+ | 1,000 req/day (no credit card) | CLI tool ♊ |
100
+ | 22 | [Chutes AI](https://chutes.ai) | 4 | S → A | Free community GPU (no credit card) | `CHUTES_API_KEY` |
101
+ | 23 | [OpenCode Zen](https://opencode.ai/zen) | 7 | S+A+ | Free with OpenCode account | Zen models ✨ |
102
+ | 24 | [Together AI](https://api.together.ai/settings/api-keys) | 19 | S+A- | No free tier requires $5 purchase | `TOGETHER_API_KEY` |
103
+ | 25 | [iFlow ⚠️](https://platform.iflow.cn) | 11 | S+ → A+ | Shutting down April 17, 2026 | `IFLOW_API_KEY` |
104
104
 
105
105
  > 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
106
106
 
107
107
  ---
108
108
 
109
- ## 🎁 Bonus Free Stuff
109
+ ## 🆓 Other Free AI Resources
110
110
 
111
- **Everything free that isn't in the CLI** — IDE extensions, coding agents, GitHub lists, trial credits, and more.
111
+ **Curated free resources outside the CLI** — IDE extensions, coding agents, GitHub lists, and trial credits.
112
112
 
113
113
  ### 📚 Awesome Lists (curated by the community)
114
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
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
@@ -480,12 +480,9 @@ export const opencodeZen = [
480
480
 
481
481
  // 📖 All sources combined - used by the main script
482
482
  // 📖 Each source has: name (display), url (API endpoint), models (array of model tuples)
483
+ // 📖 Providers ordered by generosity of free tier (most generous first)
484
+ // 📖 See README for full tier-by-tier comparison
483
485
  export const sources = {
484
- nvidia: {
485
- name: 'NIM',
486
- url: 'https://integrate.api.nvidia.com/v1/chat/completions',
487
- models: nvidiaNim,
488
- },
489
486
  groq: {
490
487
  name: 'Groq',
491
488
  url: 'https://api.groq.com/openai/v1/chat/completions',
@@ -496,92 +493,91 @@ export const sources = {
496
493
  url: 'https://api.cerebras.ai/v1/chat/completions',
497
494
  models: cerebras,
498
495
  },
499
- sambanova: {
500
- name: 'SambaNova',
501
- url: 'https://api.sambanova.ai/v1/chat/completions',
502
- models: sambanova,
496
+ googleai: {
497
+ name: 'Google AI Studio',
498
+ url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
499
+ models: googleai,
500
+ },
501
+ nvidia: {
502
+ name: 'NIM',
503
+ url: 'https://integrate.api.nvidia.com/v1/chat/completions',
504
+ models: nvidiaNim,
505
+ },
506
+ cloudflare: {
507
+ name: 'Cloudflare AI',
508
+ url: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions',
509
+ models: cloudflare,
503
510
  },
504
511
  openrouter: {
505
512
  name: 'OpenRouter',
506
513
  url: 'https://openrouter.ai/api/v1/chat/completions',
507
514
  models: openrouter,
508
515
  },
516
+ deepinfra: {
517
+ name: 'DeepInfra',
518
+ url: 'https://api.deepinfra.com/v1/openai/chat/completions',
519
+ models: deepinfra,
520
+ },
509
521
  huggingface: {
510
522
  name: 'Hugging Face',
511
523
  url: 'https://router.huggingface.co/v1/chat/completions',
512
524
  models: huggingface,
513
525
  },
514
- replicate: {
515
- name: 'Replicate',
516
- url: 'https://api.replicate.com/v1/predictions',
517
- models: replicate,
526
+ perplexity: {
527
+ name: 'Perplexity',
528
+ url: 'https://api.perplexity.ai/chat/completions',
529
+ models: perplexity,
518
530
  },
519
- deepinfra: {
520
- name: 'DeepInfra',
521
- url: 'https://api.deepinfra.com/v1/openai/chat/completions',
522
- models: deepinfra,
531
+ sambanova: {
532
+ name: 'SambaNova',
533
+ url: 'https://api.sambanova.ai/v1/chat/completions',
534
+ models: sambanova,
523
535
  },
524
536
  fireworks: {
525
537
  name: 'Fireworks',
526
538
  url: 'https://api.fireworks.ai/inference/v1/chat/completions',
527
539
  models: fireworks,
528
540
  },
529
- codestral: {
530
- name: 'Codestral',
531
- url: 'https://api.mistral.ai/v1/chat/completions',
532
- models: codestral,
533
- },
534
541
  hyperbolic: {
535
542
  name: 'Hyperbolic',
536
543
  url: 'https://api.hyperbolic.xyz/v1/chat/completions',
537
544
  models: hyperbolic,
538
545
  },
539
- scaleway: {
540
- name: 'Scaleway',
541
- url: 'https://api.scaleway.ai/v1/chat/completions',
542
- models: scaleway,
546
+ ovhcloud: {
547
+ name: 'OVHcloud AI',
548
+ url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions',
549
+ models: ovhcloud,
543
550
  },
544
- googleai: {
545
- name: 'Google AI',
546
- url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
547
- models: googleai,
551
+ replicate: {
552
+ name: 'Replicate',
553
+ url: 'https://api.replicate.com/v1/predictions',
554
+ models: replicate,
555
+ },
556
+ codestral: {
557
+ name: 'Codestral',
558
+ url: 'https://api.mistral.ai/v1/chat/completions',
559
+ models: codestral,
548
560
  },
549
561
  zai: {
550
562
  name: 'ZAI',
551
563
  url: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
552
564
  models: zai,
553
565
  },
554
- siliconflow: {
555
- name: 'SiliconFlow',
556
- url: 'https://api.siliconflow.com/v1/chat/completions',
557
- models: siliconflow,
558
- },
559
- together: {
560
- name: 'Together AI',
561
- url: 'https://api.together.xyz/v1/chat/completions',
562
- models: together,
563
- },
564
- cloudflare: {
565
- name: 'Cloudflare AI',
566
- url: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions',
567
- models: cloudflare,
568
- },
569
- perplexity: {
570
- name: 'Perplexity',
571
- url: 'https://api.perplexity.ai/chat/completions',
572
- models: perplexity,
566
+ scaleway: {
567
+ name: 'Scaleway',
568
+ url: 'https://api.scaleway.ai/v1/chat/completions',
569
+ models: scaleway,
573
570
  },
574
571
  qwen: {
575
- name: 'Alibaba Cloud (DashScope)',
572
+ name: 'Alibaba DashScope',
576
573
  url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions',
577
574
  models: qwen,
578
575
  },
579
- iflow: {
580
- name: 'iFlow',
581
- url: 'https://apis.iflow.cn/v1/chat/completions',
582
- models: iflow,
576
+ siliconflow: {
577
+ name: 'SiliconFlow',
578
+ url: 'https://api.siliconflow.com/v1/chat/completions',
579
+ models: siliconflow,
583
580
  },
584
- // 📖 CLI-only tools (no API endpoint - launched directly)
585
581
  rovo: {
586
582
  name: 'Rovo Dev CLI',
587
583
  url: null, // CLI tool - no API endpoint
@@ -600,21 +596,27 @@ export const sources = {
600
596
  binary: 'gemini',
601
597
  checkArgs: ['--version'],
602
598
  },
599
+ chutes: {
600
+ name: 'Chutes AI',
601
+ url: 'https://chutes.ai/v1/chat/completions',
602
+ models: chutes,
603
+ },
603
604
  'opencode-zen': {
604
605
  name: 'OpenCode Zen',
605
606
  url: 'https://opencode.ai/zen/v1/chat/completions',
606
607
  models: opencodeZen,
607
608
  zenOnly: true,
608
609
  },
609
- chutes: {
610
- name: 'Chutes AI',
611
- url: 'https://chutes.ai/v1/chat/completions',
612
- models: chutes,
610
+ together: {
611
+ name: 'Together AI',
612
+ url: 'https://api.together.xyz/v1/chat/completions',
613
+ models: together,
613
614
  },
614
- ovhcloud: {
615
- name: 'OVHcloud AI 🆕',
616
- url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions',
617
- models: ovhcloud,
615
+ iflow: {
616
+ name: 'iFlow ⚠️',
617
+ url: 'https://apis.iflow.cn/v1/chat/completions',
618
+ models: iflow,
619
+ shutdownDate: '2026-04-17', // 📖 Shutting down April 17, 2026
618
620
  },
619
621
  }
620
622
 
@@ -40,7 +40,7 @@ const TOOL_MODE_COMMANDS = TOOL_MODE_ORDER.map((toolMode) => {
40
40
  const meta = TOOL_METADATA[toolMode] || { label: toolMode, emoji: '🧰' }
41
41
  return {
42
42
  id: `action-set-tool-${toolMode}`,
43
- label: `${meta.emoji} ${meta.label}`,
43
+ label: meta.label,
44
44
  toolMode,
45
45
  icon: meta.emoji,
46
46
  description: TOOL_MODE_DESCRIPTIONS[toolMode] || 'Set this as the active launch target.',
@@ -55,12 +55,83 @@ const PROVIDER_TEST_MODEL_OVERRIDES = {
55
55
  const SETTINGS_TEST_MAX_ATTEMPTS = 10
56
56
  const SETTINGS_TEST_RETRY_DELAY_MS = 4000
57
57
 
58
+ // 📖 PROVIDER_AUTH_ENDPOINTS maps provider keys to their auth-check URL + method.
59
+ // 📖 For most providers this is the /models endpoint (returns 200=valid, 401=invalid).
60
+ // 📖 Providers without an auth-check endpoint use null (falls back to chat completion ping).
61
+ // 📖 Special cases:
62
+ // 📖 - replicate: uses /v1/predictions (not /models) but needs a different payload
63
+ // 📖 - cloudflare: no auth endpoint — only has chat completions, always uses ping fallback
64
+ const PROVIDER_AUTH_ENDPOINTS = {
65
+ nvidia: { url: 'https://api.nvidia.com/v1/account', method: 'GET' },
66
+ groq: { url: 'https://api.groq.com/v1/models', method: 'GET' },
67
+ cerebras: { url: 'https://api.cerebras.ai/v1/models', method: 'GET' },
68
+ sambanova: { url: 'https://api.sambanova.ai/v1/models', method: 'GET' },
69
+ openrouter: { url: 'https://openrouter.ai/api/v1/models', method: 'GET' },
70
+ huggingface: { url: 'https://router.huggingface.co/v1/models', method: 'GET' },
71
+ deepinfra: { url: 'https://api.deepinfra.com/v1/models', method: 'GET' },
72
+ fireworks: { url: 'https://api.fireworks.ai/v1/models', method: 'GET' },
73
+ hyperbolic: { url: 'https://api.hyperbolic.xyz/v1/models', method: 'GET' },
74
+ scaleway: { url: 'https://api.scaleway.ai/v1/models', method: 'GET' },
75
+ siliconflow: { url: 'https://api.siliconflow.com/v1/models', method: 'GET' },
76
+ together: { url: 'https://api.together.xyz/v1/models', method: 'GET' },
77
+ perplexity: { url: 'https://api.perplexity.ai/v1/models', method: 'GET' },
78
+ chutes: { url: 'https://chutes.ai/v1/models', method: 'GET' },
79
+ ovhcloud: { url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models', method: 'GET' },
80
+ qwen: { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', method: 'GET' },
81
+ iflow: { url: 'https://apis.iflow.cn/v1/models', method: 'GET' },
82
+ replicate: null, // 📖 Replicate has no /models endpoint; use chat completions ping
83
+ cloudflare: null, // 📖 Workers AI has no auth-check endpoint; use ping only
84
+ zai: null, // 📖 ZAI undocumented; use ping only
85
+ googleai: null, // 📖 Google AI Studio has no OpenAI-compatible /models; use ping
86
+ 'opencode-zen': null, // 📖 OpenCode Zen uses OpenCode auth only; use ping
87
+ rovo: null, // 📖 CLI tool — no API key
88
+ gemini: null, // 📖 CLI tool — no API key
89
+ }
90
+
58
91
  // 📖 Sleep helper kept local to this module so the Settings key test flow can
59
92
  // 📖 back off between retries without leaking timer logic into the rest of the TUI.
60
93
  function sleep(ms) {
61
94
  return new Promise((resolve) => setTimeout(resolve, ms))
62
95
  }
63
96
 
97
+ // 📖 testProviderKeyDirect: Fast auth-only check using /v1/account or /v1/models.
98
+ // 📖 Fires 3 parallel probes to get a fast decisive result (auth error vs timeout vs 200).
99
+ // 📖 Returns { code, ms } from the first non-timeout response, or the best available.
100
+ async function testProviderKeyDirect(apiKey, providerKey) {
101
+ const authConfig = PROVIDER_AUTH_ENDPOINTS[providerKey]
102
+ if (!authConfig) return null
103
+
104
+ const { url, method } = authConfig
105
+ const headers = { Authorization: `Bearer ${apiKey}` }
106
+ if (providerKey === 'openrouter') {
107
+ headers['HTTP-Referer'] = 'https://github.com/vava-nessa/free-coding-models'
108
+ headers['X-Title'] = 'free-coding-models'
109
+ }
110
+
111
+ const parallel = 3
112
+ const promises = Array.from({ length: parallel }, async () => {
113
+ const ctrl = new AbortController()
114
+ const timer = setTimeout(() => ctrl.abort(), 8000)
115
+ const t0 = performance.now()
116
+ try {
117
+ const resp = await fetch(url, { method, headers, signal: ctrl.signal })
118
+ return { code: resp.status, ms: Math.round(performance.now() - t0) }
119
+ } catch (err) {
120
+ const isTimeout = err.name === 'AbortError'
121
+ return { code: isTimeout ? '000' : 'ERR', ms: isTimeout ? 'TIMEOUT' : Math.round(performance.now() - t0) }
122
+ } finally {
123
+ clearTimeout(timer)
124
+ }
125
+ })
126
+
127
+ const results = await Promise.all(promises)
128
+ const success = results.find(r => r.code === 200)
129
+ if (success) return success
130
+ const authFailure = results.find(r => r.code === 401 || r.code === 403)
131
+ if (authFailure) return authFailure
132
+ return results[0]
133
+ }
134
+
64
135
  /**
65
136
  * 📖 buildProviderModelsUrl derives the matching `/models` endpoint for providers
66
137
  * 📖 that expose an OpenAI-compatible model list next to `/chat/completions`.
@@ -471,7 +542,10 @@ export function createKeyHandler(ctx) {
471
542
  }
472
543
 
473
544
  // ─── Settings key test helper ───────────────────────────────────────────────
474
- // 📖 Fires a single ping to the selected provider to verify the API key works.
545
+ // 📖 Verifies an API key by first doing a fast parallel auth-only probe (3×8s)
546
+ // 📖 to /v1/account or /v1/models, then falling back to chat completion pings.
547
+ // 📖 Auth-only result is decisive (200=valid, 401/403=invalid); only timeouts or
548
+ // 📖 providers without auth endpoints fall through to the ping-based approach.
475
549
  async function testProviderKey(providerKey) {
476
550
  const src = sources[providerKey]
477
551
  if (!src) return
@@ -484,6 +558,24 @@ export function createKeyHandler(ctx) {
484
558
  return
485
559
  }
486
560
 
561
+ // 📖 Fast path: parallel auth-only probes (3×8s) to /v1/account or /v1/models.
562
+ // 📖 200 = key valid and accepted. 401/403 = key rejected. null = no auth endpoint.
563
+ const authResult = await testProviderKeyDirect(testKey, providerKey)
564
+ if (authResult) {
565
+ if (authResult.code === 200) {
566
+ state.settingsTestResults[providerKey] = 'ok'
567
+ state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'ok', [], `Auth-only probe returned HTTP 200.`)
568
+ return
569
+ }
570
+ if (authResult.code === 401 || authResult.code === 403) {
571
+ state.settingsTestResults[providerKey] = 'auth_error'
572
+ state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'auth_error', [], `Auth probe returned HTTP ${authResult.code}.`)
573
+ return
574
+ }
575
+ // 📖 Timeout or ERR — fall through to ping-based approach below.
576
+ }
577
+
578
+ // 📖 Slow path: ping-based verification (providers without auth endpoint or timeouts).
487
579
  state.settingsTestResults[providerKey] = 'pending'
488
580
  state.settingsTestDetails[providerKey] = `Testing ${providerLabel} across up to ${SETTINGS_TEST_MAX_ATTEMPTS} probes...`
489
581
  const discoveredModelIds = []
@@ -508,7 +600,6 @@ export function createKeyHandler(ctx) {
508
600
  discoveryNote = `Live model discovery returned HTTP ${modelsResp.status}; falling back to the repo catalog.`
509
601
  }
510
602
  } catch (err) {
511
- // 📖 Discovery failure is non-fatal; we still have repo-defined fallbacks.
512
603
  discoveryNote = `Live model discovery failed (${err?.name || 'error'}); falling back to the repo catalog.`
513
604
  }
514
605
  }
@@ -519,35 +610,46 @@ export function createKeyHandler(ctx) {
519
610
  state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'fail', [], discoveryNote || 'No candidate model was available for probing.')
520
611
  return
521
612
  }
613
+
614
+ // 📖 Parallel ping burst: fire up to 5 probes simultaneously to get fast feedback.
615
+ const PARALLEL_PROBES = 5
522
616
  const attempts = []
617
+ let settled = false
523
618
 
524
- for (let attemptIndex = 0; attemptIndex < SETTINGS_TEST_MAX_ATTEMPTS; attemptIndex++) {
525
- const testModel = candidateModels[attemptIndex % candidateModels.length]
526
- const { code } = await ping(testKey, testModel, providerKey, src.url)
527
- attempts.push({ attempt: attemptIndex + 1, model: testModel, code })
619
+ while (!settled) {
620
+ const batch = []
621
+ for (let i = 0; i < PARALLEL_PROBES && attempts.length + batch.length < SETTINGS_TEST_MAX_ATTEMPTS; i++) {
622
+ const testModel = candidateModels[(attempts.length + batch.length) % candidateModels.length]
623
+ batch.push(ping(testKey, testModel, providerKey, src.url).then(({ code }) => ({ attempt: attempts.length + batch.length + 1, model: testModel, code })))
624
+ }
625
+ const batchResults = await Promise.all(batch)
626
+ attempts.push(...batchResults)
528
627
 
529
- if (code === '200') {
628
+ // 📖 Check outcome after each parallel batch.
629
+ const outcome = classifyProviderTestOutcome(attempts.map(({ code }) => code))
630
+ if (outcome === 'ok') {
530
631
  state.settingsTestResults[providerKey] = 'ok'
531
632
  state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'ok', attempts, discoveryNote)
532
- return
633
+ settled = true
634
+ continue
533
635
  }
534
-
535
- const outcome = classifyProviderTestOutcome(attempts.map(({ code: attemptCode }) => attemptCode))
536
636
  if (outcome === 'auth_error') {
537
637
  state.settingsTestResults[providerKey] = 'auth_error'
538
638
  state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, 'auth_error', attempts, discoveryNote)
539
- return
639
+ settled = true
640
+ continue
540
641
  }
541
-
542
- if (attemptIndex < SETTINGS_TEST_MAX_ATTEMPTS - 1) {
543
- state.settingsTestDetails[providerKey] = `Testing ${providerLabel}... probe ${attemptIndex + 1}/${SETTINGS_TEST_MAX_ATTEMPTS} failed on ${testModel} (${code}). Retrying in ${SETTINGS_TEST_RETRY_DELAY_MS / 1000}s.`
544
- await sleep(SETTINGS_TEST_RETRY_DELAY_MS)
642
+ if (attempts.length >= SETTINGS_TEST_MAX_ATTEMPTS) {
643
+ state.settingsTestResults[providerKey] = outcome
644
+ state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, outcome, attempts, discoveryNote)
645
+ settled = true
646
+ continue
545
647
  }
546
- }
547
648
 
548
- const finalOutcome = classifyProviderTestOutcome(attempts.map(({ code }) => code))
549
- state.settingsTestResults[providerKey] = finalOutcome
550
- state.settingsTestDetails[providerKey] = buildProviderTestDetail(providerLabel, finalOutcome, attempts, discoveryNote)
649
+ // 📖 Show progress between batches, then pause before next round.
650
+ state.settingsTestDetails[providerKey] = `Testing ${providerLabel}... ${attempts.length}/${SETTINGS_TEST_MAX_ATTEMPTS} probes tried. Retrying in ${SETTINGS_TEST_RETRY_DELAY_MS / 1000}s.`
651
+ await sleep(SETTINGS_TEST_RETRY_DELAY_MS)
652
+ }
551
653
  }
552
654
 
553
655
  // 📖 Manual update checker from settings; keeps status visible in maintenance row.
@@ -747,6 +849,20 @@ export function createKeyHandler(ctx) {
747
849
  state.settingsAddKeyMode = false
748
850
  state.settingsEditBuffer = ''
749
851
  state.settingsScrollOffset = 0
852
+
853
+ // 📖 Auto-test all configured API keys in parallel on Settings open.
854
+ // 📖 Each provider with a saved key fires a parallel auth probe batch immediately.
855
+ // 📖 The T key re-triggers a focused test on the selected row without clearing others.
856
+ const providerKeys = Object.keys(sources)
857
+ for (const pk of providerKeys) {
858
+ const testKey = getApiKey(state.config, pk)
859
+ if (testKey) {
860
+ // 📖 Fire and forget — update state as probes resolve.
861
+ testProviderKey(pk)
862
+ } else {
863
+ state.settingsTestResults[pk] = 'missing_key'
864
+ }
865
+ }
750
866
  }
751
867
 
752
868
  function openRecommendOverlay() {
@@ -2293,7 +2409,8 @@ export function createKeyHandler(ctx) {
2293
2409
  }
2294
2410
 
2295
2411
  // 📖 Alt+W: toggle footer visibility (collapse to single hint when hidden)
2296
- if (key.name === 'w' && key.alt && !key.ctrl && !key.meta) {
2412
+ // 📖 Note: readline doesn't set key.alt=true for ALT combos; detect via str starting with \x1b (ESC)
2413
+ if (key.name === 'w' && str && str.startsWith('\x1b') && !key.ctrl && !key.meta) {
2297
2414
  state.footerHidden = !state.footerHidden
2298
2415
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
2299
2416
  state.config.settings.footerHidden = state.footerHidden