pi-free 2.2.2 → 2.2.3

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