pi-free 2.1.1 → 2.2.0

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/index.ts CHANGED
@@ -1,378 +1,380 @@
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
- * - ZenMux: Unified AI API gateway with 200+ models
13
- * - Codestral: Mistral's code-focused model via codestral.mistral.ai (free tier)
14
- * - DeepInfra: AI inference cloud ($5 trial credit)
15
- * - SambaNova: Fast inference on RDU hardware (free tier, no credit card)
16
- * - Together: Fast inference on 200+ open-source models ($1 trial credit)
17
- * - Routeway: OpenAI-compatible gateway with free `:free` models
18
- * - TokenRouter: OpenAI-compatible gateway routing to 90+ models
19
- * - LLM7: AI gateway (free default/fast selectors)
20
- */
21
-
22
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
- import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
24
- import { createLogger } from "./lib/logger.ts";
25
- import {
26
- processQuotaResponse,
27
- formatQuotaStatus,
28
- } from "./lib/quota-monitor.ts";
29
- import {
30
- startModelCall,
31
- recordModelCall,
32
- getAllTelemetry,
33
- getTelemetryPath,
34
- clearTelemetry,
35
- } from "./lib/telemetry.ts";
36
- import {
37
- applyGlobalFilter,
38
- getGlobalFreeOnly,
39
- getProviderRegistry,
40
- isFreeModel,
41
- registerWithGlobalToggle,
42
- } from "./lib/registry.ts";
43
- // Import unique provider extensions (only providers NOT built into pi)
44
- import cline from "./providers/cline/cline.ts";
45
- import codestral from "./providers/codestral/codestral.ts";
46
- import crofai from "./providers/crofai/crofai.ts";
47
- import kilo from "./providers/kilo/kilo.ts";
48
- import llm7 from "./providers/llm7/llm7.ts";
49
- import deepinfra from "./providers/deepinfra/deepinfra.ts";
50
- import sambanova from "./providers/sambanova/sambanova.ts";
51
- import together from "./providers/together/together.ts";
52
- import novita from "./providers/novita/novita.ts";
53
- import routeway from "./providers/routeway/routeway.ts";
54
- import tokenRouter from "./providers/tokenrouter/tokenrouter.ts";
55
- import ollama from "./providers/ollama/ollama.ts";
56
- import zenmux from "./providers/zenmux/zenmux.ts";
57
-
58
- const _logger = createLogger("pi-free");
59
-
60
- // =============================================================================
61
- // Global Commands
62
- // =============================================================================
63
-
64
- function setupGlobalCommands(pi: ExtensionAPI) {
65
- // /toggle-free - Global free-only mode toggle
66
- pi.registerCommand("toggle-free", {
67
- description: "Toggle global free-only mode for all providers",
68
- handler: async (_args, ctx) => {
69
- const current = getGlobalFreeOnly();
70
- const next = !current;
71
- applyGlobalFilter(next, { force: true });
72
-
73
- const registry = getProviderRegistry();
74
- const providerCount = registry.size;
75
-
76
- if (next) {
77
- const totalFree = [...registry.values()].reduce(
78
- (sum, e) => sum + e.stored.free.length,
79
- 0,
80
- );
81
- ctx.ui.notify(
82
- `Free-only mode: ON (${totalFree} free models across ${providerCount} providers)`,
83
- "info",
84
- );
85
- } else {
86
- const totalAll = [...registry.values()].reduce(
87
- (sum, e) => sum + (e.stored.all.length || e.stored.free.length),
88
- 0,
89
- );
90
- ctx.ui.notify(
91
- `Free-only mode: OFF (all ${totalAll} models visible across ${providerCount} providers)`,
92
- "info",
93
- );
94
- }
95
- },
96
- });
97
-
98
- // /free-providers - Show free model counts by provider
99
- pi.registerCommand("free-providers", {
100
- description: "Show free/paid model counts for all pi-free providers",
101
- handler: async (_args, ctx) => {
102
- const lines = ["📊 Pi-Free Providers:", ""];
103
- const registry = getProviderRegistry();
104
-
105
- // Providers known to not expose pricing via API (all models show as "free")
106
- // OpenRouter and OpenCode expose actual pricing
107
- const noPricingApi = new Set([
108
- "mistral",
109
- "xai",
110
- "huggingface",
111
- "groq",
112
- "cerebras",
113
- ]);
114
- // Freemium providers - all models share a free tier quota
115
- const freemiumProviders = new Set(["sambanova", "ollama-cloud"]);
116
- // Trial credit providers - one-time credits, otherwise paid
117
- const trialCreditProviders = new Set(["deepinfra"]);
118
-
119
- for (const [id, entry] of registry) {
120
- const free = entry.stored.free.length;
121
- const all = entry.stored.all.length || free;
122
- const indicator = entry.hasKey ? "🔑" : "🆓";
123
- const paid = all - free;
124
-
125
- if (freemiumProviders.has(id)) {
126
- // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
127
- lines.push(`${indicator} ${id}: ${all} models (freemium)`);
128
- } else if (trialCreditProviders.has(id)) {
129
- // Trial credit: one-time credits, otherwise paid
130
- lines.push(`${indicator} ${id}: ${all} models ($5 trial credit)`);
131
- } else if (noPricingApi.has(id)) {
132
- // Provider doesn't expose pricing - can't determine free vs paid
133
- lines.push(
134
- `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
135
- );
136
- } else if (paid === 0 && free > 0) {
137
- // All models are actually free
138
- lines.push(`${indicator} ${id}: ${free} free models`);
139
- } else {
140
- // Mix of free and paid
141
- lines.push(
142
- `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
143
- );
144
- }
145
- }
146
-
147
- if (registry.size === 0) {
148
- lines.push("(No providers registered yet)");
149
- }
150
-
151
- ctx.ui.notify(lines.join("\n"), "info");
152
- },
153
- });
154
-
155
- // /telemetry — Show model telemetry data
156
- pi.registerCommand("free-telemetry", {
157
- description:
158
- "Show real-world performance data for free models (tokens/s, latency, success rate)",
159
- handler: async (_args, ctx) => {
160
- const allTelemetry = getAllTelemetry();
161
- const entries = Object.entries(allTelemetry);
162
-
163
- if (entries.length === 0) {
164
- ctx.ui.notify(
165
- "No telemetry data yet. Use some free models first!",
166
- "info",
167
- );
168
- return;
169
- }
170
-
171
- // Sort by total calls descending
172
- entries.sort((a, b) => b[1].totalCalls - a[1].totalCalls);
173
-
174
- const lines = ["📊 Model Telemetry:", ""];
175
- lines.push(
176
- `${`Model`.padEnd(40)} ${`Calls`.padEnd(6)} ${`OK%`.padEnd(6)} ${`Lat`.padEnd(7)} ${`tok/s`.padEnd(7)} ${`Cost`}`,
177
- );
178
- lines.push(`─`.repeat(75));
179
-
180
- for (const [key, t] of entries.slice(0, 20)) {
181
- const name = key.length > 38 ? key.slice(0, 35) + "..." : key;
182
- const calls = String(t.totalCalls).padStart(5);
183
- const ok = `${t.successRate}%`.padStart(5);
184
- const lat =
185
- t.avgLatencyMs > 0
186
- ? `${t.avgLatencyMs}ms`.padStart(6)
187
- : "—".padStart(6);
188
- const tps =
189
- t.avgTokensPerSecond > 0
190
- ? `${t.avgTokensPerSecond}`.padStart(6)
191
- : "—".padStart(6);
192
- const cost =
193
- t.totalCost > 0
194
- ? `$${t.totalCost.toFixed(4)}`.padStart(8)
195
- : "free".padStart(8);
196
- lines.push(`${name.padEnd(40)} ${calls} ${ok} ${lat} ${tps} ${cost}`);
197
- }
198
-
199
- lines.push("", `File: ${getTelemetryPath()}`);
200
- ctx.ui.notify(lines.join("\n"), "info");
201
- },
202
- });
203
-
204
- // /clear-free-telemetry — Clear all telemetry data
205
- pi.registerCommand("clear-free-telemetry", {
206
- description: "Clear all model telemetry data",
207
- handler: async (_args, ctx) => {
208
- await clearTelemetry();
209
- ctx.ui.notify("Telemetry data cleared", "info");
210
- },
211
- });
212
- }
213
-
214
- // =============================================================================
215
- // Quota Monitoring
216
- // =============================================================================
217
-
218
- function setupQuotaMonitoring(pi: ExtensionAPI) {
219
- // Capture rate-limit headers from every provider response
220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
- (pi as any).on(
222
- "after_provider_response",
223
- (event: { status: number; headers: Record<string, string> }, ctx: any) => {
224
- const providerId = ctx.model?.provider;
225
- if (!providerId) return;
226
-
227
- processQuotaResponse(providerId, event.headers);
228
-
229
- // Update status bar with quota for the active provider
230
- const status = formatQuotaStatus(providerId);
231
- if (status) {
232
- ctx.ui.setStatus("quota", status);
233
- }
234
- },
235
- );
236
-
237
- // Clear quota status when switching away from a provider
238
- pi.on("model_select", (_event, ctx) => {
239
- const providerId = ctx.model?.provider;
240
- if (!providerId) {
241
- ctx.ui.setStatus("quota", undefined);
242
- return;
243
- }
244
- // Show cached quota on provider switch (if still fresh)
245
- const status = formatQuotaStatus(providerId);
246
- ctx.ui.setStatus("quota", status);
247
- });
248
- }
249
-
250
- // =============================================================================
251
- // Model Telemetry
252
- // =============================================================================
253
-
254
- function setupTelemetry(pi: ExtensionAPI) {
255
- // Only track telemetry for FREE models (uses same isFreeModel logic as model filtering)
256
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
- (pi as any).on("before_agent_start", (_event: any, ctx: any) => {
258
- if (!ctx.model) return;
259
- if (!isFreeModel(ctx.model as any)) return;
260
- const provider = ctx.model?.provider;
261
- const model = ctx.model?.id;
262
- if (provider && model) {
263
- startModelCall(provider, model);
264
- }
265
- });
266
-
267
- // Record telemetry when a turn completes
268
- pi.on("turn_end", async (event, ctx) => {
269
- if (!ctx.model) return;
270
- if (!isFreeModel(ctx.model as any)) return;
271
-
272
- const msg = (
273
- event as {
274
- message?: {
275
- role?: string;
276
- model?: string;
277
- usage?: {
278
- input?: number;
279
- output?: number;
280
- totalTokens?: number;
281
- cost?: { total?: number };
282
- };
283
- stopReason?: string;
284
- errorMessage?: string;
285
- };
286
- }
287
- ).message;
288
-
289
- if (msg?.role !== "assistant") return;
290
-
291
- const provider = ctx.model?.provider;
292
- const model = msg.model || ctx.model?.id;
293
- if (!provider || !model) return;
294
-
295
- const usage = msg.usage;
296
- const inputTokens = usage?.input ?? 0;
297
- const outputTokens = usage?.output ?? 0;
298
- const totalTokens = usage?.totalTokens ?? inputTokens + outputTokens;
299
- const cost = usage?.cost?.total ?? 0;
300
- const isError = msg.stopReason === "error" || !!msg.errorMessage;
301
-
302
- await recordModelCall(
303
- provider,
304
- model,
305
- { input: inputTokens, output: outputTokens, totalTokens },
306
- cost,
307
- {
308
- success: !isError,
309
- stopReason: msg.stopReason,
310
- errorMessage: msg.errorMessage,
311
- },
312
- );
313
- });
314
- }
315
-
316
- // =============================================================================
317
- // Main Entry Point
318
- // =============================================================================
319
-
320
- export default async function piFreeEntry(pi: ExtensionAPI) {
321
- const globalFreeOnly = getGlobalFreeOnly();
322
- _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
323
-
324
- // Setup global commands first
325
- setupGlobalCommands(pi);
326
-
327
- // Setup quota monitoring (passive, no extra API calls)
328
- setupQuotaMonitoring(pi);
329
-
330
- // Setup model telemetry (tracks real-world performance)
331
- setupTelemetry(pi);
332
-
333
- // Load all unique providers
334
- // Each provider will register itself with the global toggle system
335
- await Promise.allSettled([
336
- kilo(pi),
337
- ollama(pi),
338
- cline(pi),
339
- zenmux(pi),
340
- crofai(pi),
341
- codestral(pi),
342
- llm7(pi),
343
- deepinfra(pi),
344
- sambanova(pi),
345
- together(pi),
346
- novita(pi),
347
- routeway(pi),
348
- tokenRouter(pi),
349
- ]);
350
-
351
- // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face,
352
- // OpenRouter/OpenCode from Pi auth, and FastRouter public model discovery)
353
- const { setupDynamicBuiltInProviders } = await import(
354
- "./providers/dynamic-built-in/index.ts"
355
- );
356
- await setupDynamicBuiltInProviders(pi);
357
-
358
- // Setup toggles for pi's built-in providers (e.g., OpenCode)
359
- setupBuiltInProviderToggles(pi);
360
-
361
- // Apply initial global filter if free-only mode is enabled
362
- if (globalFreeOnly) {
363
- _logger.info("[pi-free] Applying initial free-only filter");
364
- applyGlobalFilter(true);
365
- }
366
-
367
- const registry = getProviderRegistry();
368
- _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
369
- }
370
-
371
- // Re-export registry helpers so consumers don't need deep imports
372
- export {
373
- applyGlobalFilter,
374
- getGlobalFreeOnly,
375
- getProviderRegistry,
376
- isFreeModel,
377
- registerWithGlobalToggle,
378
- };
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
+ * - ZenMux: Unified AI API gateway with 200+ models
13
+ * - Codestral: Mistral's code-focused model via codestral.mistral.ai (free tier)
14
+ * - DeepInfra: AI inference cloud ($5 trial credit)
15
+ * - SambaNova: Fast inference on RDU hardware (free tier, no credit card)
16
+ * - Together: Fast inference on 200+ open-source models ($1 trial credit)
17
+ * - Routeway: OpenAI-compatible gateway with free `:free` models
18
+ * - TokenRouter: OpenAI-compatible gateway routing to 90+ models
19
+ * - LLM7: AI gateway (free default/fast selectors)
20
+ */
21
+
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
24
+ import { createLogger } from "./lib/logger.ts";
25
+ import {
26
+ processQuotaResponse,
27
+ formatQuotaStatus,
28
+ } from "./lib/quota-monitor.ts";
29
+ import {
30
+ startModelCall,
31
+ recordModelCall,
32
+ getAllTelemetry,
33
+ getTelemetryPath,
34
+ clearTelemetry,
35
+ } from "./lib/telemetry.ts";
36
+ import {
37
+ applyGlobalFilter,
38
+ getGlobalFreeOnly,
39
+ getProviderRegistry,
40
+ isFreeModel,
41
+ registerWithGlobalToggle,
42
+ } from "./lib/registry.ts";
43
+ // Import unique provider extensions (only providers NOT built into pi)
44
+ import cline from "./providers/cline/cline.ts";
45
+ import codestral from "./providers/codestral/codestral.ts";
46
+ import crofai from "./providers/crofai/crofai.ts";
47
+ import kilo from "./providers/kilo/kilo.ts";
48
+ import llm7 from "./providers/llm7/llm7.ts";
49
+ import deepinfra from "./providers/deepinfra/deepinfra.ts";
50
+ import sambanova from "./providers/sambanova/sambanova.ts";
51
+ import together from "./providers/together/together.ts";
52
+ import novita from "./providers/novita/novita.ts";
53
+ import routeway from "./providers/routeway/routeway.ts";
54
+ import tokenRouter from "./providers/tokenrouter/tokenrouter.ts";
55
+ import ollama from "./providers/ollama/ollama.ts";
56
+ import zenmux from "./providers/zenmux/zenmux.ts";
57
+ import bai from "./providers/bai/bai.ts";
58
+
59
+ const _logger = createLogger("pi-free");
60
+
61
+ // =============================================================================
62
+ // Global Commands
63
+ // =============================================================================
64
+
65
+ function setupGlobalCommands(pi: ExtensionAPI) {
66
+ // /toggle-free - Global free-only mode toggle
67
+ pi.registerCommand("toggle-free", {
68
+ description: "Toggle global free-only mode for all providers",
69
+ handler: async (_args, ctx) => {
70
+ const current = getGlobalFreeOnly();
71
+ const next = !current;
72
+ applyGlobalFilter(next, { force: true });
73
+
74
+ const registry = getProviderRegistry();
75
+ const providerCount = registry.size;
76
+
77
+ if (next) {
78
+ const totalFree = [...registry.values()].reduce(
79
+ (sum, e) => sum + e.stored.free.length,
80
+ 0,
81
+ );
82
+ ctx.ui.notify(
83
+ `Free-only mode: ON (${totalFree} free models across ${providerCount} providers)`,
84
+ "info",
85
+ );
86
+ } else {
87
+ const totalAll = [...registry.values()].reduce(
88
+ (sum, e) => sum + (e.stored.all.length || e.stored.free.length),
89
+ 0,
90
+ );
91
+ ctx.ui.notify(
92
+ `Free-only mode: OFF (all ${totalAll} models visible across ${providerCount} providers)`,
93
+ "info",
94
+ );
95
+ }
96
+ },
97
+ });
98
+
99
+ // /free-providers - Show free model counts by provider
100
+ pi.registerCommand("free-providers", {
101
+ description: "Show free/paid model counts for all pi-free providers",
102
+ handler: async (_args, ctx) => {
103
+ const lines = ["📊 Pi-Free Providers:", ""];
104
+ const registry = getProviderRegistry();
105
+
106
+ // Providers known to not expose pricing via API (all models show as "free")
107
+ // OpenRouter and OpenCode expose actual pricing
108
+ const noPricingApi = new Set([
109
+ "mistral",
110
+ "xai",
111
+ "huggingface",
112
+ "groq",
113
+ "cerebras",
114
+ ]);
115
+ // Freemium providers - all models share a free tier quota
116
+ const freemiumProviders = new Set(["sambanova", "ollama-cloud"]);
117
+ // Trial credit providers - one-time credits, otherwise paid
118
+ const trialCreditProviders = new Set(["deepinfra"]);
119
+
120
+ for (const [id, entry] of registry) {
121
+ const free = entry.stored.free.length;
122
+ const all = entry.stored.all.length || free;
123
+ const indicator = entry.hasKey ? "🔑" : "🆓";
124
+ const paid = all - free;
125
+
126
+ if (freemiumProviders.has(id)) {
127
+ // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
128
+ lines.push(`${indicator} ${id}: ${all} models (freemium)`);
129
+ } else if (trialCreditProviders.has(id)) {
130
+ // Trial credit: one-time credits, otherwise paid
131
+ lines.push(`${indicator} ${id}: ${all} models ($5 trial credit)`);
132
+ } else if (noPricingApi.has(id)) {
133
+ // Provider doesn't expose pricing - can't determine free vs paid
134
+ lines.push(
135
+ `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
136
+ );
137
+ } else if (paid === 0 && free > 0) {
138
+ // All models are actually free
139
+ lines.push(`${indicator} ${id}: ${free} free models`);
140
+ } else {
141
+ // Mix of free and paid
142
+ lines.push(
143
+ `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
144
+ );
145
+ }
146
+ }
147
+
148
+ if (registry.size === 0) {
149
+ lines.push("(No providers registered yet)");
150
+ }
151
+
152
+ ctx.ui.notify(lines.join("\n"), "info");
153
+ },
154
+ });
155
+
156
+ // /telemetry — Show model telemetry data
157
+ pi.registerCommand("free-telemetry", {
158
+ description:
159
+ "Show real-world performance data for free models (tokens/s, latency, success rate)",
160
+ handler: async (_args, ctx) => {
161
+ const allTelemetry = getAllTelemetry();
162
+ const entries = Object.entries(allTelemetry);
163
+
164
+ if (entries.length === 0) {
165
+ ctx.ui.notify(
166
+ "No telemetry data yet. Use some free models first!",
167
+ "info",
168
+ );
169
+ return;
170
+ }
171
+
172
+ // Sort by total calls descending
173
+ entries.sort((a, b) => b[1].totalCalls - a[1].totalCalls);
174
+
175
+ const lines = ["📊 Model Telemetry:", ""];
176
+ lines.push(
177
+ `${`Model`.padEnd(40)} ${`Calls`.padEnd(6)} ${`OK%`.padEnd(6)} ${`Lat`.padEnd(7)} ${`tok/s`.padEnd(7)} ${`Cost`}`,
178
+ );
179
+ lines.push(`─`.repeat(75));
180
+
181
+ for (const [key, t] of entries.slice(0, 20)) {
182
+ const name = key.length > 38 ? key.slice(0, 35) + "..." : key;
183
+ const calls = String(t.totalCalls).padStart(5);
184
+ const ok = `${t.successRate}%`.padStart(5);
185
+ const lat =
186
+ t.avgLatencyMs > 0
187
+ ? `${t.avgLatencyMs}ms`.padStart(6)
188
+ : "—".padStart(6);
189
+ const tps =
190
+ t.avgTokensPerSecond > 0
191
+ ? `${t.avgTokensPerSecond}`.padStart(6)
192
+ : "—".padStart(6);
193
+ const cost =
194
+ t.totalCost > 0
195
+ ? `$${t.totalCost.toFixed(4)}`.padStart(8)
196
+ : "free".padStart(8);
197
+ lines.push(`${name.padEnd(40)} ${calls} ${ok} ${lat} ${tps} ${cost}`);
198
+ }
199
+
200
+ lines.push("", `File: ${getTelemetryPath()}`);
201
+ ctx.ui.notify(lines.join("\n"), "info");
202
+ },
203
+ });
204
+
205
+ // /clear-free-telemetry — Clear all telemetry data
206
+ pi.registerCommand("clear-free-telemetry", {
207
+ description: "Clear all model telemetry data",
208
+ handler: async (_args, ctx) => {
209
+ await clearTelemetry();
210
+ ctx.ui.notify("Telemetry data cleared", "info");
211
+ },
212
+ });
213
+ }
214
+
215
+ // =============================================================================
216
+ // Quota Monitoring
217
+ // =============================================================================
218
+
219
+ function setupQuotaMonitoring(pi: ExtensionAPI) {
220
+ // Capture rate-limit headers from every provider response
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
+ (pi as any).on(
223
+ "after_provider_response",
224
+ (event: { status: number; headers: Record<string, string> }, ctx: any) => {
225
+ const providerId = ctx.model?.provider;
226
+ if (!providerId) return;
227
+
228
+ processQuotaResponse(providerId, event.headers);
229
+
230
+ // Update status bar with quota for the active provider
231
+ const status = formatQuotaStatus(providerId);
232
+ if (status) {
233
+ ctx.ui.setStatus("quota", status);
234
+ }
235
+ },
236
+ );
237
+
238
+ // Clear quota status when switching away from a provider
239
+ pi.on("model_select", (_event, ctx) => {
240
+ const providerId = ctx.model?.provider;
241
+ if (!providerId) {
242
+ ctx.ui.setStatus("quota", undefined);
243
+ return;
244
+ }
245
+ // Show cached quota on provider switch (if still fresh)
246
+ const status = formatQuotaStatus(providerId);
247
+ ctx.ui.setStatus("quota", status);
248
+ });
249
+ }
250
+
251
+ // =============================================================================
252
+ // Model Telemetry
253
+ // =============================================================================
254
+
255
+ function setupTelemetry(pi: ExtensionAPI) {
256
+ // Only track telemetry for FREE models (uses same isFreeModel logic as model filtering)
257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
+ (pi as any).on("before_agent_start", (_event: any, ctx: any) => {
259
+ if (!ctx.model) return;
260
+ if (!isFreeModel(ctx.model as any)) return;
261
+ const provider = ctx.model?.provider;
262
+ const model = ctx.model?.id;
263
+ if (provider && model) {
264
+ startModelCall(provider, model);
265
+ }
266
+ });
267
+
268
+ // Record telemetry when a turn completes
269
+ pi.on("turn_end", async (event, ctx) => {
270
+ if (!ctx.model) return;
271
+ if (!isFreeModel(ctx.model as any)) return;
272
+
273
+ const msg = (
274
+ event as {
275
+ message?: {
276
+ role?: string;
277
+ model?: string;
278
+ usage?: {
279
+ input?: number;
280
+ output?: number;
281
+ totalTokens?: number;
282
+ cost?: { total?: number };
283
+ };
284
+ stopReason?: string;
285
+ errorMessage?: string;
286
+ };
287
+ }
288
+ ).message;
289
+
290
+ if (msg?.role !== "assistant") return;
291
+
292
+ const provider = ctx.model?.provider;
293
+ const model = msg.model || ctx.model?.id;
294
+ if (!provider || !model) return;
295
+
296
+ const usage = msg.usage;
297
+ const inputTokens = usage?.input ?? 0;
298
+ const outputTokens = usage?.output ?? 0;
299
+ const totalTokens = usage?.totalTokens ?? inputTokens + outputTokens;
300
+ const cost = usage?.cost?.total ?? 0;
301
+ const isError = msg.stopReason === "error" || !!msg.errorMessage;
302
+
303
+ await recordModelCall(
304
+ provider,
305
+ model,
306
+ { input: inputTokens, output: outputTokens, totalTokens },
307
+ cost,
308
+ {
309
+ success: !isError,
310
+ stopReason: msg.stopReason,
311
+ errorMessage: msg.errorMessage,
312
+ },
313
+ );
314
+ });
315
+ }
316
+
317
+ // =============================================================================
318
+ // Main Entry Point
319
+ // =============================================================================
320
+
321
+ export default async function piFreeEntry(pi: ExtensionAPI) {
322
+ const globalFreeOnly = getGlobalFreeOnly();
323
+ _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
324
+
325
+ // Setup global commands first
326
+ setupGlobalCommands(pi);
327
+
328
+ // Setup quota monitoring (passive, no extra API calls)
329
+ setupQuotaMonitoring(pi);
330
+
331
+ // Setup model telemetry (tracks real-world performance)
332
+ setupTelemetry(pi);
333
+
334
+ // Load all unique providers
335
+ // Each provider will register itself with the global toggle system
336
+ await Promise.allSettled([
337
+ kilo(pi),
338
+ ollama(pi),
339
+ cline(pi),
340
+ zenmux(pi),
341
+ crofai(pi),
342
+ codestral(pi),
343
+ llm7(pi),
344
+ deepinfra(pi),
345
+ sambanova(pi),
346
+ together(pi),
347
+ novita(pi),
348
+ routeway(pi),
349
+ tokenRouter(pi),
350
+ bai(pi),
351
+ ]);
352
+
353
+ // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face,
354
+ // OpenRouter/OpenCode from Pi auth, and FastRouter public model discovery)
355
+ const { setupDynamicBuiltInProviders } = await import(
356
+ "./providers/dynamic-built-in/index.ts"
357
+ );
358
+ await setupDynamicBuiltInProviders(pi);
359
+
360
+ // Setup toggles for pi's built-in providers (e.g., OpenCode)
361
+ setupBuiltInProviderToggles(pi);
362
+
363
+ // Apply initial global filter if free-only mode is enabled
364
+ if (globalFreeOnly) {
365
+ _logger.info("[pi-free] Applying initial free-only filter");
366
+ applyGlobalFilter(true);
367
+ }
368
+
369
+ const registry = getProviderRegistry();
370
+ _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
371
+ }
372
+
373
+ // Re-export registry helpers so consumers don't need deep imports
374
+ export {
375
+ applyGlobalFilter,
376
+ getGlobalFreeOnly,
377
+ getProviderRegistry,
378
+ isFreeModel,
379
+ registerWithGlobalToggle,
380
+ };