free-coding-models 0.1.65 → 0.1.66

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
@@ -2,8 +2,8 @@
2
2
  <img src="https://img.shields.io/npm/v/free-coding-models?color=76b900&label=npm&logo=npm" alt="npm version">
3
3
  <img src="https://img.shields.io/node/v/free-coding-models?color=76b900&logo=node.js" alt="node version">
4
4
  <img src="https://img.shields.io/npm/l/free-coding-models?color=76b900" alt="license">
5
- <img src="https://img.shields.io/badge/models-111-76b900?logo=nvidia" alt="models count">
6
- <img src="https://img.shields.io/badge/providers-13-blue" alt="providers count">
5
+ <img src="https://img.shields.io/badge/models-134-76b900?logo=nvidia" alt="models count">
6
+ <img src="https://img.shields.io/badge/providers-17-blue" alt="providers count">
7
7
  </p>
8
8
 
9
9
  <h1 align="center">free-coding-models</h1>
@@ -24,7 +24,7 @@
24
24
 
25
25
  <p align="center">
26
26
  <strong>Find the fastest coding LLM models in seconds</strong><br>
27
- <sub>Ping free coding models from 13 providers in real-time — pick the best one for OpenCode, OpenClaw, or any AI coding assistant</sub>
27
+ <sub>Ping free coding models from 17 providers in real-time — pick the best one for OpenCode, OpenClaw, or any AI coding assistant</sub>
28
28
  </p>
29
29
 
30
30
  <p align="center">
@@ -47,7 +47,7 @@
47
47
  ## ✨ Features
48
48
 
49
49
  - **🎯 Coding-focused** — Only LLM models optimized for code generation, not chat or vision
50
- - **🌐 Multi-provider** — 111 models from NVIDIA NIM, Groq, Cerebras, SambaNova, OpenRouter, Hugging Face Inference, Replicate, DeepInfra, Fireworks AI, Codestral, Hyperbolic, Scaleway, and Google AI all free to use
50
+ - **🌐 Multi-provider** — 134 models from NVIDIA NIM, Groq, Cerebras, SambaNova, OpenRouter, Hugging Face Inference, Replicate, DeepInfra, Fireworks AI, Codestral, Hyperbolic, Scaleway, Google AI, SiliconFlow, Together AI, Cloudflare Workers AI, and Perplexity API
51
51
  - **⚙️ Settings screen** — Press `P` to manage provider API keys, enable/disable providers, test keys live, and manually check/install updates
52
52
  - **🚀 Parallel pings** — All models tested simultaneously via native `fetch`
53
53
  - **📊 Real-time animation** — Watch latency appear live in alternate screen buffer
