pi-free 2.0.2 → 2.0.4

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.
@@ -1,526 +0,0 @@
1
- /**
2
- * Cloudflare Workers AI Provider Extension
3
- *
4
- * Provides access to Cloudflare's serverless GPU network with 30+ models.
5
- * All models use Cloudflare's "Neurons" pricing system:
6
- * - 10,000 Neurons per day FREE (resets daily at 00:00 UTC)
7
- * - $0.011 per 1,000 Neurons beyond free allocation
8
- *
9
- * Setup:
10
- * 1. Create API token at https://dash.cloudflare.com/profile/api-tokens
11
- * with "Cloudflare AI" > "Read" permission
12
- * 2. Get Account ID from https://dash.cloudflare.com (right sidebar)
13
- * 3. Add credentials to ~/.pi/agent/auth.json or set env vars
14
- *
15
- * Auth (in order of priority):
16
- * - Environment: CF_API_TOKEN and CF_ACCOUNT_ID
17
- * - Config file: ~/.pi/agent/auth.json
18
- * { "cloudflare-ai": { "access": "token", "account_id": "id" } }
19
- * - Legacy: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID env vars
20
- */
21
-
22
- import { existsSync, readFileSync } from "node:fs";
23
- import { homedir } from "node:os";
24
- import { join } from "node:path";
25
- import type {
26
- ExtensionAPI,
27
- ProviderModelConfig,
28
- } from "@mariozechner/pi-coding-agent";
29
- import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
30
- import { createLogger } from "../../lib/logger.ts";
31
- import { fetchWithRetry } from "../../lib/util.ts";
32
-
33
- const _logger = createLogger("cloudflare");
34
-
35
- // =============================================================================
36
- // Auth Resolution
37
- // =============================================================================
38
-
39
- interface CloudflareAuth {
40
- token?: string;
41
- accountId?: string;
42
- }
43
-
44
- function getCloudflareAuth(): CloudflareAuth {
45
- const result: CloudflareAuth = {};
46
-
47
- // Check new env var names first
48
- if (process.env.CF_API_TOKEN) result.token = process.env.CF_API_TOKEN;
49
- if (process.env.CF_ACCOUNT_ID) result.accountId = process.env.CF_ACCOUNT_ID;
50
-
51
- // Check legacy env var names
52
- if (!result.token && process.env.CLOUDFLARE_API_TOKEN) {
53
- result.token = process.env.CLOUDFLARE_API_TOKEN;
54
- }
55
- if (!result.accountId && process.env.CLOUDFLARE_ACCOUNT_ID) {
56
- result.accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
57
- }
58
-
59
- if (result.token && result.accountId) return result;
60
-
61
- // Check ~/.pi/free.json (pi-free config)
62
- try {
63
- const configPath = join(homedir(), ".pi", "free.json");
64
- if (existsSync(configPath)) {
65
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
66
- if (!result.token && config.cloudflare_api_token) {
67
- result.token = config.cloudflare_api_token;
68
- }
69
- if (!result.accountId && config.cloudflare_account_id) {
70
- result.accountId = config.cloudflare_account_id;
71
- }
72
- }
73
- } catch {
74
- // Ignore config file errors
75
- }
76
-
77
- return result;
78
- }
79
-
80
- // =============================================================================
81
- // Compatibility Settings
82
- // =============================================================================
83
-
84
- const CLOUDFLARE_COMPAT = {
85
- supportsStore: false,
86
- supportsDeveloperRole: false,
87
- supportsReasoningEffort: false,
88
- supportsStrictMode: false,
89
- maxTokensField: "max_tokens" as const,
90
- };
91
-
92
- // =============================================================================
93
- // Known non-chat model patterns (to filter out)
94
- // =============================================================================
95
-
96
- const NON_CHAT_PATTERNS = [
97
- // Embeddings
98
- /bge-/i,
99
- /embed/i,
100
- /embedding/i,
101
- /pfnet\/plamo-embedding/i,
102
- /qwen3-embedding/i,
103
- // Image generation
104
- /flux/i,
105
- /stable-diffusion/i,
106
- /dreamshaper/i,
107
- /lucid-origin/i,
108
- /phoenix/i,
109
- // Speech/audio
110
- /whisper/i,
111
- /aura-/i,
112
- /nova-/i,
113
- /melotts/i,
114
- // Translation (not chat)
115
- /indictrans/i,
116
- /m2m100/i,
117
- // Vision-only models
118
- /llava/i,
119
- /detr-/i,
120
- /resnet/i,
121
- /unum\/uform/i,
122
- // Code/SQL only
123
- /sqlcoder/i,
124
- // Classification/reranking
125
- /reranker/i,
126
- /distilbert/i,
127
- // Safety/moderation
128
- /llama-guard/i,
129
- // Turn detection
130
- /smart-turn/i,
131
- ];
132
-
133
- // =============================================================================
134
- // Fallback models (used if API fetch fails)
135
- // =============================================================================
136
-
137
- const FALLBACK_MODELS: ProviderModelConfig[] = [
138
- {
139
- id: "@cf/moonshotai/kimi-k2.5",
140
- name: "Kimi K2.5",
141
- reasoning: true,
142
- input: ["text", "image"],
143
- cost: { input: 0.6, output: 3.0, cacheRead: 0.1, cacheWrite: 0 },
144
- contextWindow: 256000,
145
- maxTokens: 8192,
146
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
147
- },
148
- {
149
- id: "@cf/moonshotai/kimi-k2.6",
150
- name: "Kimi K2.6",
151
- reasoning: true,
152
- input: ["text", "image"],
153
- cost: { input: 0.8, output: 4.0, cacheRead: 0.1, cacheWrite: 0 },
154
- contextWindow: 256000,
155
- maxTokens: 8192,
156
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
157
- },
158
- {
159
- id: "@cf/meta/llama-4-scout-17b-16e-instruct",
160
- name: "Llama 4 Scout 17B",
161
- reasoning: false,
162
- input: ["text", "image"],
163
- cost: { input: 0.27, output: 0.85, cacheRead: 0, cacheWrite: 0 },
164
- contextWindow: 131072,
165
- maxTokens: 8192,
166
- compat: CLOUDFLARE_COMPAT,
167
- },
168
- {
169
- id: "@cf/nvidia/nemotron-3-120b-a12b",
170
- name: "Nemotron 3 Super 120B",
171
- reasoning: true,
172
- input: ["text"],
173
- cost: { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
174
- contextWindow: 256000,
175
- maxTokens: 8192,
176
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
177
- },
178
- {
179
- id: "@cf/openai/gpt-oss-120b",
180
- name: "GPT-OSS 120B",
181
- reasoning: true,
182
- input: ["text"],
183
- cost: { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
184
- contextWindow: 128000,
185
- maxTokens: 8192,
186
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
187
- },
188
- {
189
- id: "@cf/openai/gpt-oss-20b",
190
- name: "GPT-OSS 20B",
191
- reasoning: true,
192
- input: ["text"],
193
- cost: { input: 0.2, output: 0.6, cacheRead: 0, cacheWrite: 0 },
194
- contextWindow: 128000,
195
- maxTokens: 8192,
196
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
197
- },
198
- {
199
- id: "@cf/google/gemma-4-26b-a4b-it",
200
- name: "Gemma 4 26B",
201
- reasoning: true,
202
- input: ["text", "image"],
203
- cost: { input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0 },
204
- contextWindow: 256000,
205
- maxTokens: 8192,
206
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
207
- },
208
- {
209
- id: "@cf/google/gemma-3-12b-it",
210
- name: "Gemma 3 12B",
211
- reasoning: false,
212
- input: ["text", "image"],
213
- cost: { input: 0.345, output: 0.556, cacheRead: 0, cacheWrite: 0 },
214
- contextWindow: 80000,
215
- maxTokens: 8192,
216
- compat: CLOUDFLARE_COMPAT,
217
- },
218
- {
219
- id: "@cf/qwen/qwen3-30b-a3b-fp8",
220
- name: "Qwen3 30B A3B",
221
- reasoning: true,
222
- input: ["text"],
223
- cost: { input: 0.051, output: 0.34, cacheRead: 0, cacheWrite: 0 },
224
- contextWindow: 32768,
225
- maxTokens: 8192,
226
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
227
- },
228
- {
229
- id: "@cf/qwen/qwen2.5-coder-32b-instruct",
230
- name: "Qwen 2.5 Coder 32B",
231
- reasoning: false,
232
- input: ["text"],
233
- cost: { input: 0.3, output: 0.3, cacheRead: 0, cacheWrite: 0 },
234
- contextWindow: 32768,
235
- maxTokens: 8192,
236
- compat: CLOUDFLARE_COMPAT,
237
- },
238
- {
239
- id: "@cf/qwen/qwq-32b",
240
- name: "QwQ 32B (Reasoning)",
241
- reasoning: true,
242
- input: ["text"],
243
- cost: { input: 0.3, output: 0.3, cacheRead: 0, cacheWrite: 0 },
244
- contextWindow: 32768,
245
- maxTokens: 8192,
246
- compat: { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true },
247
- },
248
- {
249
- id: "@cf/zai-org/glm-4.7-flash",
250
- name: "GLM-4.7 Flash",
251
- reasoning: false,
252
- input: ["text"],
253
- cost: { input: 0.06, output: 0.4, cacheRead: 0, cacheWrite: 0 },
254
- contextWindow: 131072,
255
- maxTokens: 8192,
256
- compat: CLOUDFLARE_COMPAT,
257
- },
258
- {
259
- id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
260
- name: "Llama 3.3 70B Fast",
261
- reasoning: false,
262
- input: ["text"],
263
- cost: { input: 0.5, output: 0.5, cacheRead: 0, cacheWrite: 0 },
264
- contextWindow: 131072,
265
- maxTokens: 8192,
266
- compat: CLOUDFLARE_COMPAT,
267
- },
268
- {
269
- id: "@cf/meta/llama-3.1-405b-instruct",
270
- name: "Llama 3.1 405B",
271
- reasoning: false,
272
- input: ["text"],
273
- cost: { input: 2.0, output: 2.0, cacheRead: 0, cacheWrite: 0 },
274
- contextWindow: 131072,
275
- maxTokens: 8192,
276
- compat: CLOUDFLARE_COMPAT,
277
- },
278
- {
279
- id: "@cf/meta/llama-3.1-70b-instruct",
280
- name: "Llama 3.1 70B",
281
- reasoning: false,
282
- input: ["text"],
283
- cost: { input: 0.5, output: 0.5, cacheRead: 0, cacheWrite: 0 },
284
- contextWindow: 131072,
285
- maxTokens: 8192,
286
- compat: CLOUDFLARE_COMPAT,
287
- },
288
- {
289
- id: "@cf/meta/llama-3.2-11b-vision-instruct",
290
- name: "Llama 3.2 11B Vision",
291
- reasoning: false,
292
- input: ["text", "image"],
293
- cost: { input: 0.2, output: 0.2, cacheRead: 0, cacheWrite: 0 },
294
- contextWindow: 128000,
295
- maxTokens: 8192,
296
- compat: CLOUDFLARE_COMPAT,
297
- },
298
- ];
299
-
300
- // =============================================================================
301
- // Model metadata inference
302
- // =============================================================================
303
-
304
- interface CloudflareModel {
305
- id: string;
306
- name?: string;
307
- description?: string;
308
- task?: {
309
- id?: string;
310
- name?: string;
311
- };
312
- }
313
-
314
- function isChatModel(modelId: string): boolean {
315
- return !NON_CHAT_PATTERNS.some((pattern) => pattern.test(modelId));
316
- }
317
-
318
- function inferModelName(id: string): string {
319
- // Extract the model name part after the last /
320
- const namePart = id.split("/").pop() || id;
321
-
322
- // Remove common suffixes
323
- const clean = namePart
324
- .replace(/-instruct$/i, "")
325
- .replace(/-chat$/i, "")
326
- .replace(/-it$/i, "")
327
- .replace(/-awq$/i, " (AWQ)")
328
- .replace(/-fp8$/i, " (FP8)")
329
- .replace(/-fast$/i, " (Fast)")
330
- .replace(/-lora$/i, " (LoRA)")
331
- .replace(/-hf$/i, " (HF)")
332
- .replace(/-v\d+\.\d+$/i, "");
333
-
334
- // Convert to title case
335
- return clean
336
- .split("-")
337
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
338
- .join(" ")
339
- .replace(/\b(\d+(?:\.\d+)?)[bB]\b/g, "$1B");
340
- }
341
-
342
- function inferModelMetadata(id: string): Partial<ProviderModelConfig> {
343
- const hasVision = /vision|multimodal|vl|llava/i.test(id);
344
- const hasReasoning = /reason|r1|thinking|qwq|nemotron|oss/i.test(id);
345
-
346
- // Default context windows by model family
347
- let contextWindow = 32768;
348
- let maxTokens = 4096;
349
-
350
- if (/llama-3\.1|llama-3\.3|llama-4|gemma-4|kimi|nemotron/i.test(id)) {
351
- contextWindow = 128000;
352
- maxTokens = 8192;
353
- }
354
- if (/llama-3\.2-11b/i.test(id)) {
355
- contextWindow = 128000;
356
- maxTokens = 8192;
357
- }
358
- if (/gemma-3/i.test(id)) {
359
- contextWindow = 80000;
360
- maxTokens = 8192;
361
- }
362
-
363
- // Estimate costs based on model size (very rough approximation)
364
- let inputCost = 0.1;
365
- let outputCost = 0.3;
366
-
367
- const sizeMatch = id.match(/(\d+)(?:\.\d+)?[bB]/);
368
- if (sizeMatch) {
369
- const size = parseInt(sizeMatch[1], 10);
370
- if (size >= 100) {
371
- inputCost = 0.5;
372
- outputCost = 1.5;
373
- } else if (size >= 70) {
374
- inputCost = 0.5;
375
- outputCost = 0.5;
376
- } else if (size >= 30) {
377
- inputCost = 0.3;
378
- outputCost = 0.3;
379
- } else if (size >= 8) {
380
- inputCost = 0.2;
381
- outputCost = 0.2;
382
- }
383
- }
384
-
385
- // Override for specific known models
386
- if (id.includes("llama-3.1-405b")) {
387
- inputCost = 2.0;
388
- outputCost = 2.0;
389
- }
390
- if (id.includes("kimi-k2.5")) {
391
- inputCost = 0.6;
392
- outputCost = 3.0;
393
- }
394
- if (id.includes("kimi-k2.6")) {
395
- inputCost = 0.8;
396
- outputCost = 4.0;
397
- }
398
-
399
- return {
400
- name: inferModelName(id),
401
- reasoning: hasReasoning,
402
- input: hasVision ? (["text", "image"] as const) : (["text"] as const),
403
- cost: { input: inputCost, output: outputCost, cacheRead: 0, cacheWrite: 0 },
404
- contextWindow,
405
- maxTokens,
406
- compat: hasReasoning
407
- ? { ...CLOUDFLARE_COMPAT, requiresThinkingAsText: true }
408
- : CLOUDFLARE_COMPAT,
409
- };
410
- }
411
-
412
- // =============================================================================
413
- // Dynamic model fetching
414
- // =============================================================================
415
-
416
- async function fetchCloudflareModels(
417
- token: string,
418
- accountId: string,
419
- ): Promise<ProviderModelConfig[]> {
420
- const baseUrl = `https://api.cloudflare.com/client/v4/accounts/${accountId}`;
421
-
422
- try {
423
- const response = await fetchWithRetry(
424
- `${baseUrl}/ai/models`,
425
- {
426
- headers: {
427
- Authorization: `Bearer ${token}`,
428
- "Content-Type": "application/json",
429
- },
430
- },
431
- 3,
432
- 1000,
433
- DEFAULT_FETCH_TIMEOUT_MS,
434
- );
435
-
436
- if (!response.ok) {
437
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
438
- }
439
-
440
- const json = (await response.json()) as {
441
- success?: boolean;
442
- result?: CloudflareModel[];
443
- errors?: Array<{ message: string }>;
444
- };
445
-
446
- if (!json.success || !json.result) {
447
- throw new Error(
448
- json.errors?.[0]?.message || "API returned unsuccessful response",
449
- );
450
- }
451
-
452
- // Filter to chat/text generation models only
453
- const chatModels = json.result.filter((m) => isChatModel(m.id));
454
-
455
- // Map to ProviderModelConfig
456
- const models = chatModels.map((m): ProviderModelConfig => {
457
- const inferred = inferModelMetadata(m.id);
458
-
459
- return {
460
- id: m.id,
461
- name: m.name || inferred.name || m.id,
462
- reasoning: inferred.reasoning || false,
463
- input: inferred.input || ["text"],
464
- cost: inferred.cost || {
465
- input: 0.1,
466
- output: 0.3,
467
- cacheRead: 0,
468
- cacheWrite: 0,
469
- },
470
- contextWindow: inferred.contextWindow || 32768,
471
- maxTokens: inferred.maxTokens || 4096,
472
- compat: inferred.compat || CLOUDFLARE_COMPAT,
473
- };
474
- });
475
-
476
- _logger.info(`[cloudflare] Fetched ${models.length} chat models from API`);
477
- return models;
478
- } catch (error) {
479
- _logger.warn(
480
- `[cloudflare] Failed to fetch models from API: ${error instanceof Error ? error.message : String(error)}`,
481
- );
482
- return [];
483
- }
484
- }
485
-
486
- // =============================================================================
487
- // Extension Entry Point
488
- // =============================================================================
489
-
490
- export default async function cloudflareProvider(pi: ExtensionAPI) {
491
- const { token: apiToken, accountId } = getCloudflareAuth();
492
-
493
- if (!apiToken) {
494
- _logger.info(
495
- "[cloudflare] CF_API_TOKEN or CLOUDFLARE_API_TOKEN not set. Provider will not be available.",
496
- );
497
- return;
498
- }
499
-
500
- if (!accountId) {
501
- _logger.info(
502
- "[cloudflare] CF_ACCOUNT_ID or CLOUDFLARE_ACCOUNT_ID not set. Provider will not be available.",
503
- );
504
- return;
505
- }
506
-
507
- // Try to fetch models dynamically, fall back to hardcoded list
508
- let models = await fetchCloudflareModels(apiToken, accountId);
509
-
510
- if (models.length === 0) {
511
- _logger.info("[cloudflare] Using fallback model list");
512
- models = FALLBACK_MODELS;
513
- }
514
-
515
- pi.registerProvider("cloudflare", {
516
- baseUrl: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`,
517
- apiKey: apiToken,
518
- api: "openai-completions",
519
- authHeader: true,
520
- models,
521
- });
522
-
523
- _logger.info(
524
- `[cloudflare] Provider registered with account: ${accountId.slice(0, 8)}... (${models.length} models)`,
525
- );
526
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * Modal GLM Provider Extension
3
- *
4
- * Provides access to GLM models hosted on Modal's OpenAI-compatible endpoint.
5
- * Requires MODAL_API_KEY (or modal_api_key in ~/.pi/free.json).
6
- *
7
- * Endpoint docs: https://modal.com/glm-5-endpoint
8
- */
9
-
10
- import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
11
- import { applyHidden, PROVIDER_MODAL } from "../../config.ts";
12
- import { BASE_URL_MODAL, URL_MODAL_TOS } from "../../constants.ts";
13
- import { createProvider } from "../../provider-factory.ts";
14
-
15
- function getModalModels(): ProviderModelConfig[] {
16
- return applyHidden(
17
- [
18
- {
19
- id: "zai-org/GLM-5.1-FP8",
20
- name: "GLM-5.1 FP8 (Modal)",
21
- reasoning: true,
22
- input: ["text"],
23
- // Promotional/free-period pricing may change; keep conservative placeholders.
24
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
25
- contextWindow: 128000,
26
- maxTokens: 16384,
27
- },
28
- ],
29
- PROVIDER_MODAL,
30
- );
31
- }
32
-
33
- export default function (pi: Parameters<typeof createProvider>[0]) {
34
- return createProvider(pi, {
35
- providerId: PROVIDER_MODAL,
36
- baseUrl: BASE_URL_MODAL,
37
- apiKeyEnvVar: "MODAL_API_KEY",
38
- apiKeyConfigKey: "modal_api_key",
39
- fetchModels: async () => getModalModels(),
40
- tosUrl: URL_MODAL_TOS,
41
- skipToggle: true, // Modal only has 1 model, no need for toggle
42
- extraHeaders: {
43
- "X-Title": "Pi",
44
- "HTTP-Referer": "https://modal.com/",
45
- },
46
- });
47
- }