pi-free 2.0.9 → 2.0.11
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 +576 -544
- package/README.md +16 -0
- package/banner.svg +12 -10
- package/config.ts +86 -20
- package/constants.ts +3 -0
- package/index.ts +3 -0
- package/lib/util.ts +72 -8
- package/package.json +1 -1
- package/providers/crofai/crofai.ts +106 -15
- package/providers/deepinfra/deepinfra.ts +108 -11
- package/providers/ollama/ollama.ts +400 -85
- package/providers/ollama/thinking-levels.ts +96 -0
- package/providers/together/together.ts +197 -0
- package/providers/zenmux/zenmux.ts +32 -17
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ Want to see paid models too? Run the toggle command for your provider:
|
|
|
97
97
|
/toggle-crofai # Toggle CrofAI (💳 paid - needs API key with credits)
|
|
98
98
|
/toggle-codestral # Toggle Codestral (💳 paid - free Experiment plan)
|
|
99
99
|
/toggle-deepinfra # Toggle DeepInfra (💳 trial credit provider)
|
|
100
|
+
/toggle-together # Toggle Together AI (💳 trial credit provider)
|
|
100
101
|
/toggle-sambanova # Toggle SambaNova (🔄 freemium)
|
|
101
102
|
/toggle-llm7 # Toggle LLM7 (✅ free gateway)
|
|
102
103
|
```
|
|
@@ -412,6 +413,18 @@ AI inference cloud with 100+ open-source models:
|
|
|
412
413
|
export DEEPINFRA_TOKEN="..."
|
|
413
414
|
```
|
|
414
415
|
|
|
416
|
+
### Together AI ($1 trial credit)
|
|
417
|
+
|
|
418
|
+
Fast inference on 200+ open-source models:
|
|
419
|
+
|
|
420
|
+
- $1 one-time credit on signup (no credit card)
|
|
421
|
+
- 138 chat models (Llama, DeepSeek, Qwen, Mixtral, etc.)
|
|
422
|
+
- 60 RPM, 600 RPD (varies by model)
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
export TOGETHER_AI_API_KEY="..."
|
|
426
|
+
```
|
|
427
|
+
|
|
415
428
|
### SambaNova Cloud (free tier)
|
|
416
429
|
|
|
417
430
|
Fast inference on custom RDU hardware:
|
|
@@ -444,10 +457,13 @@ Each provider has toggle commands to switch between free and all models:
|
|
|
444
457
|
| `/toggle-huggingface` | Toggle between free/all Hugging Face models (🔧 dynamic) |
|
|
445
458
|
| `/toggle-codestral` | Toggle Codestral (💳 paid) |
|
|
446
459
|
| `/toggle-deepinfra` | Toggle DeepInfra (💳 trial credit) |
|
|
460
|
+
| `/toggle-together` | Toggle Together AI (💳 trial credit) |
|
|
447
461
|
| `/toggle-sambanova` | Toggle SambaNova (🔄 freemium) |
|
|
448
462
|
| `/toggle-llm7` | Toggle LLM7 (✅ free gateway) |
|
|
449
463
|
| `/toggle-zenmux` | Toggle ZenMux (💳 paid) |
|
|
450
464
|
| `/toggle-crofai` | Toggle CrofAI (💳 paid) |
|
|
465
|
+
| `/ollama-cloud-refresh` | Re-fetch Ollama Cloud models live (no restart needed) |
|
|
466
|
+
| `/probe-ollama` | Test Ollama Cloud models for 403 errors (auto-hide) |
|
|
451
467
|
|
|
452
468
|
**The toggle command:**
|
|
453
469
|
|
package/banner.svg
CHANGED
|
@@ -106,10 +106,11 @@
|
|
|
106
106
|
<rect x="0" y="0" width="250" height="170" rx="12" fill="url(#card1)" stroke="#7c3aed" stroke-width="0.5" stroke-opacity="0.2" filter="url(#shadow)"/>
|
|
107
107
|
<rect x="0" y="0" width="250" height="170" rx="12" fill="none" stroke="url(#accent)" stroke-width="0.5" opacity="0.1"/>
|
|
108
108
|
<text x="20" y="28" font-family="system-ui, sans-serif" font-size="13" font-weight="700" fill="#c4b5fd">Custom Providers</text>
|
|
109
|
-
<text x="20" y="
|
|
110
|
-
<text x="20" y="
|
|
111
|
-
<text x="20" y="
|
|
112
|
-
<text x="20" y="
|
|
109
|
+
<text x="20" y="50" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">Kilo · Cline · NVIDIA</text>
|
|
110
|
+
<text x="20" y="70" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">Ollama Cloud · ZenMux</text>
|
|
111
|
+
<text x="20" y="90" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">CrofAI · Codestral · LLM7</text>
|
|
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
114
|
</g>
|
|
114
115
|
|
|
115
116
|
<g transform="translate(970, 55)">
|
|
@@ -117,12 +118,13 @@
|
|
|
117
118
|
<rect x="0" y="0" width="250" height="170" rx="12" fill="url(#card2)" stroke="#06b6d4" stroke-width="0.5" stroke-opacity="0.2" filter="url(#shadow)"/>
|
|
118
119
|
<rect x="0" y="0" width="250" height="170" rx="12" fill="none" stroke="url(#accent2)" stroke-width="0.5" opacity="0.1"/>
|
|
119
120
|
<text x="20" y="28" font-family="system-ui, sans-serif" font-size="13" font-weight="700" fill="#67e8f9">Features</text>
|
|
120
|
-
<text x="20" y="
|
|
121
|
-
<text x="20" y="
|
|
122
|
-
<text x="20" y="
|
|
123
|
-
<text x="20" y="
|
|
124
|
-
<text x="20" y="
|
|
125
|
-
<text x="20" y="
|
|
121
|
+
<text x="20" y="50" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ Free model auto-detection</text>
|
|
122
|
+
<text x="20" y="68" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ Per-provider toggles</text>
|
|
123
|
+
<text x="20" y="86" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ OAuth flows (Kilo, Cline)</text>
|
|
124
|
+
<text x="20" y="104" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ Coding Index (CI) scores</text>
|
|
125
|
+
<text x="20" y="122" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ Model health probes</text>
|
|
126
|
+
<text x="20" y="140" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ Thinking level maps</text>
|
|
127
|
+
<text x="20" y="158" font-family="system-ui, sans-serif" font-size="11" fill="#8b9aaa">✦ 404/403 auto-hide on probe</text>
|
|
126
128
|
</g>
|
|
127
129
|
|
|
128
130
|
<!-- Bottom tagline -->
|
package/config.ts
CHANGED
|
@@ -28,14 +28,10 @@ interface PiFreeConfig {
|
|
|
28
28
|
zenmux_api_key?: string;
|
|
29
29
|
crofai_api_key?: string;
|
|
30
30
|
codestral_api_key?: string;
|
|
31
|
-
mistral_api_key?: string;
|
|
32
31
|
llm7_api_key?: string;
|
|
33
32
|
deepinfra_api_key?: string;
|
|
34
33
|
sambanova_api_key?: string;
|
|
35
|
-
|
|
36
|
-
cerebras_api_key?: string;
|
|
37
|
-
xai_api_key?: string;
|
|
38
|
-
hf_token?: string;
|
|
34
|
+
together_api_key?: string;
|
|
39
35
|
kilo_free_only?: boolean;
|
|
40
36
|
hidden_models?: string[];
|
|
41
37
|
free_only?: boolean;
|
|
@@ -48,6 +44,7 @@ interface PiFreeConfig {
|
|
|
48
44
|
llm7_show_paid?: boolean;
|
|
49
45
|
deepinfra_show_paid?: boolean;
|
|
50
46
|
sambanova_show_paid?: boolean;
|
|
47
|
+
together_show_paid?: boolean;
|
|
51
48
|
openrouter_show_paid?: boolean;
|
|
52
49
|
opencode_show_paid?: boolean;
|
|
53
50
|
}
|
|
@@ -58,14 +55,10 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
58
55
|
zenmux_api_key: "",
|
|
59
56
|
crofai_api_key: "",
|
|
60
57
|
codestral_api_key: "",
|
|
61
|
-
mistral_api_key: "",
|
|
62
58
|
llm7_api_key: "",
|
|
63
59
|
deepinfra_api_key: "",
|
|
64
60
|
sambanova_api_key: "",
|
|
65
|
-
|
|
66
|
-
cerebras_api_key: "",
|
|
67
|
-
xai_api_key: "",
|
|
68
|
-
hf_token: "",
|
|
61
|
+
together_api_key: "",
|
|
69
62
|
|
|
70
63
|
kilo_free_only: false,
|
|
71
64
|
hidden_models: [],
|
|
@@ -79,6 +72,7 @@ const CONFIG_TEMPLATE: PiFreeConfig = {
|
|
|
79
72
|
llm7_show_paid: false,
|
|
80
73
|
deepinfra_show_paid: false,
|
|
81
74
|
sambanova_show_paid: false,
|
|
75
|
+
together_show_paid: false,
|
|
82
76
|
openrouter_show_paid: false,
|
|
83
77
|
opencode_show_paid: false,
|
|
84
78
|
};
|
|
@@ -90,9 +84,21 @@ function ensureConfigFile(): void {
|
|
|
90
84
|
try {
|
|
91
85
|
mkdirSync(PI_DIR, { recursive: true });
|
|
92
86
|
if (existsSync(CONFIG_PATH)) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
let existing: PiFreeConfig;
|
|
88
|
+
try {
|
|
89
|
+
existing = JSON.parse(
|
|
90
|
+
readFileSync(CONFIG_PATH, "utf8"),
|
|
91
|
+
) as PiFreeConfig;
|
|
92
|
+
} catch (_parseErr) {
|
|
93
|
+
// File exists but is corrupt — DO NOT overwrite it.
|
|
94
|
+
// The user needs to fix or delete it manually.
|
|
95
|
+
_logger.error(
|
|
96
|
+
"Config file exists but is corrupt — refusing to overwrite. Fix or delete ~/.pi/free.json.",
|
|
97
|
+
{ path: CONFIG_PATH },
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Merge with template to add any missing keys, preserving existing values
|
|
96
102
|
const merged = { ...CONFIG_TEMPLATE, ...existing };
|
|
97
103
|
if (JSON.stringify(merged) !== JSON.stringify(existing)) {
|
|
98
104
|
writeFileSync(
|
|
@@ -120,7 +126,7 @@ export function loadConfigFile(): PiFreeConfig {
|
|
|
120
126
|
try {
|
|
121
127
|
return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as PiFreeConfig;
|
|
122
128
|
} catch (err) {
|
|
123
|
-
_logger.
|
|
129
|
+
_logger.error("Could not parse config file — returning empty config", {
|
|
124
130
|
path: CONFIG_PATH,
|
|
125
131
|
error: err instanceof Error ? err.message : String(err),
|
|
126
132
|
});
|
|
@@ -128,6 +134,18 @@ export function loadConfigFile(): PiFreeConfig {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Read the raw config file content without merging with template.
|
|
139
|
+
* Returns the file content as string, or undefined if unreadable.
|
|
140
|
+
*/
|
|
141
|
+
function readRawConfigFile(): string | undefined {
|
|
142
|
+
try {
|
|
143
|
+
return readFileSync(CONFIG_PATH, "utf8");
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
ensureConfigFile();
|
|
132
150
|
|
|
133
151
|
// Resolve each value: env var takes priority over config file.
|
|
@@ -188,6 +206,10 @@ export function getSambanovaShowPaid(): boolean {
|
|
|
188
206
|
);
|
|
189
207
|
}
|
|
190
208
|
|
|
209
|
+
export function getTogetherShowPaid(): boolean {
|
|
210
|
+
return resolveBool("TOGETHER_SHOW_PAID", loadConfigFile().together_show_paid);
|
|
211
|
+
}
|
|
212
|
+
|
|
191
213
|
export function getOllamaShowPaid(): boolean {
|
|
192
214
|
return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
|
|
193
215
|
}
|
|
@@ -247,28 +269,37 @@ export function getSambanovaApiKey(): string | undefined {
|
|
|
247
269
|
return resolve("SAMBANOVA_API_KEY", loadConfigFile().sambanova_api_key);
|
|
248
270
|
}
|
|
249
271
|
|
|
272
|
+
export function getTogetherApiKey(): string | undefined {
|
|
273
|
+
return resolve("TOGETHER_AI_API_KEY", loadConfigFile().together_api_key);
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
export function getOllamaApiKey(): string | undefined {
|
|
251
277
|
return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
|
|
252
278
|
}
|
|
253
279
|
|
|
280
|
+
/** Mistral is pi's built-in provider — key comes from env var only. */
|
|
254
281
|
export function getMistralApiKey(): string | undefined {
|
|
255
|
-
return
|
|
282
|
+
return process.env.MISTRAL_API_KEY;
|
|
256
283
|
}
|
|
257
284
|
|
|
285
|
+
/** Groq is pi's built-in provider — key comes from env var only. */
|
|
258
286
|
export function getGroqApiKey(): string | undefined {
|
|
259
|
-
return
|
|
287
|
+
return process.env.GROQ_API_KEY;
|
|
260
288
|
}
|
|
261
289
|
|
|
290
|
+
/** Cerebras is pi's built-in provider — key comes from env var only. */
|
|
262
291
|
export function getCerebrasApiKey(): string | undefined {
|
|
263
|
-
return
|
|
292
|
+
return process.env.CEREBRAS_API_KEY;
|
|
264
293
|
}
|
|
265
294
|
|
|
295
|
+
/** xAI is pi's built-in provider — key comes from env var only. */
|
|
266
296
|
export function getXaiApiKey(): string | undefined {
|
|
267
|
-
return
|
|
297
|
+
return process.env.XAI_API_KEY;
|
|
268
298
|
}
|
|
269
299
|
|
|
300
|
+
/** HuggingFace is pi's built-in provider — token comes from env var only. */
|
|
270
301
|
export function getHfToken(): string | undefined {
|
|
271
|
-
return
|
|
302
|
+
return process.env.HF_TOKEN;
|
|
272
303
|
}
|
|
273
304
|
|
|
274
305
|
/**
|
|
@@ -315,7 +346,42 @@ export function applyHidden<T extends { id: string }>(
|
|
|
315
346
|
|
|
316
347
|
export function saveConfig(updates: Partial<PiFreeConfig>): void {
|
|
317
348
|
try {
|
|
318
|
-
|
|
349
|
+
// Read the raw file content — never use loadConfigFile() here because
|
|
350
|
+
// if the file is unparseable, loadConfigFile() returns {} which would
|
|
351
|
+
// cause us to write a partial config and WIPE all existing keys.
|
|
352
|
+
const raw = readRawConfigFile();
|
|
353
|
+
if (raw === undefined) {
|
|
354
|
+
// File doesn't exist or can't be read — start from template
|
|
355
|
+
const merged = { ...CONFIG_TEMPLATE, ...updates };
|
|
356
|
+
writeFileSync(
|
|
357
|
+
CONFIG_PATH,
|
|
358
|
+
`${JSON.stringify(merged, null, 2)}\n`,
|
|
359
|
+
"utf8",
|
|
360
|
+
);
|
|
361
|
+
_logger.info("Config saved (new file)", {
|
|
362
|
+
path: CONFIG_PATH,
|
|
363
|
+
keys: Object.keys(updates),
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let existing: PiFreeConfig;
|
|
369
|
+
try {
|
|
370
|
+
existing = JSON.parse(raw) as PiFreeConfig;
|
|
371
|
+
} catch (parseErr) {
|
|
372
|
+
// File exists but is corrupt. REFUSE to overwrite it with a partial
|
|
373
|
+
// config — that would permanently destroy the user's keys.
|
|
374
|
+
_logger.error(
|
|
375
|
+
"REFUSING to save config — existing file is corrupt. Fix or delete ~/.pi/free.json manually.",
|
|
376
|
+
{
|
|
377
|
+
path: CONFIG_PATH,
|
|
378
|
+
error:
|
|
379
|
+
parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
319
385
|
const merged = { ...existing, ...updates };
|
|
320
386
|
writeFileSync(CONFIG_PATH, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
321
387
|
_logger.info("Config saved", {
|
package/constants.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const PROVIDER_CODESTRAL = "codestral";
|
|
|
21
21
|
export const PROVIDER_LLM7 = "llm7";
|
|
22
22
|
export const PROVIDER_DEEPINFRA = "deepinfra";
|
|
23
23
|
export const PROVIDER_SAMBANOVA = "sambanova";
|
|
24
|
+
export const PROVIDER_TOGETHER = "together";
|
|
24
25
|
|
|
25
26
|
export const ALL_UNIQUE_PROVIDERS = [
|
|
26
27
|
PROVIDER_KILO,
|
|
@@ -36,6 +37,7 @@ export const ALL_UNIQUE_PROVIDERS = [
|
|
|
36
37
|
PROVIDER_LLM7,
|
|
37
38
|
PROVIDER_DEEPINFRA,
|
|
38
39
|
PROVIDER_SAMBANOVA,
|
|
40
|
+
PROVIDER_TOGETHER,
|
|
39
41
|
] as const;
|
|
40
42
|
|
|
41
43
|
// =============================================================================
|
|
@@ -56,6 +58,7 @@ export const BASE_URL_CODESTRAL = "https://codestral.mistral.ai/v1";
|
|
|
56
58
|
export const BASE_URL_LLM7 = "https://api.llm7.io/v1";
|
|
57
59
|
export const BASE_URL_DEEPINFRA = "https://api.deepinfra.com/v1/openai";
|
|
58
60
|
export const BASE_URL_SAMBANOVA = "https://api.sambanova.ai/v1";
|
|
61
|
+
export const BASE_URL_TOGETHER = "https://api.together.xyz/v1";
|
|
59
62
|
|
|
60
63
|
/** Cline fetches free models from OpenRouter */
|
|
61
64
|
export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
|
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - Codestral: Mistral's code-focused model via codestral.mistral.ai (free tier)
|
|
14
14
|
* - DeepInfra: AI inference cloud ($5 trial credit)
|
|
15
15
|
* - SambaNova: Fast inference on RDU hardware (free tier, no credit card)
|
|
16
|
+
* - Together: Fast inference on 200+ open-source models ($1 trial credit)
|
|
16
17
|
* - LLM7: AI gateway (free default/fast selectors)
|
|
17
18
|
*/
|
|
18
19
|
|
|
@@ -38,6 +39,7 @@ import kilo from "./providers/kilo/kilo.ts";
|
|
|
38
39
|
import llm7 from "./providers/llm7/llm7.ts";
|
|
39
40
|
import deepinfra from "./providers/deepinfra/deepinfra.ts";
|
|
40
41
|
import sambanova from "./providers/sambanova/sambanova.ts";
|
|
42
|
+
import together from "./providers/together/together.ts";
|
|
41
43
|
import nvidia from "./providers/nvidia/nvidia.ts";
|
|
42
44
|
import ollama from "./providers/ollama/ollama.ts";
|
|
43
45
|
import zenmux from "./providers/zenmux/zenmux.ts";
|
|
@@ -207,6 +209,7 @@ export default async function piFreeEntry(pi: ExtensionAPI) {
|
|
|
207
209
|
llm7(pi),
|
|
208
210
|
deepinfra(pi),
|
|
209
211
|
sambanova(pi),
|
|
212
|
+
together(pi),
|
|
210
213
|
]);
|
|
211
214
|
|
|
212
215
|
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face)
|
package/lib/util.ts
CHANGED
|
@@ -384,12 +384,32 @@ export interface OpenAIModelDefaults {
|
|
|
384
384
|
|
|
385
385
|
/**
|
|
386
386
|
* Generic model shape returned by OpenAI-compatible /v1/models endpoints.
|
|
387
|
+
*
|
|
388
|
+
* Some providers (SambaNova, DeepInfra) return extended fields beyond
|
|
389
|
+
* the standard OpenAI format. We accept them loosely and use what's
|
|
390
|
+
* available, falling back to defaults otherwise.
|
|
387
391
|
*/
|
|
388
392
|
export interface OpenAIModelEntry {
|
|
389
393
|
id: string;
|
|
390
394
|
object?: string;
|
|
391
395
|
created?: number;
|
|
392
396
|
owned_by?: string;
|
|
397
|
+
/** Extended: per-model reasoning capability (some providers expose this) */
|
|
398
|
+
reasoning?: boolean;
|
|
399
|
+
/** Extended: input modalities (some providers expose this) */
|
|
400
|
+
input_modalities?: string[];
|
|
401
|
+
/** Extended: per-model context length (SambaNova, etc.) */
|
|
402
|
+
context_length?: number;
|
|
403
|
+
/** Extended: alternate field name for context length */
|
|
404
|
+
max_context_length?: number;
|
|
405
|
+
/** Extended: alternate field name for context length (snake_case) */
|
|
406
|
+
context_window?: number;
|
|
407
|
+
/** Extended: per-model max completion tokens (SambaNova, etc.) */
|
|
408
|
+
max_completion_tokens?: number;
|
|
409
|
+
/** Extended: alternate field name for max tokens */
|
|
410
|
+
max_tokens?: number;
|
|
411
|
+
/** Extended: per-model pricing (SambaNova, etc.) */
|
|
412
|
+
pricing?: { prompt?: string | number; completion?: string | number };
|
|
393
413
|
}
|
|
394
414
|
|
|
395
415
|
/**
|
|
@@ -426,8 +446,10 @@ export async function fetchOpenAICompatibleModels(
|
|
|
426
446
|
throw new Error(`${providerId} API error: ${response.status}`);
|
|
427
447
|
}
|
|
428
448
|
|
|
429
|
-
const
|
|
430
|
-
|
|
449
|
+
const body = (await response.json()) as
|
|
450
|
+
| OpenAIModelEntry[]
|
|
451
|
+
| { data?: OpenAIModelEntry[] };
|
|
452
|
+
const models = Array.isArray(body) ? body : (body.data ?? []);
|
|
431
453
|
|
|
432
454
|
logger.info(`[${providerId}] Fetched ${models.length} models`);
|
|
433
455
|
|
|
@@ -435,19 +457,61 @@ export async function fetchOpenAICompatibleModels(
|
|
|
435
457
|
.filter((m) => m.id)
|
|
436
458
|
.map((m): PiProviderModelConfig => {
|
|
437
459
|
const name = m.id.split("/").pop() || m.id;
|
|
460
|
+
|
|
461
|
+
// Use per-model context length if the API provides it (try multiple field names)
|
|
462
|
+
const contextWindow =
|
|
463
|
+
m.context_length ??
|
|
464
|
+
m.max_context_length ??
|
|
465
|
+
m.context_window ??
|
|
466
|
+
defaults.contextWindow ??
|
|
467
|
+
128_000;
|
|
468
|
+
|
|
469
|
+
// Use per-model max tokens if the API provides it (try multiple field names)
|
|
470
|
+
const maxTokens =
|
|
471
|
+
m.max_completion_tokens ??
|
|
472
|
+
m.max_tokens ??
|
|
473
|
+
defaults.maxTokens ??
|
|
474
|
+
4_096;
|
|
475
|
+
|
|
476
|
+
// Use per-model reasoning flag if the API provides it
|
|
477
|
+
const reasoning =
|
|
478
|
+
m.reasoning ?? isLikelyReasoningModel({ id: m.id, name });
|
|
479
|
+
|
|
480
|
+
// Use per-model input_modalities if the API provides it
|
|
481
|
+
const hasVision = m.input_modalities?.includes("image") ?? false;
|
|
482
|
+
const input =
|
|
483
|
+
(defaults.input as PiProviderModelConfig["input"]) ??
|
|
484
|
+
(hasVision ? ["text", "image"] : ["text"]);
|
|
485
|
+
|
|
486
|
+
// Use per-model pricing if the API provides it, otherwise use defaults
|
|
487
|
+
const inputCost =
|
|
488
|
+
(typeof m.pricing?.prompt === "number" ||
|
|
489
|
+
typeof m.pricing?.prompt === "string"
|
|
490
|
+
? Number(m.pricing.prompt)
|
|
491
|
+
: undefined) ??
|
|
492
|
+
defaults.cost?.input ??
|
|
493
|
+
0;
|
|
494
|
+
const outputCost =
|
|
495
|
+
(typeof m.pricing?.completion === "number" ||
|
|
496
|
+
typeof m.pricing?.completion === "string"
|
|
497
|
+
? Number(m.pricing.completion)
|
|
498
|
+
: undefined) ??
|
|
499
|
+
defaults.cost?.output ??
|
|
500
|
+
0;
|
|
501
|
+
|
|
438
502
|
return {
|
|
439
503
|
id: m.id,
|
|
440
504
|
name,
|
|
441
|
-
reasoning
|
|
442
|
-
input
|
|
505
|
+
reasoning,
|
|
506
|
+
input,
|
|
443
507
|
cost: {
|
|
444
|
-
input:
|
|
445
|
-
output:
|
|
508
|
+
input: inputCost,
|
|
509
|
+
output: outputCost,
|
|
446
510
|
cacheRead: 0,
|
|
447
511
|
cacheWrite: 0,
|
|
448
512
|
},
|
|
449
|
-
contextWindow
|
|
450
|
-
maxTokens
|
|
513
|
+
contextWindow,
|
|
514
|
+
maxTokens,
|
|
451
515
|
compat: getProxyModelCompat({ id: m.id, name }),
|
|
452
516
|
};
|
|
453
517
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI model providers for Pi with free model filtering. Shows only $0 cost models by default. Supports Kilo (free OAuth), Cline (free), NVIDIA (freemium), ZenMux, CrofAI, Ollama Cloud, and more.",
|
|
6
6
|
"keywords": [
|
|
@@ -1,30 +1,128 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CrofAI Provider Extension
|
|
3
3
|
*
|
|
4
|
-
* Provides access to CrofAI API - OpenAI-compatible LLM inference service
|
|
4
|
+
* Provides access to CrofAI API - OpenAI-compatible LLM inference service
|
|
5
|
+
* hosting DeepSeek, Qwen, and other open-source models.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: CrofAI's /v1/models returns per-model context_length, max_completion_tokens,
|
|
8
|
+
* name, custom_reasoning, and reasoning_effort. Pricing is per-MILLION tokens.
|
|
5
9
|
*
|
|
6
10
|
* Setup:
|
|
7
11
|
* 1. Get API key from https://ai.nahcrof.com
|
|
8
12
|
* 2. Set CROFAI_API_KEY env var or add to ~/.pi/free.json
|
|
9
13
|
*
|
|
10
|
-
* Responds to global free-only filter.
|
|
11
|
-
*
|
|
12
14
|
* Usage:
|
|
13
15
|
* pi install git:github.com/apmantza/pi-free
|
|
14
16
|
* # Set CROFAI_API_KEY env var
|
|
15
17
|
* # Models appear in /model selector
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
ExtensionAPI,
|
|
22
|
+
ProviderModelConfig,
|
|
23
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
24
|
import { getCrofaiApiKey, getCrofaiShowPaid } from "../../config.ts";
|
|
20
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
BASE_URL_CROFAI,
|
|
27
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
28
|
+
PROVIDER_CROFAI,
|
|
29
|
+
} from "../../constants.ts";
|
|
21
30
|
import { createLogger } from "../../lib/logger.ts";
|
|
31
|
+
import {
|
|
32
|
+
getProxyModelCompat,
|
|
33
|
+
isLikelyReasoningModel,
|
|
34
|
+
} from "../../lib/provider-compat.ts";
|
|
22
35
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
23
|
-
import {
|
|
36
|
+
import { fetchWithRetry } from "../../lib/util.ts";
|
|
24
37
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
25
38
|
|
|
26
39
|
const _logger = createLogger("crofai");
|
|
27
40
|
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Types
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
interface CrofaiModel {
|
|
46
|
+
id: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
context_length?: number;
|
|
49
|
+
max_completion_tokens?: number;
|
|
50
|
+
custom_reasoning?: boolean;
|
|
51
|
+
reasoning_effort?: boolean;
|
|
52
|
+
pricing?: {
|
|
53
|
+
prompt?: string;
|
|
54
|
+
completion?: string;
|
|
55
|
+
cache_prompt?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Fetch
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
function parseCrofaiPrice(priceStr: string | undefined): number {
|
|
64
|
+
if (priceStr === undefined) return 0;
|
|
65
|
+
const num = Number.parseFloat(priceStr);
|
|
66
|
+
if (Number.isNaN(num)) return 0;
|
|
67
|
+
// CrofAI pricing is per-MILLION tokens. Divide to get per-token (Pi convention).
|
|
68
|
+
return num / 1_000_000;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchCrofaiModels(
|
|
72
|
+
apiKey: string,
|
|
73
|
+
): Promise<ProviderModelConfig[]> {
|
|
74
|
+
const response = await fetchWithRetry(
|
|
75
|
+
`${BASE_URL_CROFAI}/models`,
|
|
76
|
+
{
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: `Bearer ${apiKey}`,
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
3,
|
|
83
|
+
1000,
|
|
84
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`CrofAI API error: ${response.status} ${response.statusText}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// CrofAI returns { data: [...] }
|
|
94
|
+
const json = (await response.json()) as {
|
|
95
|
+
data?: CrofaiModel[];
|
|
96
|
+
};
|
|
97
|
+
const models = json.data ?? [];
|
|
98
|
+
|
|
99
|
+
_logger.info(`[crofai] Fetched ${models.length} models`);
|
|
100
|
+
|
|
101
|
+
return models
|
|
102
|
+
.filter((m) => m.id)
|
|
103
|
+
.map((m): ProviderModelConfig => {
|
|
104
|
+
const name = m.name || m.id;
|
|
105
|
+
const reasoning =
|
|
106
|
+
m.custom_reasoning ?? isLikelyReasoningModel({ id: m.id, name });
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: m.id,
|
|
110
|
+
name,
|
|
111
|
+
reasoning,
|
|
112
|
+
input: ["text"],
|
|
113
|
+
cost: {
|
|
114
|
+
input: parseCrofaiPrice(m.pricing?.prompt),
|
|
115
|
+
output: parseCrofaiPrice(m.pricing?.completion),
|
|
116
|
+
cacheRead: parseCrofaiPrice(m.pricing?.cache_prompt),
|
|
117
|
+
cacheWrite: 0,
|
|
118
|
+
},
|
|
119
|
+
contextWindow: m.context_length ?? 128_000,
|
|
120
|
+
maxTokens: m.max_completion_tokens ?? 16_384,
|
|
121
|
+
compat: getProxyModelCompat({ id: m.id, name }),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
28
126
|
// =============================================================================
|
|
29
127
|
// Extension Entry Point
|
|
30
128
|
// =============================================================================
|
|
@@ -39,21 +137,14 @@ export default async function crofaiProvider(pi: ExtensionAPI) {
|
|
|
39
137
|
return;
|
|
40
138
|
}
|
|
41
139
|
|
|
42
|
-
// Fetch models
|
|
43
|
-
const allModels = await
|
|
44
|
-
"crofai",
|
|
45
|
-
BASE_URL_CROFAI,
|
|
46
|
-
apiKey,
|
|
47
|
-
);
|
|
140
|
+
// Fetch models
|
|
141
|
+
const allModels = await fetchCrofaiModels(apiKey);
|
|
48
142
|
|
|
49
143
|
if (allModels.length === 0) {
|
|
50
144
|
_logger.warn("[crofai] No models available");
|
|
51
145
|
return;
|
|
52
146
|
}
|
|
53
147
|
|
|
54
|
-
// Use isFreeModel with allModels for proper detection
|
|
55
|
-
// CrofAI doesn't expose pricing (all costs are $0), so Route B will be used:
|
|
56
|
-
// FREE only if "free" in name
|
|
57
148
|
const freeModels = allModels.filter((m) =>
|
|
58
149
|
isFreeModel({ ...m, provider: PROVIDER_CROFAI }, allModels),
|
|
59
150
|
);
|