pi-free 2.1.0 → 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/CHANGELOG.md +48 -3
- package/README.md +32 -4
- package/banner.svg +1 -1
- package/config.ts +644 -629
- package/constants.ts +4 -0
- package/index.ts +380 -378
- package/lib/built-in-toggle.ts +0 -40
- package/lib/probe-cache.ts +8 -0
- package/lib/provider-probe.ts +15 -0
- package/package.json +5 -5
- package/provider-helper.ts +1 -25
- package/providers/bai/bai.ts +232 -0
- package/providers/cline/cline-xml-bridge.ts +631 -105
- package/providers/cline/cline.ts +0 -23
- package/providers/codestral/codestral.ts +0 -11
- package/providers/dynamic-built-in/index.ts +12 -20
- package/providers/kilo/kilo.ts +2 -19
- package/providers/ollama/ollama.ts +12 -12
- package/providers/routeway/routeway.ts +10 -0
- package/providers/tokenrouter/tokenrouter.ts +634 -378
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
lines
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
};
|