pi-free 1.0.9 → 2.0.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +393 -367
  3. package/config.ts +170 -121
  4. package/constants.ts +23 -61
  5. package/index.ts +148 -0
  6. package/lib/built-in-toggle.ts +206 -0
  7. package/lib/json-persistence.ts +11 -10
  8. package/lib/logger.ts +2 -2
  9. package/lib/model-enhancer.ts +20 -20
  10. package/lib/open-browser.ts +41 -0
  11. package/lib/provider-cache.ts +106 -0
  12. package/lib/registry.ts +144 -0
  13. package/package.json +8 -23
  14. package/provider-factory.ts +25 -41
  15. package/provider-failover/benchmark-lookup.ts +247 -0
  16. package/provider-failover/benchmarks-chunk-0.ts +2010 -0
  17. package/provider-failover/benchmarks-chunk-1.ts +1988 -0
  18. package/provider-failover/benchmarks-chunk-2.ts +2010 -0
  19. package/provider-failover/benchmarks-chunk-3.ts +2010 -0
  20. package/provider-failover/benchmarks-chunk-4.ts +1969 -0
  21. package/provider-failover/hardcoded-benchmarks.ts +22 -10025
  22. package/provider-helper.ts +260 -259
  23. package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
  24. package/providers/cline/cline-models.ts +128 -0
  25. package/providers/{cline.ts → cline/cline.ts} +298 -257
  26. package/providers/cloudflare/cloudflare.ts +368 -0
  27. package/providers/dynamic-built-in/index.ts +432 -0
  28. package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
  29. package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
  30. package/providers/kilo/kilo.ts +235 -0
  31. package/providers/{modal.ts → modal/modal.ts} +4 -3
  32. package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
  33. package/providers/ollama/ollama.ts +172 -0
  34. package/providers/opencode-session.ts +34 -34
  35. package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
  36. package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
  37. package/providers/{qwen.ts → qwen/qwen.ts} +83 -13
  38. package/provider-failover/auto-switch.ts +0 -350
  39. package/provider-failover/errors.ts +0 -275
  40. package/provider-failover/index.ts +0 -238
  41. package/providers/cline-models.ts +0 -77
  42. package/providers/factory.ts +0 -125
  43. package/providers/fireworks.ts +0 -49
  44. package/providers/go.ts +0 -216
  45. package/providers/kilo.ts +0 -146
  46. package/providers/mistral.ts +0 -144
  47. package/providers/ollama.ts +0 -113
  48. package/providers/openrouter.ts +0 -175
  49. package/providers/zen.ts +0 -371
  50. package/usage/commands.ts +0 -17
  51. package/usage/cumulative.ts +0 -193
  52. package/usage/formatters.ts +0 -115
  53. package/usage/index.ts +0 -46
  54. package/usage/limits.ts +0 -148
  55. package/usage/metrics.ts +0 -222
  56. package/usage/sessions.ts +0 -355
  57. package/usage/store.ts +0 -99
  58. package/usage/tracking.ts +0 -329
  59. package/usage/types.ts +0 -26
  60. package/usage/widget.ts +0 -90
  61. package/widget/data.ts +0 -113
  62. package/widget/format.ts +0 -26
  63. package/widget/render.ts +0 -117
package/config.ts CHANGED
@@ -5,67 +5,75 @@
5
5
  * 1. Environment variable
6
6
  * 2. ~/.pi/free.json
7
7
  *
8
- * Per-provider paid model flags:
9
- * KILO_SHOW_PAID=true or kilo_show_paid: true
10
- * OPENROUTER_SHOW_PAID=true or openrouter_show_paid: true
11
- * NVIDIA_SHOW_PAID=true or nvidia_show_paid: true
12
- * FIREWORKS_SHOW_PAID=true or fireworks_show_paid: true
13
- * CLINE_SHOW_PAID=true or cline_show_paid: true
14
- * GO_SHOW_PAID=true or go_show_paid: true
15
- * OLLAMA_SHOW_PAID=true or ollama_show_paid: true
16
- *
17
- * PI_FREE_KILO_FREE_ONLY=true — restrict Kilo to free models even after login.
8
+ * All exported values are getter functions so that runtime changes
9
+ * (e.g. after toggle-{provider}) are visible immediately.
18
10
  */
