pi-free 2.0.11 → 2.0.13
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 +628 -576
- package/README.md +29 -42
- package/banner.svg +1 -1
- package/config.ts +99 -2
- package/constants.ts +3 -1
- package/index.ts +5 -3
- package/lib/built-in-toggle.ts +120 -60
- package/lib/registry.ts +40 -16
- package/lib/util.ts +13 -12
- package/package.json +2 -2
- package/providers/cline/cline-models.ts +3 -10
- package/providers/crofai/crofai.ts +5 -1
- package/providers/deepinfra/deepinfra.ts +7 -5
- package/providers/dynamic-built-in/index.ts +129 -33
- package/providers/model-fetcher.ts +2 -13
- package/providers/novita/novita.ts +205 -0
- package/providers/nvidia/nvidia.ts +4 -6
- package/providers/opencode-session.ts +371 -33
- package/providers/sambanova/sambanova.ts +8 -2
- package/providers/together/together.ts +6 -9
- package/providers/zenmux/zenmux.ts +6 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ When you install pi-free, it:
|
|
|
20
20
|
|
|
21
21
|
3. **Filters to show only free models by default** for providers that expose pricing — You see only the models that cost $0 to use. Paid models are hidden until you explicitly toggle them on.
|
|
22
22
|
|
|
23
|
-
4. **Provides per-provider toggle commands** — Run `/toggle-{provider}` (e.g., `/toggle-kilo
|
|
23
|
+
4. **Provides per-provider toggle commands** — Run `/toggle-{provider}` (e.g., `/toggle-kilo`) to switch between free-only mode and showing all models including paid ones. Changes apply immediately and your preference is saved for the next Pi restart.
|
|
24
24
|
|
|
25
25
|
5. **Handles authentication for you** — OAuth flows (Kilo, Cline) open your browser automatically; API keys are read from `~/.pi/free.json` or environment variables
|
|
26
26
|
|
|
@@ -46,7 +46,6 @@ Free models are shown by default — look for the provider prefixes:
|
|
|
46
46
|
|
|
47
47
|
**✅ Free Models (no payment required):**
|
|
48
48
|
|
|
49
|
-
- `opencode/` — OpenCode models (no setup required; toggle with `/toggle-opencode`)
|
|
50
49
|
- `kilo/` — Kilo models (free models available immediately, more after `/login kilo`)
|
|
51
50
|
- `openrouter/` — OpenRouter models (free account required)
|
|
52
51
|
- `cline/` — Cline models (run `/login cline` to use)
|
|
@@ -64,6 +63,7 @@ Free models are shown by default — look for the provider prefixes:
|
|
|
64
63
|
- `crofai/` — CrofAI OpenAI-compatible API (streaming, reasoning models)
|
|
65
64
|
- `codestral/` — Codestral via Mistral (free Experiment plan: 2 req/min, 1B tokens/month)
|
|
66
65
|
- `deepinfra/` — DeepInfra inference cloud ($5 one-time trial credit, no credit card)
|
|
66
|
+
- `novita/` — Novita AI (100+ open-source models, OpenAI-compatible, 3 free models)
|
|
67
67
|
|
|
68
68
|
> **Note:** Paid providers may occasionally offer free models or promotional credits. The `isFreeModel` helper automatically detects free models based on provider pricing data or model names containing "free". For providers that don't expose pricing (like CrofAI), only models with "free" in their names are marked as free.
|
|
69
69
|
|
|
@@ -74,6 +74,8 @@ Free models are shown by default — look for the provider prefixes:
|
|
|
74
74
|
- `cerebras/` — Cerebras models (when `CEREBRAS_API_KEY` set)
|
|
75
75
|
- `xai/` — xAI models (when `XAI_API_KEY` set)
|
|
76
76
|
- `huggingface/` — Hugging Face models (when `HF_TOKEN` set)
|
|
77
|
+
- `openrouter/` — OpenRouter models (fetched from openrouter.ai, when `OPENROUTER_API_KEY` set)
|
|
78
|
+
- `fastrouter/` — FastRouter models (always discovered, 170+ models, no auth for listing)
|
|
77
79
|
|
|
78
80
|
**Note:** Fireworks is now a [built-in Pi provider](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#0681---2026-04-22) — no extension needed. Set `FIREWORKS_API_KEY` to use it directly.
|
|
79
81
|
|
|
@@ -82,7 +84,6 @@ Free models are shown by default — look for the provider prefixes:
|
|
|
82
84
|
Want to see paid models too? Run the toggle command for your provider:
|
|
83
85
|
|
|
84
86
|
```
|
|
85
|
-
/toggle-opencode # Toggle OpenCode (✅ offers free models)
|
|
86
87
|
/toggle-kilo # Toggle Kilo (✅ offers free models)
|
|
87
88
|
/toggle-openrouter # Toggle OpenRouter (✅ offers free models)
|
|
88
89
|
/toggle-cline # Toggle Cline (✅ offers free models)
|
|
@@ -100,6 +101,8 @@ Want to see paid models too? Run the toggle command for your provider:
|
|
|
100
101
|
/toggle-together # Toggle Together AI (💳 trial credit provider)
|
|
101
102
|
/toggle-sambanova # Toggle SambaNova (🔄 freemium)
|
|
102
103
|
/toggle-llm7 # Toggle LLM7 (✅ free gateway)
|
|
104
|
+
/toggle-novita # Toggle Novita AI (💳 paid — 3 free models)
|
|
105
|
+
/toggle-fastrouter # Toggle FastRouter (🔧 dynamic — always discovered)
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
**Notes:**
|
|
@@ -108,10 +111,6 @@ Want to see paid models too? Run the toggle command for your provider:
|
|
|
108
111
|
- **🔧 Dynamic providers** show all fetched models by default — the toggle filters the list when you have an API key configured
|
|
109
112
|
- **Freemium providers** show all models by default; you manage your usage limits via their dashboards
|
|
110
113
|
|
|
111
|
-
You'll see a notification like: `opencode: showing free models` or `opencode: showing all models`
|
|
112
|
-
|
|
113
|
-
**Note:** Built-in provider toggles such as OpenCode and OpenRouter update in the current session — no restart needed.
|
|
114
|
-
|
|
115
114
|
### 4. Add API keys for more providers (optional)
|
|
116
115
|
|
|
117
116
|
Some providers require a free account or API key.
|
|
@@ -198,7 +197,7 @@ Providers have different pricing models. pi-free handles them all:
|
|
|
198
197
|
|
|
199
198
|
**Provider types:**
|
|
200
199
|
|
|
201
|
-
- ✅ **Free providers** (
|
|
200
|
+
- ✅ **Free providers** (Kilo, Cline) — Toggle between free-only vs paid models
|
|
202
201
|
- 🔄 **Freemium** (NVIDIA, Ollama) — Free tier with limits, toggle shows all
|
|
203
202
|
- 🔧 **Dynamic API** (Mistral, Groq, Cerebras, xAI) — Fetched when API key configured, toggle filters the list
|
|
204
203
|
|
|
@@ -213,17 +212,6 @@ Authentication is handled automatically:
|
|
|
213
212
|
|
|
214
213
|
## Using Free Models (No Setup Required)
|
|
215
214
|
|
|
216
|
-
### OpenCode
|
|
217
|
-
|
|
218
|
-
Works immediately with zero setup:
|
|
219
|
-
|
|
220
|
-
1. Press `Ctrl+L`
|
|
221
|
-
2. Search for `opencode/`
|
|
222
|
-
3. Pick any model (e.g., `opencode/big-pickle`)
|
|
223
|
-
4. Start chatting
|
|
224
|
-
|
|
225
|
-
No account, no API key, no OAuth. Run `/toggle-opencode` to switch between free and paid OpenCode models.
|
|
226
|
-
|
|
227
215
|
### Kilo (free models, more after login)
|
|
228
216
|
|
|
229
217
|
Kilo shows free models immediately. To unlock all models, authenticate with Kilo's free OAuth:
|
|
@@ -442,28 +430,27 @@ export SAMBANOVA_API_KEY="..."
|
|
|
442
430
|
|
|
443
431
|
Each provider has toggle commands to switch between free and all models:
|
|
444
432
|
|
|
445
|
-
| Command
|
|
446
|
-
|
|
|
447
|
-
| `/toggle-
|
|
448
|
-
| `/toggle-
|
|
449
|
-
| `/toggle-
|
|
450
|
-
| `/toggle-
|
|
451
|
-
| `/toggle-
|
|
452
|
-
| `/toggle-
|
|
453
|
-
| `/toggle-
|
|
454
|
-
| `/toggle-
|
|
455
|
-
| `/toggle-
|
|
456
|
-
| `/toggle-
|
|
457
|
-
| `/toggle-
|
|
458
|
-
| `/toggle-
|
|
459
|
-
| `/toggle-
|
|
460
|
-
| `/toggle-
|
|
461
|
-
| `/toggle-
|
|
462
|
-
| `/toggle-
|
|
463
|
-
| `/toggle-
|
|
464
|
-
| `/
|
|
465
|
-
| `/ollama
|
|
466
|
-
| `/probe-ollama` | Test Ollama Cloud models for 403 errors (auto-hide) |
|
|
433
|
+
| Command | Action |
|
|
434
|
+
| ----------------------- | -------------------------------------------------------- |
|
|
435
|
+
| `/toggle-kilo` | Toggle between free/all Kilo models |
|
|
436
|
+
| `/toggle-openrouter` | Toggle between free/all OpenRouter models |
|
|
437
|
+
| `/toggle-cline` | Toggle between free/all Cline models |
|
|
438
|
+
| `/toggle-nvidia` | Toggle between free/all NVIDIA models |
|
|
439
|
+
| `/toggle-ollama` | Toggle between free/all Ollama Cloud models |
|
|
440
|
+
| `/toggle-mistral` | Toggle between free/all Mistral models (🔧 dynamic) |
|
|
441
|
+
| `/toggle-groq` | Toggle between free/all Groq models (🔧 dynamic) |
|
|
442
|
+
| `/toggle-cerebras` | Toggle between free/all Cerebras models (🔧 dynamic) |
|
|
443
|
+
| `/toggle-xai` | Toggle between free/all xAI models (🔧 dynamic) |
|
|
444
|
+
| `/toggle-huggingface` | Toggle between free/all Hugging Face models (🔧 dynamic) |
|
|
445
|
+
| `/toggle-codestral` | Toggle Codestral (💳 paid) |
|
|
446
|
+
| `/toggle-deepinfra` | Toggle DeepInfra (💳 trial credit) |
|
|
447
|
+
| `/toggle-together` | Toggle Together AI (💳 trial credit) |
|
|
448
|
+
| `/toggle-sambanova` | Toggle SambaNova (🔄 freemium) |
|
|
449
|
+
| `/toggle-llm7` | Toggle LLM7 (✅ free gateway) |
|
|
450
|
+
| `/toggle-zenmux` | Toggle ZenMux (💳 paid) |
|
|
451
|
+
| `/toggle-crofai` | Toggle CrofAI (💳 paid) |
|
|
452
|
+
| `/ollama-cloud-refresh` | Re-fetch Ollama Cloud models live (no restart needed) |
|
|
453
|
+
| `/probe-ollama` | Test Ollama Cloud models for 403 errors (auto-hide) |
|
|
467
454
|
|
|
468
455
|
**The toggle command:**
|
|
469
456
|
|
|
@@ -471,7 +458,7 @@ Each provider has toggle commands to switch between free and all models:
|
|
|
471
458
|
- **For 🔄 freemium providers**: Shows all models by default; toggle switches between filtered and full list
|
|
472
459
|
- **For 🔧 dynamic API providers**: Filters the model list when you have an API key configured
|
|
473
460
|
- **Persists your preference** to `~/.pi/free.json` for next startup
|
|
474
|
-
|
|
461
|
+
|
|
475
462
|
|
|
476
463
|
### Probe Commands (Health Check)
|
|
477
464
|
|
package/banner.svg
CHANGED
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<text x="20" y="70" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">Ollama Cloud · ZenMux</text>
|
|
111
111
|
<text x="20" y="90" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">CrofAI · Codestral · LLM7</text>
|
|
112
112
|
<text x="20" y="110" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">DeepInfra · SambaNova</text>
|
|
113
|
-
<text x="20" y="130" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">Together</text>
|
|
113
|
+
<text x="20" y="130" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">Together · Novita · FastRouter</text>
|
|
114
114
|
</g>
|
|
115
115
|
|
|
116
116
|
<g transform="translate(970, 55)">
|
package/config.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface PiFreeConfig {
|
|
|
32
32
|
deepinfra_api_key?: string;
|
|
33
33
|
sambanova_api_key?: string;
|
|
34
34
|
together_api_key?: string;
|
|
35
|
+
novita_api_key?: string;
|
|
36
|
+
fastrouter_api_key?: string;
|
|
35
37
|
kilo_free_only?: boolean;
|
|
36
38
|
hidden_models?: string[];
|
|
37
39
|
free_only?: boolean;
|
|
@@ -45,6 +47,8 @@ interface PiFreeConfig {
|
|
|
45
47
|
deepinfra_show_paid?: boolean;
|
|
46
48
|
sambanova_show_paid?: boolean;
|
|
47
49
|
together_show_paid?: boolean;
|
|
50
|
+
novita_show_paid?: boolean;
|
|
51
|
+
fastrouter_show_paid?: boolean;
|
|
48
52
|
openrouter_show_paid?: boolean;
|
|
49
53
|
opencode_show_paid?: boolean;
|
|
50
54
|
}
|
|
@@ -59,6 +63,8 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
59
63
|
deepinfra_api_key: "",
|
|
60
64
|
sambanova_api_key: "",
|
|
61
65
|
together_api_key: "",
|
|
66
|
+
novita_api_key: "",
|
|
67
|
+
fastrouter_api_key: "",
|
|
62
68
|
|
|
63
69
|
kilo_free_only: false,
|
|
64
70
|
hidden_models: [],
|
|
@@ -73,6 +79,8 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
73
79
|
deepinfra_show_paid: false,
|
|
74
80
|
sambanova_show_paid: false,
|
|
75
81
|
together_show_paid: false,
|
|
82
|
+
novita_show_paid: false,
|
|
83
|
+
fastrouter_show_paid: false,
|
|
76
84
|
openrouter_show_paid: false,
|
|
77
85
|
opencode_show_paid: false,
|
|
78
86
|
};
|
|
@@ -210,6 +218,17 @@ export function getTogetherShowPaid(): boolean {
|
|
|
210
218
|
return resolveBool("TOGETHER_SHOW_PAID", loadConfigFile().together_show_paid);
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
export function getNovitaShowPaid(): boolean {
|
|
222
|
+
return resolveBool("NOVITA_SHOW_PAID", loadConfigFile().novita_show_paid);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function getFastrouterShowPaid(): boolean {
|
|
226
|
+
return resolveBool(
|
|
227
|
+
"FASTROUTER_SHOW_PAID",
|
|
228
|
+
loadConfigFile().fastrouter_show_paid,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
213
232
|
export function getOllamaShowPaid(): boolean {
|
|
214
233
|
return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
|
|
215
234
|
}
|
|
@@ -225,6 +244,41 @@ export function getOpencodeShowPaid(): boolean {
|
|
|
225
244
|
return resolveBool("OPENCODE_SHOW_PAID", loadConfigFile().opencode_show_paid);
|
|
226
245
|
}
|
|
227
246
|
|
|
247
|
+
export function getProviderShowPaid(providerId: string): boolean {
|
|
248
|
+
switch (providerId) {
|
|
249
|
+
case "kilo":
|
|
250
|
+
return getKiloShowPaid();
|
|
251
|
+
case "cline":
|
|
252
|
+
return getClineShowPaid();
|
|
253
|
+
case "zenmux":
|
|
254
|
+
return getZenmuxShowPaid();
|
|
255
|
+
case "crofai":
|
|
256
|
+
return getCrofaiShowPaid();
|
|
257
|
+
case "codestral":
|
|
258
|
+
return getCodestralShowPaid();
|
|
259
|
+
case "llm7":
|
|
260
|
+
return getLlm7ShowPaid();
|
|
261
|
+
case "deepinfra":
|
|
262
|
+
return getDeepinfraShowPaid();
|
|
263
|
+
case "sambanova":
|
|
264
|
+
return getSambanovaShowPaid();
|
|
265
|
+
case "together":
|
|
266
|
+
return getTogetherShowPaid();
|
|
267
|
+
case "novita":
|
|
268
|
+
return getNovitaShowPaid();
|
|
269
|
+
case "fastrouter":
|
|
270
|
+
return getFastrouterShowPaid();
|
|
271
|
+
case "ollama-cloud":
|
|
272
|
+
return getOllamaShowPaid();
|
|
273
|
+
case "openrouter":
|
|
274
|
+
return getOpenrouterShowPaid();
|
|
275
|
+
case "opencode":
|
|
276
|
+
return getOpencodeShowPaid();
|
|
277
|
+
default:
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
228
282
|
// =============================================================================
|
|
229
283
|
// Global free-only mode
|
|
230
284
|
// =============================================================================
|
|
@@ -273,6 +327,14 @@ export function getTogetherApiKey(): string | undefined {
|
|
|
273
327
|
return resolve("TOGETHER_AI_API_KEY", loadConfigFile().together_api_key);
|
|
274
328
|
}
|
|
275
329
|
|
|
330
|
+
export function getNovitaApiKey(): string | undefined {
|
|
331
|
+
return resolve("NOVITA_API_KEY", loadConfigFile().novita_api_key);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function getFastrouterApiKey(): string | undefined {
|
|
335
|
+
return resolve("FASTROUTER_API_KEY", loadConfigFile().fastrouter_api_key);
|
|
336
|
+
}
|
|
337
|
+
|
|
276
338
|
export function getOllamaApiKey(): string | undefined {
|
|
277
339
|
return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
|
|
278
340
|
}
|
|
@@ -302,12 +364,47 @@ export function getHfToken(): string | undefined {
|
|
|
302
364
|
return process.env.HF_TOKEN;
|
|
303
365
|
}
|
|
304
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Read an API key from ~/.pi/agent/auth.json.
|
|
369
|
+
* Pi stores built-in provider keys there (opencode, openrouter, etc.).
|
|
370
|
+
* Falls back to env var if auth.json is missing or key not found.
|
|
371
|
+
*/
|
|
372
|
+
function readAuthJsonKey(
|
|
373
|
+
providerId: string,
|
|
374
|
+
envVar: string,
|
|
375
|
+
): string | undefined {
|
|
376
|
+
// Check env var first (fast path)
|
|
377
|
+
const envVal = process.env[envVar];
|
|
378
|
+
if (envVal) return envVal;
|
|
379
|
+
|
|
380
|
+
// Check auth.json
|
|
381
|
+
try {
|
|
382
|
+
const authPath = join(PI_DIR, "agent", "auth.json");
|
|
383
|
+
if (!existsSync(authPath)) return undefined;
|
|
384
|
+
const raw = readFileSync(authPath, "utf8");
|
|
385
|
+
const auth = JSON.parse(raw) as Record<
|
|
386
|
+
string,
|
|
387
|
+
{ type?: string; key?: string }
|
|
388
|
+
>;
|
|
389
|
+
const entry = auth[providerId];
|
|
390
|
+
if (entry?.key?.trim()) return entry.key;
|
|
391
|
+
} catch {
|
|
392
|
+
// auth.json missing or corrupt — silently skip
|
|
393
|
+
}
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
305
397
|
/**
|
|
306
398
|
* OpenRouter key — pi's built-in provider reads from ~/.pi/agent/auth.json.
|
|
307
|
-
* pi-free
|
|
399
|
+
* pi-free checks env var first, then auth.json.
|
|
308
400
|
*/
|
|
309
401
|
export function getOpenrouterApiKey(): string | undefined {
|
|
310
|
-
return
|
|
402
|
+
return readAuthJsonKey("openrouter", "OPENROUTER_API_KEY");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** OpenCode key — pi's built-in provider. Read from env or auth.json. */
|
|
406
|
+
export function getOpencodeApiKey(): string | undefined {
|
|
407
|
+
return readAuthJsonKey("opencode", "OPENCODE_API_KEY");
|
|
311
408
|
}
|
|
312
409
|
|
|
313
410
|
// =============================================================================
|
package/constants.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const PROVIDER_LLM7 = "llm7";
|
|
|
22
22
|
export const PROVIDER_DEEPINFRA = "deepinfra";
|
|
23
23
|
export const PROVIDER_SAMBANOVA = "sambanova";
|
|
24
24
|
export const PROVIDER_TOGETHER = "together";
|
|
25
|
+
export const PROVIDER_NOVITA = "novita";
|
|
25
26
|
|
|
26
27
|
export const ALL_UNIQUE_PROVIDERS = [
|
|
27
28
|
PROVIDER_KILO,
|
|
@@ -38,6 +39,7 @@ export const ALL_UNIQUE_PROVIDERS = [
|
|
|
38
39
|
PROVIDER_DEEPINFRA,
|
|
39
40
|
PROVIDER_SAMBANOVA,
|
|
40
41
|
PROVIDER_TOGETHER,
|
|
42
|
+
PROVIDER_NOVITA,
|
|
41
43
|
] as const;
|
|
42
44
|
|
|
43
45
|
// =============================================================================
|
|
@@ -59,6 +61,7 @@ export const BASE_URL_LLM7 = "https://api.llm7.io/v1";
|
|
|
59
61
|
export const BASE_URL_DEEPINFRA = "https://api.deepinfra.com/v1/openai";
|
|
60
62
|
export const BASE_URL_SAMBANOVA = "https://api.sambanova.ai/v1";
|
|
61
63
|
export const BASE_URL_TOGETHER = "https://api.together.xyz/v1";
|
|
64
|
+
export const BASE_URL_NOVITA = "https://api.novita.ai/openai/v1";
|
|
62
65
|
|
|
63
66
|
/** Cline fetches free models from OpenRouter */
|
|
64
67
|
export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
|
|
@@ -84,7 +87,6 @@ export const CLINE_AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
84
87
|
// =============================================================================
|
|
85
88
|
|
|
86
89
|
export const NVIDIA_MIN_SIZE_B = 70; // Minimum model size for NVIDIA NIM
|
|
87
|
-
export const DEFAULT_MIN_SIZE_B = 30; // Default minimum model size for filtering
|
|
88
90
|
|
|
89
91
|
// =============================================================================
|
|
90
92
|
// Timeouts (milliseconds)
|
package/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import llm7 from "./providers/llm7/llm7.ts";
|
|
|
40
40
|
import deepinfra from "./providers/deepinfra/deepinfra.ts";
|
|
41
41
|
import sambanova from "./providers/sambanova/sambanova.ts";
|
|
42
42
|
import together from "./providers/together/together.ts";
|
|
43
|
+
import novita from "./providers/novita/novita.ts";
|
|
43
44
|
import nvidia from "./providers/nvidia/nvidia.ts";
|
|
44
45
|
import ollama from "./providers/ollama/ollama.ts";
|
|
45
46
|
import zenmux from "./providers/zenmux/zenmux.ts";
|
|
@@ -57,7 +58,7 @@ function setupGlobalCommands(pi: ExtensionAPI) {
|
|
|
57
58
|
handler: async (_args, ctx) => {
|
|
58
59
|
const current = getGlobalFreeOnly();
|
|
59
60
|
const next = !current;
|
|
60
|
-
applyGlobalFilter(pi, next);
|
|
61
|
+
applyGlobalFilter(pi, next, { force: true });
|
|
61
62
|
|
|
62
63
|
const registry = getProviderRegistry();
|
|
63
64
|
const providerCount = registry.size;
|
|
@@ -210,10 +211,11 @@ export default async function piFreeEntry(pi: ExtensionAPI) {
|
|
|
210
211
|
deepinfra(pi),
|
|
211
212
|
sambanova(pi),
|
|
212
213
|
together(pi),
|
|
214
|
+
novita(pi),
|
|
213
215
|
]);
|
|
214
216
|
|
|
215
|
-
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face
|
|
216
|
-
//
|
|
217
|
+
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face,
|
|
218
|
+
// OpenRouter/OpenCode from Pi auth, and FastRouter public model discovery)
|
|
217
219
|
const { setupDynamicBuiltInProviders } = await import(
|
|
218
220
|
"./providers/dynamic-built-in/index.ts"
|
|
219
221
|
);
|
package/lib/built-in-toggle.ts
CHANGED
|
@@ -18,11 +18,24 @@ import type {
|
|
|
18
18
|
} from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { getOpencodeShowPaid, getOpenrouterShowPaid } from "../config.ts";
|
|
20
20
|
import { createLogger } from "./logger.ts";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getProviderRegistry,
|
|
23
|
+
isFreeModel,
|
|
24
|
+
registerWithGlobalToggle,
|
|
25
|
+
} from "./registry.ts";
|
|
22
26
|
import { createToggleState } from "./toggle-state.ts";
|
|
27
|
+
import {
|
|
28
|
+
OPENCODE_DYNAMIC_API,
|
|
29
|
+
createOpenCodeSessionTracker,
|
|
30
|
+
createOpenCodeStreamSimple,
|
|
31
|
+
isOpenCodeProvider,
|
|
32
|
+
} from "../providers/opencode-session.ts";
|
|
23
33
|
|
|
24
34
|
const _logger = createLogger("built-in-toggle");
|
|
25
35
|
|
|
36
|
+
// OpenCode requires per-request ids; see createOpenCodeStreamSimple().
|
|
37
|
+
const _opencodeSession = createOpenCodeSessionTracker();
|
|
38
|
+
|
|
26
39
|
// =============================================================================
|
|
27
40
|
// Configuration
|
|
28
41
|
// =============================================================================
|
|
@@ -34,6 +47,7 @@ interface BuiltInToggleConfig {
|
|
|
34
47
|
|
|
35
48
|
const BUILT_IN_TOGGLE_PROVIDERS: BuiltInToggleConfig[] = [
|
|
36
49
|
{ id: "opencode", getShowPaid: getOpencodeShowPaid },
|
|
50
|
+
{ id: "opencode-go", getShowPaid: getOpencodeShowPaid },
|
|
37
51
|
{ id: "openrouter", getShowPaid: getOpenrouterShowPaid },
|
|
38
52
|
];
|
|
39
53
|
|
|
@@ -55,68 +69,38 @@ let commandsRegistered = false;
|
|
|
55
69
|
// =============================================================================
|
|
56
70
|
|
|
57
71
|
export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
72
|
+
const activeConfigs = BUILT_IN_TOGGLE_PROVIDERS.filter(
|
|
73
|
+
(config) => !getProviderRegistry().has(config.id),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (activeConfigs.length === 0) {
|
|
77
|
+
_logger.info(
|
|
78
|
+
"[built-in-toggle] OpenCode/OpenRouter already registered dynamically; skipping fallback capture",
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
// Register toggle commands once (available even before models load)
|
|
59
84
|
if (!commandsRegistered) {
|
|
60
|
-
for (const config of
|
|
85
|
+
for (const config of activeConfigs) {
|
|
61
86
|
registerToggleCommand(pi, config);
|
|
62
87
|
}
|
|
63
|
-
setupStatusBar(pi);
|
|
88
|
+
setupStatusBar(pi, activeConfigs);
|
|
64
89
|
commandsRegistered = true;
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
// Capture built-in models on session start and apply initial filter
|
|
68
93
|
pi.on("session_start", async (_event, ctx) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
|
|
94
|
+
for (const config of activeConfigs) {
|
|
72
95
|
if (providerStates.has(config.id)) {
|
|
73
|
-
// Already captured
|
|
96
|
+
// Already captured — skip to avoid re-registering
|
|
74
97
|
continue;
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
if (providerModels.length === 0) continue;
|
|
81
|
-
|
|
82
|
-
const allModels = providerModels.map(modelToProviderConfig);
|
|
83
|
-
const freeModels = allModels.filter((m) =>
|
|
84
|
-
isFreeModel({ ...m, provider: config.id }, allModels),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const baseUrl = providerModels[0].baseUrl;
|
|
88
|
-
const api = providerModels[0].api;
|
|
89
|
-
const apiKeyEnv = getApiKeyEnvForProvider(config.id);
|
|
90
|
-
|
|
91
|
-
const reRegister = (models: ProviderModelConfig[]) => {
|
|
92
|
-
pi.registerProvider(config.id, {
|
|
93
|
-
baseUrl,
|
|
94
|
-
apiKey: apiKeyEnv,
|
|
95
|
-
api,
|
|
96
|
-
models,
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const stored = { free: freeModels, all: allModels };
|
|
101
|
-
const toggleState = createToggleState<ProviderModelConfig>({
|
|
102
|
-
providerId: config.id,
|
|
103
|
-
initialShowPaid: config.getShowPaid(),
|
|
104
|
-
initialModels: stored,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
providerStates.set(config.id, {
|
|
108
|
-
stored,
|
|
109
|
-
reRegister,
|
|
110
|
-
toggleState,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
registerWithGlobalToggle(config.id, stored, reRegister, true);
|
|
100
|
+
const state = tryCaptureProvider(pi, config, ctx);
|
|
101
|
+
if (!state) continue;
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const applied = toggleState.applyCurrent(reRegister);
|
|
103
|
+
const applied = state.toggleState.applyCurrent(state.reRegister);
|
|
120
104
|
_logger.info(
|
|
121
105
|
`[built-in-toggle] ${config.id}: applied ${applied.mode} mode with ${applied.models.length} models`,
|
|
122
106
|
);
|
|
@@ -124,6 +108,63 @@ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
|
124
108
|
});
|
|
125
109
|
}
|
|
126
110
|
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// On-demand model capture (called by toggle command when state is missing)
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
function tryCaptureProvider(
|
|
116
|
+
pi: ExtensionAPI,
|
|
117
|
+
config: BuiltInToggleConfig,
|
|
118
|
+
ctx: any,
|
|
119
|
+
): BuiltInProviderState | undefined {
|
|
120
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
121
|
+
const providerModels = available.filter(
|
|
122
|
+
(m: Model<Api>) => m.provider === config.id,
|
|
123
|
+
);
|
|
124
|
+
if (providerModels.length === 0) return undefined;
|
|
125
|
+
|
|
126
|
+
const allModels = providerModels.map((m: Model<Api>) =>
|
|
127
|
+
modelToProviderConfig(m, config.id),
|
|
128
|
+
);
|
|
129
|
+
const freeModels = allModels.filter((m: ProviderModelConfig) =>
|
|
130
|
+
isFreeModel({ ...m, provider: config.id }, allModels),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const baseUrl = providerModels[0].baseUrl;
|
|
134
|
+
const api = providerModels[0].api;
|
|
135
|
+
const apiKeyEnv = getApiKeyEnvForProvider(config.id);
|
|
136
|
+
|
|
137
|
+
const reRegister = (models: ProviderModelConfig[]) => {
|
|
138
|
+
pi.registerProvider(config.id, {
|
|
139
|
+
baseUrl,
|
|
140
|
+
apiKey: apiKeyEnv,
|
|
141
|
+
api: isOpenCodeProvider(config.id) ? OPENCODE_DYNAMIC_API : api,
|
|
142
|
+
...(isOpenCodeProvider(config.id)
|
|
143
|
+
? { streamSimple: createOpenCodeStreamSimple(_opencodeSession) }
|
|
144
|
+
: {}),
|
|
145
|
+
models,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const stored = { free: freeModels, all: allModels };
|
|
150
|
+
const toggleState = createToggleState<ProviderModelConfig>({
|
|
151
|
+
providerId: config.id,
|
|
152
|
+
initialShowPaid: config.getShowPaid(),
|
|
153
|
+
initialModels: stored,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const state: BuiltInProviderState = { stored, reRegister, toggleState };
|
|
157
|
+
providerStates.set(config.id, state);
|
|
158
|
+
|
|
159
|
+
registerWithGlobalToggle(config.id, stored, reRegister, true);
|
|
160
|
+
|
|
161
|
+
_logger.info(
|
|
162
|
+
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
// =============================================================================
|
|
128
169
|
// Per-provider toggle command
|
|
129
170
|
// =============================================================================
|
|
@@ -136,13 +177,17 @@ function registerToggleCommand(
|
|
|
136
177
|
pi.registerCommand(commandName, {
|
|
137
178
|
description: `Toggle free/paid ${config.id} models`,
|
|
138
179
|
handler: async (_args, ctx) => {
|
|
139
|
-
|
|
180
|
+
let state = providerStates.get(config.id);
|
|
140
181
|
if (!state) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
182
|
+
// Models may have loaded after session_start — try on-demand capture
|
|
183
|
+
state = tryCaptureProvider(pi, config, ctx);
|
|
184
|
+
if (!state) {
|
|
185
|
+
ctx.ui.notify(
|
|
186
|
+
`${config.id}: models not loaded yet. Start a session first.`,
|
|
187
|
+
"warning",
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
const applied = state.toggleState.toggle(state.reRegister);
|
|
@@ -166,8 +211,11 @@ function registerToggleCommand(
|
|
|
166
211
|
// Helpers
|
|
167
212
|
// =============================================================================
|
|
168
213
|
|
|
169
|
-
function modelToProviderConfig(
|
|
170
|
-
|
|
214
|
+
function modelToProviderConfig(
|
|
215
|
+
m: Model<Api>,
|
|
216
|
+
providerId?: string,
|
|
217
|
+
): ProviderModelConfig {
|
|
218
|
+
const base: ProviderModelConfig = {
|
|
171
219
|
id: m.id,
|
|
172
220
|
name: m.name,
|
|
173
221
|
api: m.api,
|
|
@@ -179,18 +227,29 @@ function modelToProviderConfig(m: Model<Api>): ProviderModelConfig {
|
|
|
179
227
|
headers: m.headers,
|
|
180
228
|
compat: (m as any).compat,
|
|
181
229
|
};
|
|
230
|
+
|
|
231
|
+
// Use a custom OpenCode API wrapper so per-request headers are regenerated
|
|
232
|
+
// for every LLM call instead of being frozen at registration time.
|
|
233
|
+
if (providerId && isOpenCodeProvider(providerId)) {
|
|
234
|
+
base.api = OPENCODE_DYNAMIC_API;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return base;
|
|
182
238
|
}
|
|
183
239
|
|
|
184
240
|
// =============================================================================
|
|
185
241
|
// Status bar for provider selection
|
|
186
242
|
// =============================================================================
|
|
187
243
|
|
|
188
|
-
function setupStatusBar(
|
|
244
|
+
function setupStatusBar(
|
|
245
|
+
pi: ExtensionAPI,
|
|
246
|
+
configs: BuiltInToggleConfig[],
|
|
247
|
+
): void {
|
|
189
248
|
pi.on("model_select", (_event, ctx) => {
|
|
190
249
|
const selected = _event.model?.provider;
|
|
191
250
|
|
|
192
|
-
// Clear status for all built-in
|
|
193
|
-
for (const config of
|
|
251
|
+
// Clear status for all fallback-captured built-in providers
|
|
252
|
+
for (const config of configs) {
|
|
194
253
|
if (selected !== config.id) {
|
|
195
254
|
ctx.ui.setStatus(`${config.id}-status`, undefined);
|
|
196
255
|
}
|
|
@@ -220,6 +279,7 @@ function setupStatusBar(pi: ExtensionAPI): void {
|
|
|
220
279
|
function getApiKeyEnvForProvider(providerId: string): string {
|
|
221
280
|
const envMap: Record<string, string> = {
|
|
222
281
|
opencode: "OPENCODE_API_KEY",
|
|
282
|
+
"opencode-go": "OPENCODE_API_KEY",
|
|
223
283
|
openrouter: "OPENROUTER_API_KEY",
|
|
224
284
|
};
|
|
225
285
|
return envMap[providerId] || `${providerId.toUpperCase()}_API_KEY`;
|