@@ -88,10 +88,14 @@ Before using `free-coding-models`, make sure you have:
88
88
  - **Hyperbolic** — [app.hyperbolic.ai/settings](https://app.hyperbolic.ai/settings) → API Keys ($1 free trial)
89
89
  - **Scaleway** — [console.scaleway.com/iam/api-keys](https://console.scaleway.com/iam/api-keys) → IAM → API Keys (1M free tokens)
90
90
  - **Google AI Studio** — [aistudio.google.com/apikey](https://aistudio.google.com/apikey) → Get API key (free Gemma models, 14.4K req/day)
91
+ - **SiliconFlow** — [cloud.siliconflow.cn/account/ak](https://cloud.siliconflow.cn/account/ak) → API Keys (free-model quotas vary by model)
92
+ - **Together AI** — [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys) → API Keys (credits/promotions vary)
93
+ - **Cloudflare Workers AI** — [dash.cloudflare.com](https://dash.cloudflare.com) → Create API token + set `CLOUDFLARE_ACCOUNT_ID` (Free: 10k neurons/day)
94
+ - **Perplexity API** — [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) → API Key (tiered limits by spend)
91
95
  3. **OpenCode** *(optional)* — [Install OpenCode](https://github.com/opencode-ai/opencode) to use the OpenCode integration
92
96
  4. **OpenClaw** *(optional)* — [Install OpenClaw](https://openclaw.ai) to use the OpenClaw integration
93
97
 
94
- > 💡 **Tip:** You don't need all thirteen providers. One key is enough to get started. Add more later via the Settings screen (`P` key). Models without a key still show real latency (`🔑 NO KEY`) so you can evaluate providers before signing up.
98
+ > 💡 **Tip:** You don't need all seventeen providers. One key is enough to get started. Add more later via the Settings screen (`P` key). Models without a key still show real latency (`🔑 NO KEY`) so you can evaluate providers before signing up.
95
99
 
96
100
  ---
97
101
 
@@ -172,13 +176,13 @@ When you run `free-coding-models` without `--opencode` or `--openclaw`, you get
172
176
  Use `↑↓` arrows to select, `Enter` to confirm. Then the TUI launches with your chosen mode shown in the header badge.
173
177
 
174
178
  **How it works:**
175
- 1. **Ping phase** — All enabled models are pinged in parallel (up to 111 across 13 providers)
179
+ 1. **Ping phase** — All enabled models are pinged in parallel (up to 134 across 17 providers)
176
180
  2. **Continuous monitoring** — Models are re-pinged every 2 seconds forever
177
181
  3. **Real-time updates** — Watch "Latest", "Avg", and "Up%" columns update live
178
182
  4. **Select anytime** — Use ↑↓ arrows to navigate, press Enter on a model to act
179
183
  5. **Smart detection** — Automatically detects if NVIDIA NIM is configured in OpenCode or OpenClaw
180
184
 
181
- Setup wizard (first run — walks through all 13 providers):
185
+ Setup wizard (first run — walks through all 17 providers):
182
186
 
183
187
  ```
184
188
  🔑 First-time setup — API keys
@@ -208,7 +212,7 @@ Setup wizard (first run — walks through all 13 providers):
208
212
  You can add or change keys anytime with the P key in the TUI.
209
213
  ```
210
214
 
211
- You don't need all thirteen — skip any provider by pressing Enter. At least one key is required.
215
+ You don't need all seventeen — skip any provider by pressing Enter. At least one key is required.
212
216
 
213
217
  ### Adding or changing keys later
214
218
 
@@ -257,6 +261,10 @@ HUGGINGFACE_API_KEY=hf_xxx free-coding-models
257
261
  REPLICATE_API_TOKEN=r8_xxx free-coding-models
258
262
  DEEPINFRA_API_KEY=di_xxx free-coding-models
259
263
  FIREWORKS_API_KEY=fw_xxx free-coding-models
264
+ SILICONFLOW_API_KEY=sk_xxx free-coding-models
265
+ TOGETHER_API_KEY=together_xxx free-coding-models
266
+ CLOUDFLARE_API_TOKEN=cf_xxx CLOUDFLARE_ACCOUNT_ID=your_account_id free-coding-models
267
+ PERPLEXITY_API_KEY=pplx_xxx free-coding-models
260
268
  FREE_CODING_MODELS_TELEMETRY=0 free-coding-models
261
269
  ```
262
270
 
@@ -306,13 +314,46 @@ When enabled, telemetry events include: event name, app version, selected mode,
306
314
  1. Sign up at [fireworks.ai](https://fireworks.ai)
307
315
  2. Open Settings → Access Tokens and create a token
308
316
 
317
+ **Mistral Codestral**:
318
+ 1. Sign up at [codestral.mistral.ai](https://codestral.mistral.ai)
319
+ 2. Go to API Keys → Create
320
+
321
+ **Hyperbolic**:
322
+ 1. Sign up at [app.hyperbolic.ai/settings](https://app.hyperbolic.ai/settings)
323
+ 2. Create an API key in Settings
324
+
325
+ **Scaleway**:
326
+ 1. Sign up at [console.scaleway.com/iam/api-keys](https://console.scaleway.com/iam/api-keys)
327
+ 2. Go to IAM → API Keys
328
+
329
+ **Google AI Studio**:
330
+ 1. Sign up at [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
331
+ 2. Create an API key for Gemini/Gemma endpoints
332
+
333
+ **SiliconFlow**:
334
+ 1. Sign up at [cloud.siliconflow.cn/account/ak](https://cloud.siliconflow.cn/account/ak)
335
+ 2. Create API key in Account → API Keys
336
+
337
+ **Together AI**:
338
+ 1. Sign up at [api.together.ai/settings/api-keys](https://api.together.ai/settings/api-keys)
339
+ 2. Create an API key in Settings
340
+
341
+ **Cloudflare Workers AI**:
342
+ 1. Sign up at [dash.cloudflare.com](https://dash.cloudflare.com)
343
+ 2. Create an API token with Workers AI permissions
344
+ 3. Export both `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`
345
+
346
+ **Perplexity API**:
347
+ 1. Sign up at [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api)
348
+ 2. Create API key (`PERPLEXITY_API_KEY`)
349
+
309
350
  > 💡 **Free tiers** — each provider exposes a dev/free tier with its own quotas.
310
351
 
311
352
  ---
312
353
 
313
354
  ## 🤖 Coding Models
314
355
 
315
- **111 coding models** across 13 providers and 8 tiers, ranked by [SWE-bench Verified](https://www.swebench.com) — the industry-standard benchmark measuring real GitHub issue resolution. Scores are self-reported by providers unless noted.
356
+ **134 coding models** across 17 providers and 8 tiers, ranked by [SWE-bench Verified](https://www.swebench.com) — the industry-standard benchmark measuring real GitHub issue resolution. Scores are self-reported by providers unless noted.
316
357
 
317
358
  ### NVIDIA NIM (44 models)
318
359
 
@@ -327,7 +368,7 @@ When enabled, telemetry events include: event name, app version, selected mode,
327
368
  | **B** 20–30% | R1 Distill 8B (28.2%), R1 Distill 7B (22.6%) |
328
369
  | **C** <20% | Gemma 2 9B (18.0%), Phi 4 Mini (14.0%), Phi 3.5 Mini (12.0%) |
329
370
 
330
- ### Groq (6 models)
371
+ ### Groq (10 models)
331
372
 
332
373
  | Tier | SWE-bench | Model |
333
374
  |------|-----------|-------|
@@ -336,7 +377,7 @@ When enabled, telemetry events include: event name, app version, selected mode,
336
377
  | **A** 40–50% | Llama 4 Scout (44.0%), R1 Distill 70B (43.9%) |
337
378
  | **A-** 35–40% | Llama 3.3 70B (39.5%) |
338
379
 
339
- ### Cerebras (3 models)
380
+ ### Cerebras (7 models)
340
381
 
341
382
  | Tier | SWE-bench | Model |
342
383
  |------|-----------|-------|
@@ -582,6 +623,11 @@ This script:
582
623
  | `HYPERBOLIC_API_KEY` | Hyperbolic key |
583
624
  | `SCALEWAY_API_KEY` | Scaleway key |
584
625
  | `GOOGLE_API_KEY` | Google AI Studio key |
626
+ | `SILICONFLOW_API_KEY` | SiliconFlow key |
627
+ | `TOGETHER_API_KEY` | Together AI key |
628
+ | `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_API_KEY` | Cloudflare Workers AI token/key |
629
+ | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID (required for Workers AI endpoint URL) |
630
+ | `PERPLEXITY_API_KEY` / `PPLX_API_KEY` | Perplexity API key |
585
631
  | `FREE_CODING_MODELS_TELEMETRY` | `0` disables analytics, `1` enables analytics |
586
632
  | `FREE_CODING_MODELS_POSTHOG_KEY` | PostHog project API key used for anonymous event capture |
587
633
  | `FREE_CODING_MODELS_POSTHOG_HOST` | Optional PostHog ingest host (`https://eu.i.posthog.com` default) |
@@ -597,7 +643,11 @@ This script:
597
643
  "openrouter": "sk-or-xxx",
598
644
  "huggingface": "hf_xxx",
599
645
  "replicate": "r8_xxx",
600
- "deepinfra": "di_xxx"
646
+ "deepinfra": "di_xxx",
647
+ "siliconflow": "sk_xxx",
648
+ "together": "together_xxx",
649
+ "cloudflare": "cf_xxx",
650
+ "perplexity": "pplx_xxx"
601
651
  },
602
652
  "providers": {
603
653
  "nvidia": { "enabled": true },
@@ -606,7 +656,11 @@ This script:
606
656
  "openrouter": { "enabled": true },
607
657
  "huggingface": { "enabled": true },
608
658
  "replicate": { "enabled": true },
609
- "deepinfra": { "enabled": true }
659
+ "deepinfra": { "enabled": true },
660
+ "siliconflow": { "enabled": true },
661
+ "together": { "enabled": true },
662
+ "cloudflare": { "enabled": true },
663
+ "perplexity": { "enabled": true }
610
664
  },
611
665
  "favorites": [
612
666
  "nvidia/deepseek-ai/deepseek-v3.2"
@@ -60,7 +60,8 @@
60
60
  * ⚙️ Configuration:
61
61
  * - API keys stored per-provider in ~/.free-coding-models.json (0600 perms)
62
62
  * - Old ~/.free-coding-models plain-text auto-migrated as nvidia key on first run
63
- * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, HUGGINGFACE_API_KEY/HF_TOKEN, REPLICATE_API_TOKEN, DEEPINFRA_API_KEY/DEEPINFRA_TOKEN, FIREWORKS_API_KEY, etc.
63
+ * - Env vars override config: NVIDIA_API_KEY, GROQ_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY, HUGGINGFACE_API_KEY/HF_TOKEN, REPLICATE_API_TOKEN, DEEPINFRA_API_KEY/DEEPINFRA_TOKEN, FIREWORKS_API_KEY, SILICONFLOW_API_KEY, TOGETHER_API_KEY, PERPLEXITY_API_KEY, etc.
64
+ * - Cloudflare Workers AI requires both CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and CLOUDFLARE_ACCOUNT_ID
64
65
  * - Models loaded from sources.js — all provider/model definitions are centralized there
65
66
  * - OpenCode config: ~/.config/opencode/opencode.json
66
67
  * - OpenClaw config: ~/.openclaw/openclaw.json
@@ -755,6 +756,52 @@ const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
755
756
  // 📖 Spinner cell: braille (1-wide) + padding to fill CELL_W visual chars
756
757
  const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].padEnd(CELL_W))
757
758
 
759
+ // 📖 Overlay-specific backgrounds so Settings (P) and Help (K) are visually distinct
760
+ // 📖 from the main table and from each other.
761
+ const SETTINGS_OVERLAY_BG = chalk.bgRgb(14, 20, 30)
762
+ const HELP_OVERLAY_BG = chalk.bgRgb(24, 16, 32)
763
+ const OVERLAY_PANEL_WIDTH = 116
764
+
765
+ // 📖 Strip ANSI color/control sequences to estimate visible text width before padding.
766
+ function stripAnsi(input) {
767
+ return String(input).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\][^\x1b]*\x1b\\/g, '')
768
+ }
769
+
770
+ // 📖 Tint overlay lines with a fixed dark panel width so the background is clearly visible.
771
+ function tintOverlayLines(lines, bgColor) {
772
+ return lines.map((line) => {
773
+ const text = String(line)
774
+ const visibleWidth = stripAnsi(text).length
775
+ const padding = ' '.repeat(Math.max(0, OVERLAY_PANEL_WIDTH - visibleWidth))
776
+ return bgColor(text + padding)
777
+ })
778
+ }
779
+
780
+ // 📖 Clamp overlay scroll to valid bounds for the current terminal height.
781
+ function clampOverlayOffset(offset, totalLines, terminalRows) {
782
+ const viewportRows = Math.max(1, terminalRows || 1)
783
+ const maxOffset = Math.max(0, totalLines - viewportRows)
784
+ return Math.max(0, Math.min(maxOffset, offset))
785
+ }
786
+
787
+ // 📖 Ensure a target line is visible inside overlay viewport (used by Settings cursor).
788
+ function keepOverlayTargetVisible(offset, targetLine, totalLines, terminalRows) {
789
+ const viewportRows = Math.max(1, terminalRows || 1)
790
+ let next = clampOverlayOffset(offset, totalLines, terminalRows)
791
+ if (targetLine < next) next = targetLine
792
+ else if (targetLine >= next + viewportRows) next = targetLine - viewportRows + 1
793
+ return clampOverlayOffset(next, totalLines, terminalRows)
794
+ }
795
+
796
+ // 📖 Slice overlay lines to terminal viewport and pad with blanks to avoid stale frames.
797
+ function sliceOverlayLines(lines, offset, terminalRows) {
798
+ const viewportRows = Math.max(1, terminalRows || 1)
799
+ const nextOffset = clampOverlayOffset(offset, lines.length, terminalRows)
800
+ const visible = lines.slice(nextOffset, nextOffset + viewportRows)
801
+ while (visible.length < viewportRows) visible.push('')
802
+ return { visible, offset: nextOffset }
803
+ }
804
+
758
805
  // ─── Table renderer ───────────────────────────────────────────────────────────
759
806
 
760
807
  // 📖 Core logic functions (getAvg, getVerdict, getUptime, sortResults, etc.)
@@ -1139,6 +1186,15 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
1139
1186
  // 📖 providerKey and url determine provider-specific request format.
1140
1187
  // 📖 apiKey can be null — in that case no Authorization header is sent.
1141
1188
  // 📖 A 401 response still tells us the server is UP and gives us real latency.
1189
+ function resolveCloudflareUrl(url) {
1190
+ // 📖 Cloudflare's OpenAI-compatible endpoint is account-scoped.
1191
+ // 📖 We resolve {account_id} from env so provider setup can stay simple in config.
1192
+ const accountId = (process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()
1193
+ if (!url.includes('{account_id}')) return url
1194
+ if (!accountId) return url.replace('{account_id}', 'missing-account-id')
1195
+ return url.replace('{account_id}', encodeURIComponent(accountId))
1196
+ }
1197
+
1142
1198
  function buildPingRequest(apiKey, modelId, providerKey, url) {
1143
1199
  if (providerKey === 'replicate') {
1144
1200
  // 📖 Replicate uses /v1/predictions with a different payload than OpenAI chat-completions.
@@ -1151,6 +1207,17 @@ function buildPingRequest(apiKey, modelId, providerKey, url) {
1151
1207
  }
1152
1208
  }
1153
1209
 
1210
+ if (providerKey === 'cloudflare') {
1211
+ // 📖 Cloudflare Workers AI uses OpenAI-compatible payload but needs account_id in URL.
1212
+ const headers = { 'Content-Type': 'application/json' }
1213
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`
1214
+ return {
1215
+ url: resolveCloudflareUrl(url),
1216
+ headers,
1217
+ body: { model: modelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
1218
+ }
1219
+ }
1220
+
1154
1221
  const headers = { 'Content-Type': 'application/json' }
1155
1222
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`
1156
1223
  if (providerKey === 'openrouter') {
@@ -1228,6 +1295,10 @@ const ENV_VAR_NAMES = {
1228
1295
  hyperbolic: 'HYPERBOLIC_API_KEY',
1229
1296
  scaleway: 'SCALEWAY_API_KEY',
1230
1297
  googleai: 'GOOGLE_API_KEY',
1298
+ siliconflow:'SILICONFLOW_API_KEY',
1299
+ together: 'TOGETHER_API_KEY',
1300
+ cloudflare: 'CLOUDFLARE_API_TOKEN',
1301
+ perplexity: 'PERPLEXITY_API_KEY',
1231
1302
  }
1232
1303
 
1233
1304
  // 📖 Provider metadata used by the setup wizard and Settings details panel.
@@ -1324,6 +1395,34 @@ const PROVIDER_METADATA = {
1324
1395
  signupHint: 'Get API key',
1325
1396
  rateLimits: '14.4K req/day, 30/min',
1326
1397
  },
1398
+ siliconflow: {
1399
+ label: 'SiliconFlow',
1400
+ color: chalk.rgb(255, 120, 30),
1401
+ signupUrl: 'https://cloud.siliconflow.cn/account/ak',
1402
+ signupHint: 'API Keys → Create',
1403
+ rateLimits: 'Free models: usually 100 RPM, varies by model',
1404
+ },
1405
+ together: {
1406
+ label: 'Together AI',
1407
+ color: chalk.rgb(0, 180, 255),
1408
+ signupUrl: 'https://api.together.ai/settings/api-keys',
1409
+ signupHint: 'Settings → API keys',
1410
+ rateLimits: 'Credits/promos vary by account (check console)',
1411
+ },
1412
+ cloudflare: {
1413
+ label: 'Cloudflare Workers AI',
1414
+ color: chalk.rgb(242, 119, 36),
1415
+ signupUrl: 'https://dash.cloudflare.com',
1416
+ signupHint: 'Create AI API token + set CLOUDFLARE_ACCOUNT_ID',
1417
+ rateLimits: 'Free: 10k neurons/day, text-gen 300 RPM',
1418
+ },
1419
+ perplexity: {
1420
+ label: 'Perplexity API',
1421
+ color: chalk.rgb(0, 210, 190),
1422
+ signupUrl: 'https://www.perplexity.ai/settings/api',
1423
+ signupHint: 'Generate API key (billing may be required)',
1424
+ rateLimits: 'Tiered limits by spend (default ~50 RPM)',
1425
+ },
1327
1426
  }
1328
1427
 
1329
1428
  // 📖 OpenCode config location varies by platform
@@ -1692,6 +1791,53 @@ After installation, you can use: opencode --model ${modelRef}`
1692
1791
  },
1693
1792
  models: {}
1694
1793
  }
1794
+ } else if (providerKey === 'siliconflow') {
1795
+ config.provider.siliconflow = {
1796
+ npm: '@ai-sdk/openai-compatible',
1797
+ name: 'SiliconFlow',
1798
+ options: {
1799
+ baseURL: 'https://api.siliconflow.com/v1',
1800
+ apiKey: '{env:SILICONFLOW_API_KEY}'
1801
+ },
1802
+ models: {}
1803
+ }
1804
+ } else if (providerKey === 'together') {
1805
+ config.provider.together = {
1806
+ npm: '@ai-sdk/openai-compatible',
1807
+ name: 'Together AI',
1808
+ options: {
1809
+ baseURL: 'https://api.together.xyz/v1',
1810
+ apiKey: '{env:TOGETHER_API_KEY}'
1811
+ },
1812
+ models: {}
1813
+ }
1814
+ } else if (providerKey === 'cloudflare') {
1815
+ const cloudflareAccountId = (process.env.CLOUDFLARE_ACCOUNT_ID || '').trim()
1816
+ if (!cloudflareAccountId) {
1817
+ console.log(chalk.yellow(' ⚠ Cloudflare Workers AI requires CLOUDFLARE_ACCOUNT_ID for OpenCode integration.'))
1818
+ console.log(chalk.dim(' Export CLOUDFLARE_ACCOUNT_ID and retry this selection.'))
1819
+ console.log()
1820
+ return
1821
+ }
1822
+ config.provider.cloudflare = {
1823
+ npm: '@ai-sdk/openai-compatible',
1824
+ name: 'Cloudflare Workers AI',
1825
+ options: {
1826
+ baseURL: `https://api.cloudflare.com/client/v4/accounts/${cloudflareAccountId}/ai/v1`,
1827
+ apiKey: '{env:CLOUDFLARE_API_TOKEN}'
1828
+ },
1829
+ models: {}
1830
+ }
1831
+ } else if (providerKey === 'perplexity') {
1832
+ config.provider.perplexity = {
1833
+ npm: '@ai-sdk/openai-compatible',
1834
+ name: 'Perplexity API',
1835
+ options: {
1836
+ baseURL: 'https://api.perplexity.ai',
1837
+ apiKey: '{env:PERPLEXITY_API_KEY}'
1838
+ },
1839
+ models: {}
1840
+ }
1695
1841
  }
1696
1842
  }
1697
1843
 
@@ -2302,7 +2448,9 @@ async function main() {
2302
2448
  st.scrollOffset = st.cursor - modelSlots + 1
2303
2449
  }
2304
2450
  // Final clamp
2305
- const maxOffset = Math.max(0, total - maxSlots)
2451
+ // 📖 Keep one extra scroll step when top indicator is visible,
2452
+ // 📖 otherwise the last rows become unreachable at the bottom.
2453
+ const maxOffset = Math.max(0, total - maxSlots + 1)
2306
2454
  if (st.scrollOffset > maxOffset) st.scrollOffset = maxOffset
2307
2455
  if (st.scrollOffset < 0) st.scrollOffset = 0
2308
2456
  }
@@ -2337,6 +2485,8 @@ async function main() {
2337
2485
  config, // 📖 Live reference to the config object (updated on save)
2338
2486
  visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
2339
2487
  helpVisible: false, // 📖 Whether the help overlay (K key) is active
2488
+ settingsScrollOffset: 0, // 📖 Vertical scroll offset for Settings overlay viewport
2489
+ helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
2340
2490
  }
2341
2491
 
2342
2492
  // 📖 Re-clamp viewport on terminal resize
@@ -2395,6 +2545,7 @@ async function main() {
2395
2545
  const updateRowIdx = providerKeys.length + 1
2396
2546
  const EL = '\x1b[K'
2397
2547
  const lines = []
2548
+ const cursorLineByRow = {}
2398
2549
 
2399
2550
  lines.push('')
2400
2551
  lines.push(` ${chalk.bold('⚙ Settings')} ${chalk.dim('— free-coding-models v' + LOCAL_VERSION)}`)
@@ -2437,6 +2588,7 @@ async function main() {
2437
2588
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
2438
2589
 
2439
2590
  const row = `${bullet}[ ${enabledBadge} ] ${providerName} ${keyDisplay.padEnd(30)} ${testBadge} ${rateSummary}`
2591
+ cursorLineByRow[i] = lines.length
2440
2592
  lines.push(isCursor ? chalk.bgRgb(30, 30, 60)(row) : row)
2441
2593
  }
2442
2594
 
@@ -2451,6 +2603,11 @@ async function main() {
2451
2603
  lines.push(chalk.dim(` 1) Create a ${selectedMeta.label || selectedSource.name} account: ${selectedMeta.signupUrl || 'signup link missing'}`))
2452
2604
  lines.push(chalk.dim(` 2) ${selectedMeta.signupHint || 'Generate an API key and paste it with Enter on this row'}`))
2453
2605
  lines.push(chalk.dim(` 3) Press ${chalk.yellow('T')} to test your key. Status: ${setupStatus}`))
2606
+ if (selectedProviderKey === 'cloudflare') {
2607
+ const hasAccountId = Boolean((process.env.CLOUDFLARE_ACCOUNT_ID || '').trim())
2608
+ const accountIdStatus = hasAccountId ? chalk.green('CLOUDFLARE_ACCOUNT_ID detected ✅') : chalk.yellow('Set CLOUDFLARE_ACCOUNT_ID ⚠')
2609
+ lines.push(chalk.dim(` 4) Export ${chalk.yellow('CLOUDFLARE_ACCOUNT_ID')} in your shell. Status: ${accountIdStatus}`))
2610
+ }
2454
2611
  lines.push('')
2455
2612
  }
2456
2613
 
@@ -2467,6 +2624,7 @@ async function main() {
2467
2624
  ? chalk.dim('[Config]')
2468
2625
  : chalk.yellow('[Env override]')
2469
2626
  const telemetryRow = `${telemetryRowBullet}${chalk.bold('Anonymous usage analytics').padEnd(44)} ${telemetryStatus} ${telemetrySource}`
2627
+ cursorLineByRow[telemetryRowIdx] = lines.length
2470
2628
  lines.push(telemetryCursor ? chalk.bgRgb(30, 30, 60)(telemetryRow) : telemetryRow)
2471
2629
 
2472
2630
  lines.push('')
@@ -2488,6 +2646,7 @@ async function main() {
2488
2646
  if (updateState === 'error') updateStatus = chalk.red('Check failed (press U to retry)')
2489
2647
  if (updateState === 'installing') updateStatus = chalk.cyan('Installing update…')
2490
2648
  const updateRow = `${updateBullet}${chalk.bold(updateActionLabel).padEnd(44)} ${updateStatus}`
2649
+ cursorLineByRow[updateRowIdx] = lines.length
2491
2650
  lines.push(updateCursor ? chalk.bgRgb(30, 30, 60)(updateRow) : updateRow)
2492
2651
  if (updateState === 'error' && state.settingsUpdateError) {
2493
2652
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
@@ -2501,9 +2660,19 @@ async function main() {
2501
2660
  }
2502
2661
  lines.push('')
2503
2662
 
2504
- const cleared = lines.map(l => l + EL)
2505
- const remaining = state.terminalRows > 0 ? Math.max(0, state.terminalRows - cleared.length) : 0
2506
- for (let i = 0; i < remaining; i++) cleared.push(EL)
2663
+ // 📖 Keep selected Settings row visible on small terminals by scrolling the overlay viewport.
2664
+ const targetLine = cursorLineByRow[state.settingsCursor] ?? 0
2665
+ state.settingsScrollOffset = keepOverlayTargetVisible(
2666
+ state.settingsScrollOffset,
2667
+ targetLine,
2668
+ lines.length,
2669
+ state.terminalRows
2670
+ )
2671
+ const { visible, offset } = sliceOverlayLines(lines, state.settingsScrollOffset, state.terminalRows)
2672
+ state.settingsScrollOffset = offset
2673
+
2674
+ const tintedLines = tintOverlayLines(visible, SETTINGS_OVERLAY_BG)
2675
+ const cleared = tintedLines.map(l => l + EL)
2507
2676
  return cleared.join('\n')
2508
2677
  }
2509
2678
 
@@ -2514,7 +2683,7 @@ async function main() {
2514
2683
  const EL = '\x1b[K'
2515
2684
  const lines = []
2516
2685
  lines.push('')
2517
- lines.push(` ${chalk.bold('❓ Keyboard Shortcuts')} ${chalk.dim('— press K or Esc to close')}`)
2686
+ lines.push(` ${chalk.bold('❓ Keyboard Shortcuts')} ${chalk.dim('— ↑↓ / PgUp / PgDn / Home / End scroll • K or Esc close')}`)
2518
2687
  lines.push('')
2519
2688
  lines.push(` ${chalk.bold('Main TUI')}`)
2520
2689
  lines.push(` ${chalk.bold('Navigation')}`)
@@ -2541,6 +2710,8 @@ async function main() {
2541
2710
  lines.push('')
2542
2711
  lines.push(` ${chalk.bold('Settings (P)')}`)
2543
2712
  lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
2713
+ lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
2714
+ lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
2544
2715
  lines.push(` ${chalk.yellow('Enter')} Edit key / toggle analytics / check-install update`)
2545
2716
  lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
2546
2717
  lines.push(` ${chalk.yellow('T')} Test selected provider key`)
@@ -2558,9 +2729,11 @@ async function main() {
2558
2729
  lines.push(` ${chalk.cyan('free-coding-models --no-telemetry')} ${chalk.dim('Disable telemetry for this run')}`)
2559
2730
  lines.push(` ${chalk.dim('Flags can be combined: --openclaw --tier S')}`)
2560
2731
  lines.push('')
2561
- const cleared = lines.map(l => l + EL)
2562
- const remaining = state.terminalRows > 0 ? Math.max(0, state.terminalRows - cleared.length) : 0
2563
- for (let i = 0; i < remaining; i++) cleared.push(EL)
2732
+ // 📖 Help overlay can be longer than viewport, so keep a dedicated scroll offset.
2733
+ const { visible, offset } = sliceOverlayLines(lines, state.helpScrollOffset, state.terminalRows)
2734
+ state.helpScrollOffset = offset
2735
+ const tintedLines = tintOverlayLines(visible, HELP_OVERLAY_BG)
2736
+ const cleared = tintedLines.map(l => l + EL)
2564
2737
  return cleared.join('\n')
2565
2738
  }
2566
2739
 
@@ -2635,9 +2808,20 @@ async function main() {
2635
2808
  const onKeyPress = async (str, key) => {
2636
2809
  if (!key) return
2637
2810
 
2638
- // 📖 Help overlay: Esc or K closes it handle before everything else so Esc isn't swallowed elsewhere
2639
- if (state.helpVisible && (key.name === 'escape' || key.name === 'k')) {
2640
- state.helpVisible = false
2811
+ // 📖 Help overlay: full keyboard navigation + key swallowing while overlay is open.
2812
+ if (state.helpVisible) {
2813
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
2814
+ if (key.name === 'escape' || key.name === 'k') {
2815
+ state.helpVisible = false
2816
+ return
2817
+ }
2818
+ if (key.name === 'up') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
2819
+ if (key.name === 'down') { state.helpScrollOffset += 1; return }
2820
+ if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
2821
+ if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
2822
+ if (key.name === 'home') { state.helpScrollOffset = 0; return }
2823
+ if (key.name === 'end') { state.helpScrollOffset = Number.MAX_SAFE_INTEGER; return }
2824
+ if (key.ctrl && key.name === 'c') { exit(0); return }
2641
2825
  return
2642
2826
  }
2643
2827
 
@@ -2717,6 +2901,28 @@ async function main() {
2717
2901
  return
2718
2902
  }
2719
2903
 
2904
+ if (key.name === 'pageup') {
2905
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
2906
+ state.settingsCursor = Math.max(0, state.settingsCursor - pageStep)
2907
+ return
2908
+ }
2909
+
2910
+ if (key.name === 'pagedown') {
2911
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
2912
+ state.settingsCursor = Math.min(updateRowIdx, state.settingsCursor + pageStep)
2913
+ return
2914
+ }
2915
+
2916
+ if (key.name === 'home') {
2917
+ state.settingsCursor = 0
2918
+ return
2919
+ }
2920
+
2921
+ if (key.name === 'end') {
2922
+ state.settingsCursor = updateRowIdx
2923
+ return
2924
+ }
2925
+
2720
2926
  if (key.name === 'return') {
2721
2927
  if (state.settingsCursor === telemetryRowIdx) {
2722
2928
  ensureTelemetryConfig(state.config)
@@ -2784,6 +2990,7 @@ async function main() {
2784
2990
  state.settingsCursor = 0
2785
2991
  state.settingsEditMode = false
2786
2992
  state.settingsEditBuffer = ''
2993
+ state.settingsScrollOffset = 0
2787
2994
  return
2788
2995
  }
2789
2996
 
@@ -2816,12 +3023,21 @@ async function main() {
2816
3023
  if (key.name === 'f') {
2817
3024
  const selected = state.visibleSorted[state.cursor]
2818
3025
  if (!selected) return
3026
+ const wasFavorite = selected.isFavorite
2819
3027
  toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
2820
3028
  syncFavoriteFlags(state.results, state.config)
2821
3029
  applyTierFilter()
2822
- const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
2823
3030
  const visible = state.results.filter(r => !r.hidden)
2824
3031
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
3032
+
3033
+ // 📖 UX rule: when unpinning a favorite, jump back to the top of the list.
3034
+ if (wasFavorite) {
3035
+ state.cursor = 0
3036
+ state.scrollOffset = 0
3037
+ return
3038
+ }
3039
+
3040
+ const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
2825
3041
  const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
2826
3042
  if (newCursor >= 0) state.cursor = newCursor
2827
3043
  else if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
@@ -2864,6 +3080,7 @@ async function main() {
2864
3080
  // 📖 Help overlay key: K = toggle help overlay
2865
3081
  if (key.name === 'k') {
2866
3082
  state.helpVisible = !state.helpVisible
3083
+ if (state.helpVisible) state.helpScrollOffset = 0
2867
3084
  return
2868
3085
  }
2869
3086
 
@@ -2882,18 +3099,20 @@ async function main() {
2882
3099
  }
2883
3100
 
2884
3101
  if (key.name === 'up') {
2885
- if (state.cursor > 0) {
2886
- state.cursor--
2887
- adjustScrollOffset(state)
2888
- }
3102
+ // 📖 Main list wrap navigation: top -> bottom on Up.
3103
+ const count = state.visibleSorted.length
3104
+ if (count === 0) return
3105
+ state.cursor = state.cursor > 0 ? state.cursor - 1 : count - 1
3106
+ adjustScrollOffset(state)
2889
3107
  return
2890
3108
  }
2891
3109
 
2892
3110
  if (key.name === 'down') {
2893
- if (state.cursor < state.visibleSorted.length - 1) {
2894
- state.cursor++
2895
- adjustScrollOffset(state)
2896
- }
3111
+ // 📖 Main list wrap navigation: bottom -> top on Down.
3112
+ const count = state.visibleSorted.length
3113
+ if (count === 0) return
3114
+ state.cursor = state.cursor < count - 1 ? state.cursor + 1 : 0
3115
+ adjustScrollOffset(state)
2897
3116
  return
2898
3117
  }
2899
3118
 
package/lib/config.js CHANGED
@@ -24,7 +24,11 @@
24
24
  * "codestral": "csk-xxx",
25
25
  * "hyperbolic": "eyJ...",
26
26
  * "scaleway": "scw-xxx",
27
- * "googleai": "AIza..."
27
+ * "googleai": "AIza...",
28
+ * "siliconflow":"sk-xxx",
29
+ * "together": "together-xxx",
30
+ * "cloudflare": "cf-xxx",
31
+ * "perplexity": "pplx-xxx"
28
32
  * },
29
33
  * "providers": {
30
34
  * "nvidia": { "enabled": true },
@@ -39,7 +43,11 @@
39
43
  * "codestral": { "enabled": true },
40
44
  * "hyperbolic": { "enabled": true },
41
45
  * "scaleway": { "enabled": true },
42
- * "googleai": { "enabled": true }
46
+ * "googleai": { "enabled": true },
47
+ * "siliconflow":{ "enabled": true },
48
+ * "together": { "enabled": true },
49
+ * "cloudflare": { "enabled": true },
50
+ * "perplexity": { "enabled": true }
43
51
  * },
44
52
  * "favorites": [
45
53
  * "nvidia/deepseek-ai/deepseek-v3.2"
@@ -94,6 +102,10 @@ const ENV_VARS = {
94
102
  hyperbolic: 'HYPERBOLIC_API_KEY',
95
103
  scaleway: 'SCALEWAY_API_KEY',
96
104
  googleai: 'GOOGLE_API_KEY',
105
+ siliconflow:'SILICONFLOW_API_KEY',
106
+ together: 'TOGETHER_API_KEY',
107
+ cloudflare: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_API_KEY'],
108
+ perplexity: ['PERPLEXITY_API_KEY', 'PPLX_API_KEY'],
97
109
  }
98
110
 
99
111
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
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
@@ -27,8 +27,8 @@
27
27
  * 📖 Secondary: https://swe-rebench.com (independent evals, scores are lower)
28
28
  * 📖 Leaderboard tracker: https://www.marc0.dev/en/leaderboard
29
29
  *
30
- * @exports nvidiaNim, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai — model arrays per provider
31
- * @exports sources — map of { nvidia, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai } each with { name, url, models }
30
+ * @exports nvidiaNim, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity — model arrays per provider
31
+ * @exports sources — map of { nvidia, groq, cerebras, sambanova, openrouter, huggingface, replicate, deepinfra, fireworks, codestral, hyperbolic, scaleway, googleai, siliconflow, together, cloudflare, perplexity } each with { name, url, models }
32
32
  * @exports MODELS — flat array of [modelId, label, tier, sweScore, ctx, providerKey]
33
33
  *
34
34
  * 📖 MODELS now includes providerKey as 6th element so ping() knows which
@@ -230,6 +230,54 @@ export const googleai = [
230
230
  ['gemma-3-4b-it', 'Gemma 3 4B', 'C', '10.0%', '128k'],
231
231
  ]
232
232
 
233
+ // 📖 SiliconFlow source - https://cloud.siliconflow.cn
234
+ // 📖 OpenAI-compatible endpoint: https://api.siliconflow.com/v1/chat/completions
235
+ // 📖 Free model quotas vary by model and can change over time.
236
+ export const siliconflow = [
237
+ ['Qwen/Qwen3-Coder-480B-A35B-Instruct', 'Qwen3 Coder 480B', 'S+', '70.6%', '256k'],
238
+ ['deepseek-ai/DeepSeek-V3.2', 'DeepSeek V3.2', 'S+', '73.1%', '128k'],
239
+ ['Qwen/Qwen3-235B-A22B', 'Qwen3 235B', 'S+', '70.0%', '128k'],
240
+ ['deepseek-ai/DeepSeek-R1', 'DeepSeek R1', 'S', '61.0%', '128k'],
241
+ ['Qwen/Qwen3-Coder-30B-A3B-Instruct', 'Qwen3 Coder 30B', 'A+', '55.0%', '32k'],
242
+ ['Qwen/Qwen2.5-Coder-32B-Instruct', 'Qwen2.5 Coder 32B', 'A', '46.0%', '32k'],
243
+ ]
244
+
245
+ // 📖 Together AI source - https://api.together.ai
246
+ // 📖 OpenAI-compatible endpoint: https://api.together.xyz/v1/chat/completions
247
+ // 📖 Credits/promotions vary by account and region; verify current quota in console.
248
+ export const together = [
249
+ ['moonshotai/Kimi-K2.5', 'Kimi K2.5', 'S+', '76.8%', '128k'],
250
+ ['Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', 'Qwen3 Coder 480B', 'S+', '70.6%', '256k'],
251
+ ['deepseek-ai/DeepSeek-V3.1', 'DeepSeek V3.1', 'S', '62.0%', '128k'],
252
+ ['deepseek-ai/DeepSeek-R1', 'DeepSeek R1', 'S', '61.0%', '128k'],
253
+ ['openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '60.0%', '128k'],
254
+ ['openai/gpt-oss-20b', 'GPT OSS 20B', 'A', '42.0%', '128k'],
255
+ ['meta-llama/Llama-3.3-70B-Instruct-Turbo', 'Llama 3.3 70B', 'A-', '39.5%', '128k'],
256
+ ]
257
+
258
+ // 📖 Cloudflare Workers AI source - https://developers.cloudflare.com/workers-ai
259
+ // 📖 OpenAI-compatible endpoint requires account id:
260
+ // 📖 https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions
261
+ // 📖 Free plan includes daily neuron quota and provider-level request limits.
262
+ export const cloudflare = [
263
+ ['@cf/openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '60.0%', '128k'],
264
+ ['@cf/qwen/qwen2.5-coder-32b-instruct', 'Qwen2.5 Coder 32B', 'A', '46.0%', '32k'],
265
+ ['@cf/deepseek-ai/deepseek-r1-distill-qwen-32b', 'R1 Distill 32B', 'A', '43.9%', '128k'],
266
+ ['@cf/openai/gpt-oss-20b', 'GPT OSS 20B', 'A', '42.0%', '128k'],
267
+ ['@cf/meta/llama-3.3-70b-instruct-fp8-fast', 'Llama 3.3 70B', 'A-', '39.5%', '128k'],
268
+ ['@cf/meta/llama-3.1-8b-instruct', 'Llama 3.1 8B', 'B', '28.8%', '128k'],
269
+ ]
270
+
271
+ // 📖 Perplexity source - https://docs.perplexity.ai
272
+ // 📖 Chat Completions endpoint: https://api.perplexity.ai/chat/completions
273
+ // 📖 Sonar models focus on search/reasoning and have tiered API rate limits.
274
+ export const perplexity = [
275
+ ['sonar-reasoning-pro', 'Sonar Reasoning Pro', 'A+', '50.0%', '128k'],
276
+ ['sonar-reasoning', 'Sonar Reasoning', 'A', '45.0%', '128k'],
277
+ ['sonar-pro', 'Sonar Pro', 'B+', '32.0%', '128k'],
278
+ ['sonar', 'Sonar', 'B', '25.0%', '128k'],
279
+ ]
280
+
233
281
  // 📖 All sources combined - used by the main script
234
282
  // 📖 Each source has: name (display), url (API endpoint), models (array of model tuples)
235
283
  export const sources = {
@@ -298,6 +346,26 @@ export const sources = {
298
346
  url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
299
347
  models: googleai,
300
348
  },
349
+ siliconflow: {
350
+ name: 'SiliconFlow',
351
+ url: 'https://api.siliconflow.com/v1/chat/completions',
352
+ models: siliconflow,
353
+ },
354
+ together: {
355
+ name: 'Together AI',
356
+ url: 'https://api.together.xyz/v1/chat/completions',
357
+ models: together,
358
+ },
359
+ cloudflare: {
360
+ name: 'Cloudflare AI',
361
+ url: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions',
362
+ models: cloudflare,
363
+ },
364
+ perplexity: {
365
+ name: 'Perplexity',
366
+ url: 'https://api.perplexity.ai/chat/completions',
367
+ models: perplexity,
368
+ },
301
369
  }
302
370
 
303
371
  // 📖 Flatten all models from all sources — each entry includes providerKey as 6th element