19
11
 
20
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
13
  import { join } from "node:path";
14
+ import {
15
+ PROVIDER_CLINE,
16
+ PROVIDER_KILO,
17
+ PROVIDER_MODAL,
18
+ PROVIDER_NVIDIA,
19
+ PROVIDER_QWEN,
20
+ } from "./constants.ts";
22
21
  import { createLogger } from "./lib/logger.ts";
23
22
 
24
23
  const _logger = createLogger("config");
25
24
 
26
25
  interface PiFreeConfig {
27
- openrouter_api_key?: string;
28
26
  nvidia_api_key?: string;
29
- opencode_api_key?: string;
30
- opencode_go_api_key?: string;
31
- fireworks_api_key?: string;
32
- mistral_api_key?: string;
27
+ cloudflare_api_token?: string;
28
+ cloudflare_account_id?: string;
33
29
  ollama_api_key?: string;
34
30
  modal_api_key?: string;
31
+ opencode_api_key?: string;
32
+ mistral_api_key?: string;
33
+ groq_api_key?: string;
34
+ cerebras_api_key?: string;
35
+ xai_api_key?: string;
36
+ hf_token?: string;
37
+ openrouter_api_key?: string;
35
38
  kilo_free_only?: boolean;
36
39
  hidden_models?: string[];
37
- // Per-provider paid model flags
40
+ free_only?: boolean;
38
41
  kilo_show_paid?: boolean;
39
- openrouter_show_paid?: boolean;
40
42
  nvidia_show_paid?: boolean;
41
- fireworks_show_paid?: boolean;
42
- cline_show_paid?: boolean;
43
- zen_show_paid?: boolean;
44
- go_show_paid?: boolean;
45
- mistral_show_paid?: boolean;
43
+ cloudflare_show_paid?: boolean;
46
44
  ollama_show_paid?: boolean;
45
+ cline_show_paid?: boolean;
46
+ qwen_show_paid?: boolean;
47
+ modal_show_paid?: boolean;
48
+ openrouter_show_paid?: boolean;
49
+ opencode_show_paid?: boolean;
47
50
  }
48
51
 
49
52
  const CONFIG_TEMPLATE: PiFreeConfig = {
50
- openrouter_api_key: "",
51
53
  nvidia_api_key: "",
52
- opencode_api_key: "",
53
- opencode_go_api_key: "",
54
- fireworks_api_key: "",
55
- mistral_api_key: "",
54
+ cloudflare_api_token: "",
55
+ cloudflare_account_id: "",
56
56
  ollama_api_key: "",
57
57
  modal_api_key: "",
58
+ opencode_api_key: "",
59
+ mistral_api_key: "",
60
+ groq_api_key: "",
61
+ cerebras_api_key: "",
62
+ xai_api_key: "",
63
+ hf_token: "",
64
+ openrouter_api_key: "",
58
65
  kilo_free_only: false,
59
66
  hidden_models: [],
67
+ free_only: true,
60
68
  kilo_show_paid: false,
61
- openrouter_show_paid: false,
62
69
  nvidia_show_paid: false,
63
- fireworks_show_paid: false,
64
- cline_show_paid: false,
65
- zen_show_paid: false,
66
- go_show_paid: false,
67
- mistral_show_paid: false,
70
+ cloudflare_show_paid: false,
68
71
  ollama_show_paid: false,
72
+ cline_show_paid: false,
73
+ qwen_show_paid: false,
74
+ modal_show_paid: false,
75
+ openrouter_show_paid: false,
76
+ opencode_show_paid: false,
69
77
  };
70
78
 
71
79
  const PI_DIR = join(process.env.HOME || process.env.USERPROFILE || "", ".pi");
