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.
- package/CHANGELOG.md +37 -0
- package/README.md +393 -416
- package/config.ts +6 -2
- package/constants.ts +1 -0
- package/index.ts +6 -44
- package/lib/built-in-toggle.ts +206 -0
- package/package.json +67 -67
- package/provider-helper.ts +260 -260
- package/providers/cline/cline-models.ts +1 -1
- package/providers/cline/cline.ts +5 -7
- package/providers/dynamic-built-in/index.ts +432 -513
- package/providers/kilo/kilo.ts +5 -5
- package/providers/nvidia/nvidia.ts +1 -1
- package/providers/ollama/ollama.ts +3 -3
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} from "../../
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
context_window
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
}
|