pi-free 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,513 +1,432 @@
1
- /**
2
- * Dynamic Built-in Provider Fetcher
3
- *
4
- * Fetches models dynamically from Pi's built-in providers
5
- * when the user has configured an API key.
6
- *
7
- * Providers handled:
8
- * - mistral (MISTRAL_API_KEY)
9
- * - groq (GROQ_API_KEY)
10
- * - cerebras (CEREBRAS_API_KEY)
11
- * - xai (XAI_API_KEY)
12
- * - huggingface (HF_TOKEN - optional)
13
- *
14
- * OpenAI is intentionally skipped per user request.
15
- */
16
-
17
- import type {
18
- ExtensionAPI,
19
- ProviderModelConfig,
20
- } from "@mariozechner/pi-coding-agent";
21
- import {
22
- getCerebrasApiKey,
23
- getGroqApiKey,
24
- getHfToken,
25
- getMistralApiKey,
26
- getOpenrouterApiKey,
27
- getOpenrouterShowPaid,
28
- getXaiApiKey,
29
- saveConfig,
30
- } from "../../config.ts";
31
- import {
32
- BASE_URL_OPENROUTER,
33
- DEFAULT_FETCH_TIMEOUT_MS,
34
- } from "../../constants.ts";
35
- import { createLogger } from "../../lib/logger.ts";
36
- import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
37
- import { fetchWithRetry } from "../../lib/util.ts";
38
- import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
39
-
40
- const _logger = createLogger("dynamic-built-in");
41
-
42
- // =============================================================================
43
- // OpenRouter Fetcher
44
- // =============================================================================
45
-
46
- async function fetchOpenRouterModels(
47
- apiKey: string,
48
- ): Promise<ProviderModelConfig[]> {
49
- const models = await fetchOpenRouterCompatibleModels({
50
- baseUrl: BASE_URL_OPENROUTER,
51
- apiKey: apiKey || undefined,
52
- freeOnly: false,
53
- });
54
- _logger.info(`[dynamic] Fetched ${models.length} models from OpenRouter`);
55
- return models;
56
- }
57
-
58
- // =============================================================================
59
- // Provider Configurations
60
- // =============================================================================
61
-
62
- interface DynamicProviderConfig {
63
- providerId: string;
64
- getApiKey: () => string | undefined;
65
- baseUrl: string;
66
- api: "openai-completions" | "mistral-conversations" | "anthropic-messages";
67
- fetchModels: (apiKey: string) => Promise<ProviderModelConfig[]>;
68
- defaultShowPaid: boolean;
69
- }
70
-
71
- // =============================================================================
72
- // Fetch Functions for Each Provider
73
- // =============================================================================
74
-
75
- async function fetchMistralModels(
76
- apiKey: string,
77
- ): Promise<ProviderModelConfig[]> {
78
- const response = await fetchWithRetry(
79
- "https://api.mistral.ai/v1/models",
80
- {
81
- headers: {
82
- Authorization: `Bearer ${apiKey}`,
83
- "Content-Type": "application/json",
84
- },
85
- },
86
- 3,
87
- 1000,
88
- DEFAULT_FETCH_TIMEOUT_MS,
89
- );
90
-
91
- if (!response.ok) {
92
- throw new Error(`Mistral API error: ${response.status}`);
93
- }
94
-
95
- const json = (await response.json()) as {
96
- data?: Array<{
97
- id: string;
98
- name?: string;
99
- capabilities?: {
100
- completion_chat?: boolean;
101
- completion_fim?: boolean;
102
- function_calling?: boolean;
103
- vision?: boolean;
104
- };
105
- max_context_length?: number;
106
- }>;
107
- };
108
-
109
- const models = json.data ?? [];
110
- _logger.info(`[dynamic] Fetched ${models.length} models from Mistral`);
111
-
112
- return models
113
- .filter((m) => m.capabilities?.completion_chat) // Only chat models
114
- .map(
115
- (m): ProviderModelConfig => ({
116
- id: m.id,
117
- name: m.name || m.id,
118
- reasoning: false, // Mistral doesn't expose this
119
- input: m.capabilities?.vision ? ["text", "image"] : ["text"],
120
- cost: {
121
- // Mistral pricing not exposed via API, use defaults
122
- input: 0,
123
- output: 0,
124
- cacheRead: 0,
125
- cacheWrite: 0,
126
- },
127
- contextWindow: m.max_context_length ?? 32768,
128
- maxTokens: m.max_context_length
129
- ? Math.floor(m.max_context_length / 2)
130
- : 4096,
131
- }),
132
- );
133
- }
134
-
135
- async function fetchGroqModels(apiKey: string): Promise<ProviderModelConfig[]> {
136
- const response = await fetchWithRetry(
137
- "https://api.groq.com/openai/v1/models",
138
- {
139
- headers: {
140
- Authorization: `Bearer ${apiKey}`,
141
- "Content-Type": "application/json",
142
- },
143
- },
144
- 3,
145
- 1000,
146
- DEFAULT_FETCH_TIMEOUT_MS,
147
- );
148
-
149
- if (!response.ok) {
150
- throw new Error(`Groq API error: ${response.status}`);
151
- }
152
-
153
- const json = (await response.json()) as {
154
- data?: Array<{
155
- id: string;
156
- object: string;
157
- owned_by?: string;
158
- context_window?: number;
159
- }>;
160
- };
161
-
162
- const models = json.data?.filter((m) => m.object === "model") ?? [];
163
- _logger.info(`[dynamic] Fetched ${models.length} models from Groq`);
164
-
165
- return models.map(
166
- (m): ProviderModelConfig => ({
167
- id: m.id,
168
- name: m.id
169
- .split("-")
170
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
171
- .join(" "),
172
- reasoning: false,
173
- input: ["text"], // Groq models are text-only
174
- cost: {
175
- // Groq pricing not exposed via API
176
- input: 0,
177
- output: 0,
178
- cacheRead: 0,
179
- cacheWrite: 0,
180
- },
181
- contextWindow: m.context_window ?? 8192,
182
- maxTokens: m.context_window ? Math.floor(m.context_window / 2) : 4096,
183
- }),
184
- );
185
- }
186
-
187
- async function fetchCerebrasModels(
188
- apiKey: string,
189
- ): Promise<ProviderModelConfig[]> {
190
- // Cerebras has limited model list, fetch from their API
191
- const response = await fetchWithRetry(
192
- "https://api.cerebras.ai/v1/models",
193
- {
194
- headers: {
195
- Authorization: `Bearer ${apiKey}`,
196
- "Content-Type": "application/json",
197
- },
198
- },
199
- 3,
200
- 1000,
201
- DEFAULT_FETCH_TIMEOUT_MS,
202
- );
203
-
204
- if (!response.ok) {
205
- throw new Error(`Cerebras API error: ${response.status}`);
206
- }
207
-
208
- const json = (await response.json()) as {
209
- data?: Array<{
210
- model?: string;
211
- model_type?: string;
212
- max_context_length?: number;
213
- }>;
214
- };
215
-
216
- const models = json.data ?? [];
217
- _logger.info(`[dynamic] Fetched ${models.length} models from Cerebras`);
218
-
219
- return models.map(
220
- (m): ProviderModelConfig => ({
221
- id: m.model || "unknown",
222
- name: m.model || "Unknown",
223
- reasoning: false,
224
- input: ["text"],
225
- cost: {
226
- input: 0,
227
- output: 0,
228
- cacheRead: 0,
229
- cacheWrite: 0,
230
- },
231
- contextWindow: m.max_context_length ?? 8192,
232
- maxTokens: m.max_context_length
233
- ? Math.floor(m.max_context_length / 2)
234
- : 4096,
235
- }),
236
- );
237
- }
238
-
239
- async function fetchXAIModels(apiKey: string): Promise<ProviderModelConfig[]> {
240
- const response = await fetchWithRetry(
241
- "https://api.x.ai/v1/models",
242
- {
243
- headers: {
244
- Authorization: `Bearer ${apiKey}`,
245
- "Content-Type": "application/json",
246
- },
247
- },
248
- 3,
249
- 1000,
250
- DEFAULT_FETCH_TIMEOUT_MS,
251
- );
252
-
253
- if (!response.ok) {
254
- throw new Error(`xAI API error: ${response.status}`);
255
- }
256
-
257
- const json = (await response.json()) as {
258
- data?: Array<{
259
- id: string;
260
- model?: string;
261
- input_modalities?: string[];
262
- }>;
263
- };
264
-
265
- const models = json.data ?? [];
266
- _logger.info(`[dynamic] Fetched ${models.length} models from xAI`);
267
-
268
- return models.map(
269
- (m): ProviderModelConfig => ({
270
- id: m.id,
271
- name: m.model || m.id,
272
- reasoning: false,
273
- input: m.input_modalities?.includes("image")
274
- ? ["text", "image"]
275
- : ["text"],
276
- cost: {
277
- input: 0,
278
- output: 0,
279
- cacheRead: 0,
280
- cacheWrite: 0,
281
- },
282
- contextWindow: 128000, // xAI default
283
- maxTokens: 4096,
284
- }),
285
- );
286
- }
287
-
288
- async function fetchHuggingFaceModels(
289
- apiKey?: string,
290
- ): Promise<ProviderModelConfig[]> {
291
- // Hugging Face has a public model list, no auth required for listing
292
- // But with auth we get better rate limits
293
- const headers: Record<string, string> = {
294
- "Content-Type": "application/json",
295
- };
296
- if (apiKey) {
297
- headers.Authorization = `Bearer ${apiKey}`;
298
- }
299
-
300
- // Hugging Face inference API models endpoint
301
- const response = await fetchWithRetry(
302
- "https://api-inference.huggingface.co/models?pipeline_tag=text-generation&limit=100",
303
- { headers },
304
- 3,
305
- 1000,
306
- DEFAULT_FETCH_TIMEOUT_MS,
307
- );
308
-
309
- if (!response.ok) {
310
- throw new Error(`Hugging Face API error: ${response.status}`);
311
- }
312
-
313
- const json = (await response.json()) as Array<{
314
- id: string;
315
- modelId?: string;
316
- }>;
317
-
318
- const models = Array.isArray(json) ? json.slice(0, 50) : []; // Limit to 50
319
- _logger.info(`[dynamic] Fetched ${models.length} models from Hugging Face`);
320
-
321
- return models.map(
322
- (m): ProviderModelConfig => ({
323
- id: m.id || m.modelId || "unknown",
324
- name: (m.id || m.modelId || "unknown").split("/").pop() || "Unknown",
325
- reasoning: false,
326
- input: ["text"],
327
- cost: {
328
- input: 0,
329
- output: 0,
330
- cacheRead: 0,
331
- cacheWrite: 0,
332
- },
333
- contextWindow: 4096,
334
- maxTokens: 2048,
335
- }),
336
- );
337
- }
338
-
339
- // =============================================================================
340
- // Provider Configurations Map
341
- // =============================================================================
342
-
343
- const DYNAMIC_PROVIDERS: Omit<DynamicProviderConfig, "fetchModels">[] = [
344
- {
345
- providerId: "mistral",
346
- getApiKey: getMistralApiKey,
347
- baseUrl: "https://api.mistral.ai/v1",
348
- api: "openai-completions",
349
- defaultShowPaid: false,
350
- },
351
- {
352
- providerId: "groq",
353
- getApiKey: getGroqApiKey,
354
- baseUrl: "https://api.groq.com/openai/v1",
355
- api: "openai-completions",
356
- defaultShowPaid: false,
357
- },
358
- {
359
- providerId: "cerebras",
360
- getApiKey: getCerebrasApiKey,
361
- baseUrl: "https://api.cerebras.ai/v1",
362
- api: "openai-completions",
363
- defaultShowPaid: false,
364
- },
365
- {
366
- providerId: "xai",
367
- getApiKey: getXaiApiKey,
368
- baseUrl: "https://api.x.ai/v1",
369
- api: "openai-completions",
370
- defaultShowPaid: false,
371
- },
372
- {
373
- providerId: "huggingface",
374
- getApiKey: getHfToken,
375
- baseUrl: "https://api-inference.huggingface.co",
376
- api: "openai-completions",
377
- defaultShowPaid: false,
378
- },
379
- {
380
- providerId: "openrouter",
381
- getApiKey: getOpenrouterApiKey,
382
- baseUrl: BASE_URL_OPENROUTER,
383
- api: "openai-completions",
384
- defaultShowPaid: false,
385
- },
386
- ];
387
-
388
- // Map provider IDs to their fetch functions
389
- const FETCH_FUNCTIONS: Record<
390
- string,
391
- (apiKey: string) => Promise<ProviderModelConfig[]>
392
- > = {
393
- mistral: fetchMistralModels,
394
- groq: fetchGroqModels,
395
- cerebras: fetchCerebrasModels,
396
- xai: fetchXAIModels,
397
- huggingface: fetchHuggingFaceModels,
398
- openrouter: fetchOpenRouterModels,
399
- };
400
-
401
- // Providers that support free/paid toggling (have pricing info in API)
402
- // OpenRouter exposes actual pricing
403
- const TOGGLEABLE_PROVIDERS = new Set(["openrouter"]);
404
-
405
- // =============================================================================
406
- // Main Setup Function
407
- // =============================================================================
408
-
409
- export async function setupDynamicBuiltInProviders(
410
- pi: ExtensionAPI,
411
- ): Promise<void> {
412
- _logger.info("[dynamic] Setting up dynamic built-in providers...");
413
-
414
- for (const config of DYNAMIC_PROVIDERS) {
415
- const apiKey = config.getApiKey();
416
-
417
- if (!apiKey) {
418
- _logger.info(
419
- `[dynamic] Skipping ${config.providerId} - no API key configured`,
420
- );
421
- continue;
422
- }
423
-
424
- try {
425
- _logger.info(`[dynamic] Fetching models for ${config.providerId}...`);
426
-
427
- // Fetch models
428
- const allModels = await FETCH_FUNCTIONS[config.providerId](apiKey);
429
- const freeModels = allModels.filter(isFreeModel);
430
-
431
- _logger.info(
432
- `[dynamic] ${config.providerId}: ${allModels.length} total, ${freeModels.length} free`,
433
- );
434
-
435
- // Create re-register function for global toggle
436
- const reRegister = (models: ProviderModelConfig[]) => {
437
- pi.registerProvider(config.providerId, {
438
- baseUrl: config.baseUrl,
439
- apiKey,
440
- api: config.api,
441
- models,
442
- });
443
- };
444
-
445
- // Register with global toggle
446
- registerWithGlobalToggle(
447
- config.providerId,
448
- { free: freeModels, all: allModels },
449
- reRegister,
450
- true, // hasKey
451
- );
452
-
453
- // Initial registration (default to free)
454
- const initialModels = config.defaultShowPaid ? allModels : freeModels;
455
- reRegister(initialModels);
456
-
457
- // Register toggle command only for providers with pricing info
458
- if (TOGGLEABLE_PROVIDERS.has(config.providerId)) {
459
- const initialShowPaid = getOpenrouterShowPaid();
460
- setupProviderToggle(
461
- pi,
462
- config.providerId,
463
- freeModels,
464
- allModels,
465
- reRegister,
466
- initialShowPaid,
467
- );
468
- }
469
-
470
- _logger.info(`[dynamic] ${config.providerId}: registered successfully`);
471
- } catch (error) {
472
- _logger.error(
473
- `[dynamic] Failed to setup ${config.providerId}`,
474
- error instanceof Error
475
- ? { error: error.message }
476
- : { error: String(error) },
477
- );
478
- }
479
- }
480
- }
481
-
482
- // =============================================================================
483
- // Per-Provider Toggle Command
484
- // =============================================================================
485
-
486
- function setupProviderToggle(
487
- pi: ExtensionAPI,
488
- providerId: string,
489
- freeModels: ProviderModelConfig[],
490
- allModels: ProviderModelConfig[],
491
- reRegister: (models: ProviderModelConfig[]) => void,
492
- initialShowPaid = false,
493
- ): void {
494
- let showPaid = initialShowPaid;
495
-
496
- pi.registerCommand(`${providerId}-toggle`, {
497
- description: `Toggle free/paid ${providerId} models`,
498
- handler: async (_args, ctx) => {
499
- showPaid = !showPaid;
500
- const modelsToShow = showPaid ? allModels : freeModels;
501
- reRegister(modelsToShow);
502
-
503
- // Persist to config for openrouter
504
- if (providerId === "openrouter") {
505
- saveConfig({ openrouter_show_paid: showPaid });
506
- }
507
-
508
- const count = modelsToShow.length;
509
- const type = showPaid ? "paid" : "free";
510
- ctx.ui.notify(`${providerId}: ${count} ${type} models`, "info");
511
- },
512
- });
513
- }
1
+ /**
2
+ * Dynamic Built-in Provider Fetcher
3
+ *
4
+ * Fetches models dynamically from Pi's built-in providers
5
+ * when the user has configured an API key.
6
+ *
7
+ * Providers handled:
8
+ * - mistral (MISTRAL_API_KEY)
9
+ * - groq (GROQ_API_KEY)
10
+ * - cerebras (CEREBRAS_API_KEY)
11
+ * - xai (XAI_API_KEY)
12
+ * - huggingface (HF_TOKEN - optional)
13
+ *
14
+ * OpenAI is intentionally skipped per user request.
15
+ */
16
+
17
+ import type {
18
+ ExtensionAPI,
19
+ ProviderModelConfig,
20
+ } from "@mariozechner/pi-coding-agent";
21
+ import {
22
+ getCerebrasApiKey,
23
+ getGroqApiKey,
24
+ getHfToken,
25
+ getMistralApiKey,
26
+ getXaiApiKey,
27
+ } from "../../config.ts";
28
+ import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
29
+ import { createLogger } from "../../lib/logger.ts";
30
+ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
31
+ import { fetchWithRetry } from "../../lib/util.ts";
32
+
33
+ const _logger = createLogger("dynamic-built-in");
34
+
35
+ // =============================================================================
36
+ // Provider Configurations
37
+ // =============================================================================
38
+
39
+ interface DynamicProviderConfig {
40
+ providerId: string;
41
+ getApiKey: () => string | undefined;
42
+ baseUrl: string;
43
+ api: "openai-completions" | "mistral-conversations" | "anthropic-messages";
44
+ fetchModels: (apiKey: string) => Promise<ProviderModelConfig[]>;
45
+ defaultShowPaid: boolean;
46
+ }
47
+
48
+ // =============================================================================
49
+ // Fetch Functions for Each Provider
50
+ // =============================================================================
51
+
52
+ async function fetchMistralModels(
53
+ apiKey: string,
54
+ ): Promise<ProviderModelConfig[]> {
55
+ const response = await fetchWithRetry(
56
+ "https://api.mistral.ai/v1/models",
57
+ {
58
+ headers: {
59
+ Authorization: `Bearer ${apiKey}`,
60
+ "Content-Type": "application/json",
61
+ },
62
+ },
63
+ 3,
64
+ 1000,
65
+ DEFAULT_FETCH_TIMEOUT_MS,
66
+ );
67
+
68
+ if (!response.ok) {
69
+ throw new Error(`Mistral API error: ${response.status}`);
70
+ }
71
+
72
+ const json = (await response.json()) as {
73
+ data?: Array<{
74
+ id: string;
75
+ name?: string;
76
+ capabilities?: {
77
+ completion_chat?: boolean;
78
+ completion_fim?: boolean;
79
+ function_calling?: boolean;
80
+ vision?: boolean;
81
+ };
82
+ max_context_length?: number;
83
+ }>;
84
+ };
85
+
86
+ const models = json.data ?? [];
87
+ _logger.info(`[dynamic] Fetched ${models.length} models from Mistral`);
88
+
89
+ return models
90
+ .filter((m) => m.capabilities?.completion_chat) // Only chat models
91
+ .map(
92
+ (m): ProviderModelConfig => ({
93
+ id: m.id,
94
+ name: m.name || m.id,
95
+ reasoning: false, // Mistral doesn't expose this
96
+ input: m.capabilities?.vision ? ["text", "image"] : ["text"],
97
+ cost: {
98
+ // Mistral pricing not exposed via API, use defaults
99
+ input: 0,
100
+ output: 0,
101
+ cacheRead: 0,
102
+ cacheWrite: 0,
103
+ },
104
+ contextWindow: m.max_context_length ?? 32768,
105
+ maxTokens: m.max_context_length
106
+ ? Math.floor(m.max_context_length / 2)
107
+ : 4096,
108
+ }),
109
+ );
110
+ }
111
+
112
+ async function fetchGroqModels(apiKey: string): Promise<ProviderModelConfig[]> {
113
+ const response = await fetchWithRetry(
114
+ "https://api.groq.com/openai/v1/models",
115
+ {
116
+ headers: {
117
+ Authorization: `Bearer ${apiKey}`,
118
+ "Content-Type": "application/json",
119
+ },
120
+ },
121
+ 3,
122
+ 1000,
123
+ DEFAULT_FETCH_TIMEOUT_MS,
124
+ );
125
+
126
+ if (!response.ok) {
127
+ throw new Error(`Groq API error: ${response.status}`);
128
+ }
129
+
130
+ const json = (await response.json()) as {
131
+ data?: Array<{
132
+ id: string;
133
+ object: string;
134
+ owned_by?: string;
135
+ context_window?: number;
136
+ }>;
137
+ };
138
+
139
+ const models = json.data?.filter((m) => m.object === "model") ?? [];
140
+ _logger.info(`[dynamic] Fetched ${models.length} models from Groq`);
141
+
142
+ return models.map(
143
+ (m): ProviderModelConfig => ({
144
+ id: m.id,
145
+ name: m.id
146
+ .split("-")
147
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
148
+ .join(" "),
149
+ reasoning: false,
150
+ input: ["text"], // Groq models are text-only
151
+ cost: {
152
+ // Groq pricing not exposed via API
153
+ input: 0,
154
+ output: 0,
155
+ cacheRead: 0,
156
+ cacheWrite: 0,
157
+ },
158
+ contextWindow: m.context_window ?? 8192,
159
+ maxTokens: m.context_window ? Math.floor(m.context_window / 2) : 4096,
160
+ }),
161
+ );
162
+ }
163
+
164
+ async function fetchCerebrasModels(
165
+ apiKey: string,
166
+ ): Promise<ProviderModelConfig[]> {
167
+ // Cerebras has limited model list, fetch from their API
168
+ const response = await fetchWithRetry(
169
+ "https://api.cerebras.ai/v1/models",
170
+ {
171
+ headers: {
172
+ Authorization: `Bearer ${apiKey}`,
173
+ "Content-Type": "application/json",
174
+ },
175
+ },
176
+ 3,
177
+ 1000,
178
+ DEFAULT_FETCH_TIMEOUT_MS,
179
+ );
180
+
181
+ if (!response.ok) {
182
+ throw new Error(`Cerebras API error: ${response.status}`);
183
+ }
184
+
185
+ const json = (await response.json()) as {
186
+ data?: Array<{
187
+ model?: string;
188
+ model_type?: string;
189
+ max_context_length?: number;
190
+ }>;
191
+ };
192
+
193
+ const models = json.data ?? [];
194
+ _logger.info(`[dynamic] Fetched ${models.length} models from Cerebras`);
195
+
196
+ return models.map(
197
+ (m): ProviderModelConfig => ({
198
+ id: m.model || "unknown",
199
+ name: m.model || "Unknown",
200
+ reasoning: false,
201
+ input: ["text"],
202
+ cost: {
203
+ input: 0,
204
+ output: 0,
205
+ cacheRead: 0,
206
+ cacheWrite: 0,
207
+ },
208
+ contextWindow: m.max_context_length ?? 8192,
209
+ maxTokens: m.max_context_length
210
+ ? Math.floor(m.max_context_length / 2)
211
+ : 4096,
212
+ }),
213
+ );
214
+ }
215
+
216
+ async function fetchXAIModels(apiKey: string): Promise<ProviderModelConfig[]> {
217
+ const response = await fetchWithRetry(
218
+ "https://api.x.ai/v1/models",
219
+ {
220
+ headers: {
221
+ Authorization: `Bearer ${apiKey}`,
222
+ "Content-Type": "application/json",
223
+ },
224
+ },
225
+ 3,
226
+ 1000,
227
+ DEFAULT_FETCH_TIMEOUT_MS,
228
+ );
229
+
230
+ if (!response.ok) {
231
+ throw new Error(`xAI API error: ${response.status}`);
232
+ }
233
+
234
+ const json = (await response.json()) as {
235
+ data?: Array<{
236
+ id: string;
237
+ model?: string;
238
+ input_modalities?: string[];
239
+ }>;
240
+ };
241
+
242
+ const models = json.data ?? [];
243
+ _logger.info(`[dynamic] Fetched ${models.length} models from xAI`);
244
+
245
+ return models.map(
246
+ (m): ProviderModelConfig => ({
247
+ id: m.id,
248
+ name: m.model || m.id,
249
+ reasoning: false,
250
+ input: m.input_modalities?.includes("image")
251
+ ? ["text", "image"]
252
+ : ["text"],
253
+ cost: {
254
+ input: 0,
255
+ output: 0,
256
+ cacheRead: 0,
257
+ cacheWrite: 0,
258
+ },
259
+ contextWindow: 128000, // xAI default
260
+ maxTokens: 4096,
261
+ }),
262
+ );
263
+ }
264
+
265
+ async function fetchHuggingFaceModels(
266
+ apiKey?: string,
267
+ ): Promise<ProviderModelConfig[]> {
268
+ // Hugging Face has a public model list, no auth required for listing
269
+ // But with auth we get better rate limits
270
+ const headers: Record<string, string> = {
271
+ "Content-Type": "application/json",
272
+ };
273
+ if (apiKey) {
274
+ headers.Authorization = `Bearer ${apiKey}`;
275
+ }
276
+
277
+ // Hugging Face inference API models endpoint
278
+ const response = await fetchWithRetry(
279
+ "https://api-inference.huggingface.co/models?pipeline_tag=text-generation&limit=100",
280
+ { headers },
281
+ 3,
282
+ 1000,
283
+ DEFAULT_FETCH_TIMEOUT_MS,
284
+ );
285
+
286
+ if (!response.ok) {
287
+ throw new Error(`Hugging Face API error: ${response.status}`);
288
+ }
289
+
290
+ const json = (await response.json()) as Array<{
291
+ id: string;
292
+ modelId?: string;
293
+ }>;
294
+
295
+ const models = Array.isArray(json) ? json.slice(0, 50) : []; // Limit to 50
296
+ _logger.info(`[dynamic] Fetched ${models.length} models from Hugging Face`);
297
+
298
+ return models.map(
299
+ (m): ProviderModelConfig => ({
300
+ id: m.id || m.modelId || "unknown",
301
+ name: (m.id || m.modelId || "unknown").split("/").pop() || "Unknown",
302
+ reasoning: false,
303
+ input: ["text"],
304
+ cost: {
305
+ input: 0,
306
+ output: 0,
307
+ cacheRead: 0,
308
+ cacheWrite: 0,
309
+ },
310
+ contextWindow: 4096,
311
+ maxTokens: 2048,
312
+ }),
313
+ );
314
+ }
315
+
316
+ // =============================================================================
317
+ // Provider Configurations Map
318
+ // =============================================================================
319
+
320
+ const DYNAMIC_PROVIDERS: Omit<DynamicProviderConfig, "fetchModels">[] = [
321
+ {
322
+ providerId: "mistral",
323
+ getApiKey: getMistralApiKey,
324
+ baseUrl: "https://api.mistral.ai/v1",
325
+ api: "openai-completions",
326
+ defaultShowPaid: false,
327
+ },
328
+ {
329
+ providerId: "groq",
330
+ getApiKey: getGroqApiKey,
331
+ baseUrl: "https://api.groq.com/openai/v1",
332
+ api: "openai-completions",
333
+ defaultShowPaid: false,
334
+ },
335
+ {
336
+ providerId: "cerebras",
337
+ getApiKey: getCerebrasApiKey,
338
+ baseUrl: "https://api.cerebras.ai/v1",
339
+ api: "openai-completions",
340
+ defaultShowPaid: false,
341
+ },
342
+ {
343
+ providerId: "xai",
344
+ getApiKey: getXaiApiKey,
345
+ baseUrl: "https://api.x.ai/v1",
346
+ api: "openai-completions",
347
+ defaultShowPaid: false,
348
+ },
349
+ {
350
+ providerId: "huggingface",
351
+ getApiKey: getHfToken,
352
+ baseUrl: "https://api-inference.huggingface.co",
353
+ api: "openai-completions",
354
+ defaultShowPaid: false,
355
+ },
356
+ ];
357
+
358
+ // Map provider IDs to their fetch functions
359
+ const FETCH_FUNCTIONS: Record<
360
+ string,
361
+ (apiKey: string) => Promise<ProviderModelConfig[]>
362
+ > = {
363
+ mistral: fetchMistralModels,
364
+ groq: fetchGroqModels,
365
+ cerebras: fetchCerebrasModels,
366
+ xai: fetchXAIModels,
367
+ huggingface: fetchHuggingFaceModels,
368
+ };
369
+
370
+ // =============================================================================
371
+ // Main Setup Function
372
+ // =============================================================================
373
+
374
+ export async function setupDynamicBuiltInProviders(
375
+ pi: ExtensionAPI,
376
+ ): Promise<void> {
377
+ _logger.info("[dynamic] Setting up dynamic built-in providers...");
378
+
379
+ for (const config of DYNAMIC_PROVIDERS) {
380
+ const apiKey = config.getApiKey();
381
+
382
+ if (!apiKey) {
383
+ _logger.info(
384
+ `[dynamic] Skipping ${config.providerId} - no API key configured`,
385
+ );
386
+ continue;
387
+ }
388
+
389
+ try {
390
+ _logger.info(`[dynamic] Fetching models for ${config.providerId}...`);
391
+
392
+ // Fetch models
393
+ const allModels = await FETCH_FUNCTIONS[config.providerId](apiKey);
394
+ const freeModels = allModels.filter(isFreeModel);
395
+
396
+ _logger.info(
397
+ `[dynamic] ${config.providerId}: ${allModels.length} total, ${freeModels.length} free`,
398
+ );
399
+
400
+ // Create re-register function for global toggle
401
+ const reRegister = (models: ProviderModelConfig[]) => {
402
+ pi.registerProvider(config.providerId, {
403
+ baseUrl: config.baseUrl,
404
+ apiKey,
405
+ api: config.api,
406
+ models,
407
+ });
408
+ };
409
+
410
+ // Register with global toggle
411
+ registerWithGlobalToggle(
412
+ config.providerId,
413
+ { free: freeModels, all: allModels },
414
+ reRegister,
415
+ true, // hasKey
416
+ );
417
+
418
+ // Initial registration (default to free)
419
+ const initialModels = config.defaultShowPaid ? allModels : freeModels;
420
+ reRegister(initialModels);
421
+
422
+ _logger.info(`[dynamic] ${config.providerId}: registered successfully`);
423
+ } catch (error) {
424
+ _logger.error(
425
+ `[dynamic] Failed to setup ${config.providerId}`,
426
+ error instanceof Error
427
+ ? { error: error.message }
428
+ : { error: String(error) },
429
+ );
430
+ }
431
+ }
432
+ }