@@ -75,7 +83,6 @@ function ensureConfigFile(): void {
75
83
  try {
76
84
  mkdirSync(PI_DIR, { recursive: true });
77
85
  if (existsSync(CONFIG_PATH)) {
78
- // Merge: add any new template keys without touching existing values
79
86
  const existing = JSON.parse(
80
87
  readFileSync(CONFIG_PATH, "utf8"),
81
88
  ) as PiFreeConfig;
@@ -102,25 +109,26 @@ function ensureConfigFile(): void {
102
109
  }
103
110
  }
104
111
 
105
- function loadConfigFile(): PiFreeConfig {
112
+ export function loadConfigFile(): PiFreeConfig {
106
113
  try {
107
114
  return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as PiFreeConfig;
108
- } catch {
115
+ } catch (err) {
116
+ _logger.warn("Could not parse config file — returning empty config", {
117
+ path: CONFIG_PATH,
118
+ error: err instanceof Error ? err.message : String(err),
119
+ });
109
120
  return {};
110
121
  }
111
122
  }
112
123
 
113
124
  ensureConfigFile();
114
- const file = loadConfigFile();
115
125
 
116
126
  // Resolve each value: env var takes priority over config file.
117
- // Treat empty strings in the config file as unset.
118
127
  function resolve(envKey: string, fileVal?: string): string | undefined {
119
128
  return process.env[envKey] || (fileVal?.trim() ? fileVal : undefined);
120
129
  }
121
130
 
122
131
  // Resolve boolean flag: env var takes priority, then config file.
123
- // If neither is set, defaults to false (free-only mode).
124
132
  function resolveBool(envKey: string, fileVal?: boolean): boolean {
125
133
  const envValue = process.env[envKey];
126
134
  if (envValue === "true") return true;
@@ -128,104 +136,134 @@ function resolveBool(envKey: string, fileVal?: boolean): boolean {
128
136
  return fileVal === true;
129
137
  }
130
138
 
131
- // Global fallback (deprecated, use per-provider flags)
132
- // Returns true only if explicitly enabled via env var
133
- export const SHOW_PAID = process.env.PI_FREE_SHOW_PAID === "true";
139
+ // =============================================================================
140
+ // Per-provider paid-model flags (getters so toggles reflect immediately)
141
+ // =============================================================================
134
142
 
135
- // Per-provider paid model flags - default to false (free-only) if not set
136
- export const KILO_SHOW_PAID = resolveBool(
137
- "KILO_SHOW_PAID",
138
- file.kilo_show_paid,
139
- );
143
+ export function getKiloShowPaid(): boolean {
144
+ return resolveBool("KILO_SHOW_PAID", loadConfigFile().kilo_show_paid);
145
+ }
140
146
 
141
- export const OPENROUTER_SHOW_PAID = resolveBool(
142
- "OPENROUTER_SHOW_PAID",
143
- file.openrouter_show_paid,
144
- );
147
+ export function getNvidiaShowPaid(): boolean {
148
+ return resolveBool("NVIDIA_SHOW_PAID", loadConfigFile().nvidia_show_paid);
149
+ }
145
150
 
146
- export const NVIDIA_SHOW_PAID = resolveBool(
147
- "NVIDIA_SHOW_PAID",
148
- file.nvidia_show_paid,
149
- );
151
+ export function getClineShowPaid(): boolean {
152
+ return resolveBool("CLINE_SHOW_PAID", loadConfigFile().cline_show_paid);
153
+ }
150
154
 
151
- export const FIREWORKS_SHOW_PAID = resolveBool(
152
- "FIREWORKS_SHOW_PAID",
153
- file.fireworks_show_paid,
154
- );
155
+ /** @deprecated Qwen provider is deprecated. */
156
+ export function getQwenShowPaid(): boolean {
157
+ return resolveBool("QWEN_SHOW_PAID", loadConfigFile().qwen_show_paid);
158
+ }
155
159
 
156
- export const CLINE_SHOW_PAID = resolveBool(
157
- "CLINE_SHOW_PAID",
158
- file.cline_show_paid,
159
- );
160
+ export function getModalShowPaid(): boolean {
161
+ return resolveBool("MODAL_SHOW_PAID", loadConfigFile().modal_show_paid);
162
+ }
160
163
 
161
- export const ZEN_SHOW_PAID = resolveBool("ZEN_SHOW_PAID", file.zen_show_paid);
164
+ export function getOllamaShowPaid(): boolean {
165
+ return resolveBool("OLLAMA_SHOW_PAID", loadConfigFile().ollama_show_paid);
166
+ }
162
167
 
163
- export const GO_SHOW_PAID = resolveBool("GO_SHOW_PAID", file.go_show_paid);
168
+ export function getCloudflareShowPaid(): boolean {
169
+ return resolveBool(
170
+ "CLOUDFLARE_SHOW_PAID",
171
+ loadConfigFile().cloudflare_show_paid,
172
+ );
173
+ }
164
174
 
165
- export const MISTRAL_SHOW_PAID = resolveBool(
166
- "MISTRAL_SHOW_PAID",
167
- file.mistral_show_paid,
168
- );
175
+ export function getOpenrouterShowPaid(): boolean {
176
+ return resolveBool(
177
+ "OPENROUTER_SHOW_PAID",
178
+ loadConfigFile().openrouter_show_paid,
179
+ );
180
+ }
169
181
 
170
- export const OLLAMA_SHOW_PAID = resolveBool(
171
- "OLLAMA_SHOW_PAID",
172
- file.ollama_show_paid,
173
- );
182
+ export function getOpencodeShowPaid(): boolean {
183
+ return resolveBool("OPENCODE_SHOW_PAID", loadConfigFile().opencode_show_paid);
184
+ }
174
185
 
175
- export const KILO_FREE_ONLY = resolveBool(
176
- "PI_FREE_KILO_FREE_ONLY",
177
- file.kilo_free_only,
178
- );
186
+ // =============================================================================
187
+ // Global free-only mode
188
+ // =============================================================================
179
189
 
180
- const HIDDEN: Set<string> = new Set(file.hidden_models ?? []);
190
+ export function getFreeOnly(): boolean {
191
+ return resolveBool("PI_FREE_ONLY", loadConfigFile().free_only);
192
+ }
181
193
 
182
- /** Removes any models whose id appears in hidden_models. */
183
- export function applyHidden<T extends { id: string }>(models: T[]): T[] {
184
- if (HIDDEN.size === 0) return models;
185
- return models.filter((m) => !HIDDEN.has(m.id));
186
- }
187
-
188
- export const OPENROUTER_API_KEY = resolve(
189
- "OPENROUTER_API_KEY",
190
- file.openrouter_api_key,
191
- );
192
- export const NVIDIA_API_KEY = resolve("NVIDIA_API_KEY", file.nvidia_api_key);
193
- export const OPENCODE_API_KEY = resolve(
194
- "OPENCODE_API_KEY",
195
- file.opencode_api_key,
196
- );
197
- export const OPENCODE_GO_API_KEY = resolve(
198
- "OPENCODE_GO_API_KEY",
199
- file.opencode_go_api_key,
200
- );
201
- export const FIREWORKS_API_KEY = resolve(
202
- "FIREWORKS_API_KEY",
203
- file.fireworks_api_key,
204
- );
205
- export const MISTRAL_API_KEY = resolve("MISTRAL_API_KEY", file.mistral_api_key);
206
- export const OLLAMA_API_KEY = resolve("OLLAMA_API_KEY", file.ollama_api_key);
207
- export const MODAL_API_KEY = resolve("MODAL_API_KEY", file.modal_api_key);
194
+ export function getKiloFreeOnly(): boolean {
195
+ return resolveBool("PI_FREE_KILO_FREE_ONLY", loadConfigFile().kilo_free_only);
196
+ }
208
197
 
209
- // Re-export provider names for consistency
210
- export {
211
- PROVIDER_CLINE,
212
- PROVIDER_FIREWORKS,
213
- PROVIDER_GO,
214
- PROVIDER_KILO,
215
- PROVIDER_MISTRAL,
216
- PROVIDER_NVIDIA,
217
- PROVIDER_OLLAMA,
218
- PROVIDER_OPENROUTER,
219
- PROVIDER_QWEN,
220
- PROVIDER_ZEN,
221
- PROVIDER_MODAL,
222
- } from "./constants.ts";
198
+ // =============================================================================
199
+ // API Keys (getters so runtime config changes are visible)
200
+ // =============================================================================
201
+
202
+ export function getNvidiaApiKey(): string | undefined {
203
+ return resolve("NVIDIA_API_KEY", loadConfigFile().nvidia_api_key);
204
+ }
205
+
206
+ export function getModalApiKey(): string | undefined {
207
+ return resolve("MODAL_API_KEY", loadConfigFile().modal_api_key);
208
+ }
209
+
210
+ export function getOllamaApiKey(): string | undefined {
211
+ return resolve("OLLAMA_API_KEY", loadConfigFile().ollama_api_key);
212
+ }
213
+
214
+ export function getCloudflareApiToken(): string | undefined {
215
+ return resolve("CLOUDFLARE_API_TOKEN", loadConfigFile().cloudflare_api_token);
216
+ }
217
+
218
+ export function getCloudflareAccountId(): string | undefined {
219
+ return resolve(
220
+ "CLOUDFLARE_ACCOUNT_ID",
221
+ loadConfigFile().cloudflare_account_id,
222
+ );
223
+ }
224
+
225
+ export function getOpencodeApiKey(): string | undefined {
226
+ return resolve("OPENCODE_API_KEY", loadConfigFile().opencode_api_key);
227
+ }
228
+
229
+ export function getMistralApiKey(): string | undefined {
230
+ return resolve("MISTRAL_API_KEY", loadConfigFile().mistral_api_key);
231
+ }
232
+
233
+ export function getGroqApiKey(): string | undefined {
234
+ return resolve("GROQ_API_KEY", loadConfigFile().groq_api_key);
235
+ }
236
+
237
+ export function getCerebrasApiKey(): string | undefined {
238
+ return resolve("CEREBRAS_API_KEY", loadConfigFile().cerebras_api_key);
239
+ }
240
+
241
+ export function getXaiApiKey(): string | undefined {
242
+ return resolve("XAI_API_KEY", loadConfigFile().xai_api_key);
243
+ }
244
+
245
+ export function getHfToken(): string | undefined {
246
+ return resolve("HF_TOKEN", loadConfigFile().hf_token);
247
+ }
248
+
249
+ export function getOpenrouterApiKey(): string | undefined {
250
+ return resolve("OPENROUTER_API_KEY", loadConfigFile().openrouter_api_key);
251
+ }
252
+
253
+ // =============================================================================
254
+ // Hidden models (re-reads config on every call)
255
+ // =============================================================================
256
+
257
+ export function applyHidden<T extends { id: string }>(models: T[]): T[] {
258
+ const hidden = new Set(loadConfigFile().hidden_models ?? []);
259
+ if (hidden.size === 0) return models;
260
+ return models.filter((m) => !hidden.has(m.id));
261
+ }
223
262
 
224
263
  // =============================================================================
225
- // Config Persistence
264
+ // Persistence
226
265
  // =============================================================================
227
266
 
228
- /** Save updated config values to ~/.pi/free.json */
229
267
  export function saveConfig(updates: Partial<PiFreeConfig>): void {
230
268
  try {
231
269
  const existing = loadConfigFile();
@@ -243,7 +281,18 @@ export function saveConfig(updates: Partial<PiFreeConfig>): void {
243
281
  }
244
282
  }
245
283
 
246
- /** Get current config values (for checking state) */
247
284
  export function getConfig(): PiFreeConfig {
248
285
  return loadConfigFile();
249
286
  }
287
+
288
+ // =============================================================================
289
+ // Re-export provider names for consistency
290
+ // =============================================================================
291
+
292
+ export {
293
+ PROVIDER_CLINE,
294
+ PROVIDER_KILO,
295
+ PROVIDER_MODAL,
296
+ PROVIDER_NVIDIA,
297
+ PROVIDER_QWEN,
298
+ };
package/constants.ts CHANGED
@@ -4,33 +4,26 @@
4
4
  */
5
5
 
6
6
  // =============================================================================
7
- // Provider names (must match registerProvider calls)
7
+ // Provider names (unique providers NOT built into pi)
8
8
  // =============================================================================
9
9
 
10
10
  export const PROVIDER_KILO = "kilo";
11
- export const PROVIDER_ZEN = "zen";
12
- export const PROVIDER_GO = "go";
13
- export const PROVIDER_OPENROUTER = "openrouter";
14
- export const PROVIDER_NVIDIA = "nvidia";
15
11
  export const PROVIDER_CLINE = "cline";
16
- export const PROVIDER_FIREWORKS = "fireworks";
17
- export const PROVIDER_OLLAMA = "ollama";
18
- export const PROVIDER_MISTRAL = "mistral";
12
+ export const PROVIDER_NVIDIA = "nvidia";
13
+ export const PROVIDER_CLOUDFLARE = "cloudflare";
14
+ export const PROVIDER_OLLAMA = "ollama-cloud";
15
+ /** @deprecated Qwen provider is deprecated. The 1,000 req/day free tier is no longer available. */
19
16
  export const PROVIDER_QWEN = "qwen";
20
17
  export const PROVIDER_MODAL = "modal";
21
18
 
22
- export const ALL_PROVIDERS = [
19
+ export const ALL_UNIQUE_PROVIDERS = [
23
20
  PROVIDER_KILO,
24
- PROVIDER_ZEN,
25
- PROVIDER_GO,
26
- PROVIDER_OPENROUTER,
27
- PROVIDER_NVIDIA,
28
21
  PROVIDER_CLINE,
29
- PROVIDER_FIREWORKS,
30
- PROVIDER_MISTRAL,
31
- PROVIDER_OLLAMA,
22
+ PROVIDER_NVIDIA,
23
+ /** @deprecated Qwen free tier no longer available */
32
24
  PROVIDER_QWEN,
33
25
  PROVIDER_MODAL,
26
+ PROVIDER_OLLAMA,
34
27
  ] as const;
35
28
 
36
29
  // =============================================================================
@@ -38,14 +31,16 @@ export const ALL_PROVIDERS = [
38
31
  // =============================================================================
39
32
 
40
33
  export const BASE_URL_KILO = "https://api.kilo.ai/api/gateway";
41
- export const BASE_URL_ZEN = "https://opencode.ai/zen/v1";
42
- export const BASE_URL_GO = "https://opencode.ai/zen/go/v1";
43
- export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
44
34
  export const BASE_URL_NVIDIA = "https://integrate.api.nvidia.com/v1";
35
+ export const BASE_URL_CLOUDFLARE = "https://api.cloudflare.com/client/v4";
36
+ export const BASE_URL_OLLAMA = "https://ollama.com/v1"; // OpenAI-compatible API endpoint
45
37
  export const BASE_URL_CLINE = "https://api.cline.bot/api/v1";
46
- export const BASE_URL_FIREWORKS = "https://api.fireworks.ai/inference/v1";
47
- export const BASE_URL_OLLAMA = "https://ollama.com/v1";
48
38
  export const BASE_URL_MODAL = "https://api.us-west-2.modal.direct/v1";
39
+ export const BASE_URL_QWEN =
40
+ "https://dashscope.aliyuncs.com/compatible-mode/v1";
41
+
42
+ /** Cline fetches free models from OpenRouter */
43
+ export const BASE_URL_OPENROUTER = "https://openrouter.ai/api/v1";
49
44
 
50
45
  // =============================================================================
51
46
  // External URLs
@@ -53,12 +48,9 @@ export const BASE_URL_MODAL = "https://api.us-west-2.modal.direct/v1";
53
48
 
54
49
  export const URL_MODELS_DEV = "https://models.dev/api.json";
55
50
  export const URL_KILO_TOS = "https://kilo.ai/terms";
56
- export const URL_ZEN_TOS = "https://opencode.ai/terms";
57
- export const URL_GO_TOS = "https://opencode.ai/terms";
58
51
  export const URL_CLINE_TOS = "https://cline.bot/tos";
59
52
  export const URL_QWEN_TOS = "https://terms.alicloud.com/";
60
53
  export const URL_MODAL_TOS = "https://modal.com/terms";
61
- export const BASE_URL_QWEN = "https://dashscope.aliyuncs.com/compatible-mode/v1";
62
54
 
63
55
  // =============================================================================
64
56
  // Cline auth
@@ -77,47 +69,17 @@ export const DEFAULT_MIN_SIZE_B = 30; // Default minimum model size for filterin
77
69
  // Timeouts (milliseconds)
78
70
  // =============================================================================
79
71
 
80
- // Timeout for fetch operations
72
+ /** Timeout for fetch operations */
81
73
  export const DEFAULT_FETCH_TIMEOUT_MS: number = 10_000;
82
74
 
83
- export interface TestConfig {
84
- timeout: number;
85
- retries: number;
86
- label: string;
87
- }
88
-
89
- // LSP test - fixed - added missing property
90
- export const testConfig: TestConfig = {
91
- timeout: 5000,
92
- retries: 3,
93
- label: "test",
94
- };
95
-
96
- // LSP test - fixed return type
97
- export function calculateTimeout(base: number): number {
98
- return base * 2;
99
- }
100
-
101
- // LSP test - unused variable (should show hint/warning if configured)
102
- export function unusedParamTest(required: string, _unused: number): string {
103
- return required.toUpperCase();
104
- }
105
75
  export const KILO_POLL_INTERVAL_MS = 3_000;
106
76
  export const KILO_TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
107
77
 
108
78
  // =============================================================================
109
- // Additional OpenAI-compatible providers
79
+ // Removed providers (now built into pi):
80
+ // - openrouter: use pi's built-in with OPENROUTER_API_KEY
81
+ // - zen/opencode: use pi's built-in with OPENCODE_API_KEY
82
+ // - go/opencode-go: use pi's built-in with OPENCODE_API_KEY
83
+ // - mistral: use pi's built-in with MISTRAL_API_KEY
84
+ // - ollama: add to ~/.pi/agent/models.json as custom provider
110
85
  // =============================================================================
111
-
112
- export const PROVIDER_GROQ = "groq";
113
- export const PROVIDER_TOGETHER = "together";
114
- export const PROVIDER_DEEPINFRA = "deepinfra";
115
- export const PROVIDER_PERPLEXITY = "perplexity";
116
- export const PROVIDER_XAI = "xai";
117
-
118
- export const BASE_URL_GROQ = "https://api.groq.com/openai/v1";
119
- export const BASE_URL_TOGETHER = "https://api.together.xyz/v1";
120
- export const BASE_URL_DEEPINFRA = "https://api.deepinfra.com/v1/openai";
121
- export const BASE_URL_MISTRAL = "https://api.mistral.ai/v1";
122
- export const BASE_URL_PERPLEXITY = "https://api.perplexity.ai";
123
- export const BASE_URL_XAI = "https://api.x.ai/v1";
package/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Pi-Free Providers Index
3
+ *
4
+ * Provides free model filtering for ALL providers (built-in + extension)
5
+ * plus unique free/paid providers not covered by pi's built-in providers.
6
+ *
7
+ * Unique providers:
8
+ * - Kilo: OAuth-based free models
9
+ * - Cline: Cline bot integration
10
+ * - NVIDIA: NVIDIA NIM hosting (free tier available)
11
+ * - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
12
+ * - Qwen: OAuth-based Qwen access (deprecated)
13
+ * - Modal: Modal Labs hosting
14
+ */
15
+
16
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
17
+ import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
18
+ import { createLogger } from "./lib/logger.ts";
19
+ import {
20
+ applyGlobalFilter,
21
+ getGlobalFreeOnly,
22
+ getProviderRegistry,
23
+ isFreeModel,
24
+ registerWithGlobalToggle,
25
+ } from "./lib/registry.ts";
26
+ // Import unique provider extensions (only providers NOT built into pi)
27
+ import cline from "./providers/cline/cline.ts";
28
+ import cloudflare from "./providers/cloudflare/cloudflare.ts";
29
+ import kilo from "./providers/kilo/kilo.ts";
30
+ import modal from "./providers/modal/modal.ts";
31
+ import nvidia from "./providers/nvidia/nvidia.ts";
32
+ import ollama from "./providers/ollama/ollama.ts";
33
+ import qwen from "./providers/qwen/qwen.ts";
34
+
35
+ const _logger = createLogger("pi-free");
36
+
37
+ // =============================================================================
38
+ // Global Commands
39
+ // =============================================================================
40
+
41
+ function setupGlobalCommands(pi: ExtensionAPI) {
42
+ // /free-providers - Show free model counts by provider
43
+ pi.registerCommand("free-providers", {
44
+ description: "Show free/paid model counts for all pi-free providers",
45
+ handler: async (_args, ctx) => {
46
+ const lines = ["📊 Pi-Free Providers:", ""];
47
+ const registry = getProviderRegistry();
48
+
49
+ // Providers known to not expose pricing via API (all models show as "free")
50
+ // OpenRouter and OpenCode expose actual pricing
51
+ const noPricingApi = new Set([
52
+ "mistral",
53
+ "xai",
54
+ "huggingface",
55
+ "groq",
56
+ "cerebras",
57
+ ]);
58
+ // Freemium providers - all models share a free tier quota
59
+ const freemiumProviders = new Set(["nvidia"]);
60
+
61
+ for (const [id, entry] of registry) {
62
+ const free = entry.stored.free.length;
63
+ const all = entry.stored.all.length || free;
64
+ const indicator = entry.hasKey ? "🔑" : "🆓";
65
+ const paid = all - free;
66
+
67
+ if (freemiumProviders.has(id)) {
68
+ // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
69
+ lines.push(`${indicator} ${id}: ${all} models (freemium)`);
70
+ } else if (noPricingApi.has(id)) {
71
+ // Provider doesn't expose pricing - can't determine free vs paid
72
+ lines.push(
73
+ `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
74
+ );
75
+ } else if (paid === 0 && free > 0) {
76
+ // All models are actually free
77
+ lines.push(`${indicator} ${id}: ${free} free models`);
78
+ } else {
79
+ // Mix of free and paid
80
+ lines.push(
81
+ `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
82
+ );
83
+ }
84
+ }
85
+
86
+ if (registry.size === 0) {
87
+ lines.push("(No providers registered yet)");
88
+ }
89
+
90
+ ctx.ui.notify(lines.join("\n"), "info");
91
+ },
92
+ });
93
+ }
94
+
95
+ // =============================================================================
96
+ // Main Entry Point
97
+ // =============================================================================
98
+
99
+ export default async function (pi: ExtensionAPI) {
100
+ const globalFreeOnly = getGlobalFreeOnly();
101
+ _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
102
+
103
+ // Setup global commands first
104
+ setupGlobalCommands(pi);
105
+
106
+ // Load all unique providers
107
+ // Each provider will register itself with the global toggle system
108
+ await Promise.allSettled([
109
+ cloudflare(pi),
110
+ modal(pi),
111
+ nvidia(pi),
112
+ kilo(pi),
113
+ ollama(pi),
114
+ // Qwen is deprecated
115
+ qwen(pi).catch((err) => {
116
+ _logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
117
+ }),
118
+ cline(pi),
119
+ ]);
120
+
121
+ // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
122
+ // These only activate if the user has configured API keys (OpenRouter works without key too)
123
+ const { setupDynamicBuiltInProviders } = await import(
124
+ "./providers/dynamic-built-in/index.ts"
125
+ );
126
+ await setupDynamicBuiltInProviders(pi);
127
+
128
+ // Setup toggles for pi's built-in providers (e.g., OpenCode)
129
+ setupBuiltInProviderToggles(pi);
130
+
131
+ // Apply initial global filter if free-only mode is enabled
132
+ if (globalFreeOnly) {
133
+ _logger.info("[pi-free] Applying initial free-only filter");
134
+ await applyGlobalFilter(pi, true);
135
+ }
136
+
137
+ const registry = getProviderRegistry();
138
+ _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
139
+ }
140
+
141
+ // Re-export registry helpers so consumers don't need deep imports
142
+ export {
143
+ applyGlobalFilter,
144
+ getGlobalFreeOnly,
145
+ getProviderRegistry,
146
+ isFreeModel,
147
+ registerWithGlobalToggle,
148
+ };