pi-free 2.0.2 → 2.0.5
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 +84 -12
- package/README.md +44 -97
- package/banner.svg +132 -0
- package/config.ts +24 -52
- package/constants.ts +6 -0
- package/index.ts +175 -148
- package/lib/built-in-toggle.ts +40 -1
- package/lib/model-detection.ts +176 -139
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +46 -0
- package/lib/registry.ts +200 -144
- package/lib/types.ts +101 -108
- package/lib/util.ts +262 -256
- package/package.json +9 -8
- package/provider-failover/benchmark-lookup.ts +191 -140
- package/provider-helper.ts +19 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline.ts +58 -14
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +260 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +474 -415
- package/providers/ollama/ollama.ts +295 -280
- package/providers/opencode-session.ts +3 -4
- package/providers/qwen/qwen-models.ts +101 -101
- package/providers/qwen/qwen.ts +47 -49
- package/providers/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +71 -55
- package/provider-factory.ts +0 -207
- package/providers/cloudflare/cloudflare.ts +0 -526
- package/providers/modal/modal.ts +0 -47
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dynamic Built-in Provider Fetcher
|
|
3
3
|
*
|
|
4
|
-
* Fetches models dynamically from Pi's built-in providers
|
|
5
|
-
* when the user has configured an API key.
|
|
4
|
+
* Fetches models dynamically from Pi's built-in providers via their
|
|
5
|
+
* standard /models endpoints when the user has configured an API key.
|
|
6
|
+
*
|
|
7
|
+
* Uses a single generic fetch function instead of per-provider boilerplate.
|
|
8
|
+
* Discovery runs concurrently with 1s timeout per provider, fire-and-forget
|
|
9
|
+
* so extension init never blocks. Pi's built-in defaults serve until
|
|
10
|
+
* discovery completes and replaces them.
|
|
6
11
|
*
|
|
7
12
|
* Providers handled:
|
|
8
13
|
* - mistral (MISTRAL_API_KEY)
|
|
9
14
|
* - groq (GROQ_API_KEY)
|
|
10
15
|
* - cerebras (CEREBRAS_API_KEY)
|
|
11
16
|
* - xai (XAI_API_KEY)
|
|
12
|
-
* - huggingface (HF_TOKEN - optional)
|
|
17
|
+
* - huggingface (HF_TOKEN - optional, special-cased API shape)
|
|
13
18
|
*
|
|
14
19
|
* OpenAI is intentionally skipped per user request.
|
|
15
20
|
*/
|
|
@@ -25,249 +30,90 @@ import {
|
|
|
25
30
|
getMistralApiKey,
|
|
26
31
|
getXaiApiKey,
|
|
27
32
|
} from "../../config.ts";
|
|
28
|
-
import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
|
|
29
33
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
|
+
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
30
35
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
31
|
-
import {
|
|
36
|
+
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
32
37
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
33
38
|
|
|
34
39
|
const _logger = createLogger("dynamic-built-in");
|
|
35
40
|
|
|
36
41
|
// =============================================================================
|
|
37
|
-
//
|
|
42
|
+
// Generic Model Fetcher
|
|
38
43
|
// =============================================================================
|
|
39
44
|
|
|
40
|
-
interface
|
|
41
|
-
providerId: string;
|
|
42
|
-
getApiKey: () => string | undefined;
|
|
45
|
+
interface FetchModelsOptions {
|
|
43
46
|
baseUrl: string;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
apiKey: string;
|
|
48
|
+
compat?: ProviderModelConfig["compat"];
|
|
49
|
+
modelDefaults?: Partial<ProviderModelConfig>;
|
|
50
|
+
timeoutMs?: number;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Fetch models from any standard {baseUrl}/models endpoint.
|
|
55
|
+
* Handles both OpenAI-style { object: "list", data: [...] } and plain arrays.
|
|
56
|
+
* Uses AbortSignal.timeout for non-retry, fail-fast behaviour.
|
|
57
|
+
*/
|
|
58
|
+
async function fetchModelsFromEndpoint(
|
|
59
|
+
opts: FetchModelsOptions,
|
|
55
60
|
): Promise<ProviderModelConfig[]> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
3,
|
|
65
|
-
1000,
|
|
66
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
throw new Error(`Mistral API error: ${response.status}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const json = (await response.json()) as {
|
|
74
|
-
data?: Array<{
|
|
75
|
-
id: string;
|
|
76
|
-
name?: string;
|
|
77
|
-
capabilities?: {
|
|
78
|
-
completion_chat?: boolean;
|
|
79
|
-
completion_fim?: boolean;
|
|
80
|
-
function_calling?: boolean;
|
|
81
|
-
vision?: boolean;
|
|
82
|
-
};
|
|
83
|
-
max_context_length?: number;
|
|
84
|
-
}>;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const models = json.data ?? [];
|
|
88
|
-
_logger.info(`[dynamic] Fetched ${models.length} models from Mistral`);
|
|
89
|
-
|
|
90
|
-
return models
|
|
91
|
-
.filter((m) => m.capabilities?.completion_chat) // Only chat models
|
|
92
|
-
.map(
|
|
93
|
-
(m): ProviderModelConfig => ({
|
|
94
|
-
id: m.id,
|
|
95
|
-
name: m.name || m.id,
|
|
96
|
-
reasoning: false, // Mistral doesn't expose this
|
|
97
|
-
input: m.capabilities?.vision ? ["text", "image"] : ["text"],
|
|
98
|
-
cost: {
|
|
99
|
-
// Mistral pricing not exposed via API, use defaults
|
|
100
|
-
input: 0,
|
|
101
|
-
output: 0,
|
|
102
|
-
cacheRead: 0,
|
|
103
|
-
cacheWrite: 0,
|
|
104
|
-
},
|
|
105
|
-
contextWindow: m.max_context_length ?? 32768,
|
|
106
|
-
maxTokens: m.max_context_length
|
|
107
|
-
? Math.floor(m.max_context_length / 2)
|
|
108
|
-
: 4096,
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function fetchGroqModels(apiKey: string): Promise<ProviderModelConfig[]> {
|
|
114
|
-
const response = await fetchWithRetry(
|
|
115
|
-
"https://api.groq.com/openai/v1/models",
|
|
116
|
-
{
|
|
117
|
-
headers: {
|
|
118
|
-
Authorization: `Bearer ${apiKey}`,
|
|
119
|
-
"Content-Type": "application/json",
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
3,
|
|
123
|
-
1000,
|
|
124
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(`Groq API error: ${response.status}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const json = (await response.json()) as {
|
|
132
|
-
data?: Array<{
|
|
133
|
-
id: string;
|
|
134
|
-
object: string;
|
|
135
|
-
owned_by?: string;
|
|
136
|
-
context_window?: number;
|
|
137
|
-
}>;
|
|
61
|
+
let cleanBase = opts.baseUrl;
|
|
62
|
+
while (cleanBase.endsWith("/")) cleanBase = cleanBase.slice(0, -1);
|
|
63
|
+
const url = `${cleanBase}/models`;
|
|
64
|
+
const headers: Record<string, string> = {
|
|
65
|
+
Accept: "application/json",
|
|
66
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
138
67
|
};
|
|
139
68
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
(m): ProviderModelConfig => ({
|
|
145
|
-
id: m.id,
|
|
146
|
-
name: m.id
|
|
147
|
-
.split("-")
|
|
148
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
149
|
-
.join(" "),
|
|
150
|
-
reasoning: false,
|
|
151
|
-
input: ["text"], // Groq models are text-only
|
|
152
|
-
cost: {
|
|
153
|
-
// Groq pricing not exposed via API
|
|
154
|
-
input: 0,
|
|
155
|
-
output: 0,
|
|
156
|
-
cacheRead: 0,
|
|
157
|
-
cacheWrite: 0,
|
|
158
|
-
},
|
|
159
|
-
contextWindow: m.context_window ?? 8192,
|
|
160
|
-
maxTokens: m.context_window ? Math.floor(m.context_window / 2) : 4096,
|
|
161
|
-
}),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function fetchCerebrasModels(
|
|
166
|
-
apiKey: string,
|
|
167
|
-
): Promise<ProviderModelConfig[]> {
|
|
168
|
-
// Cerebras has limited model list, fetch from their API
|
|
169
|
-
const response = await fetchWithRetry(
|
|
170
|
-
"https://api.cerebras.ai/v1/models",
|
|
171
|
-
{
|
|
172
|
-
headers: {
|
|
173
|
-
Authorization: `Bearer ${apiKey}`,
|
|
174
|
-
"Content-Type": "application/json",
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
3,
|
|
178
|
-
1000,
|
|
179
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
180
|
-
);
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
headers,
|
|
71
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 1_000),
|
|
72
|
+
});
|
|
181
73
|
|
|
182
74
|
if (!response.ok) {
|
|
183
|
-
throw new Error(`
|
|
75
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
184
76
|
}
|
|
185
77
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
cost: {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
78
|
+
const body = (await response.json()) as
|
|
79
|
+
| Array<Record<string, unknown>>
|
|
80
|
+
| { data?: Array<Record<string, unknown>> };
|
|
81
|
+
const rawModels: Array<Record<string, unknown>> = Array.isArray(body)
|
|
82
|
+
? body
|
|
83
|
+
: (body.data ?? []);
|
|
84
|
+
|
|
85
|
+
return rawModels.map((m) => {
|
|
86
|
+
const id = String(m.id ?? "");
|
|
87
|
+
const inputModalities = m.input_modalities as string[] | undefined;
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
name: (m.name as string) ?? (m.model as string) ?? id,
|
|
91
|
+
reasoning: !!(m.reasoning ?? false),
|
|
92
|
+
input: inputModalities?.includes("image")
|
|
93
|
+
? (["text", "image"] as const)
|
|
94
|
+
: (["text"] as const),
|
|
95
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
96
|
+
contextWindow:
|
|
97
|
+
((m.max_context_length ?? m.context_window) as number) ??
|
|
98
|
+
opts.modelDefaults?.contextWindow ??
|
|
99
|
+
128_000,
|
|
100
|
+
maxTokens:
|
|
101
|
+
((m.max_tokens ?? m.max_completion_tokens) as number) ??
|
|
102
|
+
opts.modelDefaults?.maxTokens ??
|
|
103
|
+
16_384,
|
|
104
|
+
...opts.modelDefaults,
|
|
105
|
+
...(opts.compat ? { compat: opts.compat } : {}),
|
|
106
|
+
} satisfies ProviderModelConfig;
|
|
107
|
+
});
|
|
215
108
|
}
|
|
216
109
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
headers: {
|
|
222
|
-
Authorization: `Bearer ${apiKey}`,
|
|
223
|
-
"Content-Type": "application/json",
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
3,
|
|
227
|
-
1000,
|
|
228
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
if (!response.ok) {
|
|
232
|
-
throw new Error(`xAI API error: ${response.status}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const json = (await response.json()) as {
|
|
236
|
-
data?: Array<{
|
|
237
|
-
id: string;
|
|
238
|
-
model?: string;
|
|
239
|
-
input_modalities?: string[];
|
|
240
|
-
}>;
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const models = json.data ?? [];
|
|
244
|
-
_logger.info(`[dynamic] Fetched ${models.length} models from xAI`);
|
|
245
|
-
|
|
246
|
-
return models.map(
|
|
247
|
-
(m): ProviderModelConfig => ({
|
|
248
|
-
id: m.id,
|
|
249
|
-
name: m.model || m.id,
|
|
250
|
-
reasoning: false,
|
|
251
|
-
input: m.input_modalities?.includes("image")
|
|
252
|
-
? ["text", "image"]
|
|
253
|
-
: ["text"],
|
|
254
|
-
cost: {
|
|
255
|
-
input: 0,
|
|
256
|
-
output: 0,
|
|
257
|
-
cacheRead: 0,
|
|
258
|
-
cacheWrite: 0,
|
|
259
|
-
},
|
|
260
|
-
contextWindow: 128000, // xAI default
|
|
261
|
-
maxTokens: 4096,
|
|
262
|
-
}),
|
|
263
|
-
);
|
|
264
|
-
}
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Hugging Face (special-cased: non-standard API shape)
|
|
112
|
+
// =============================================================================
|
|
265
113
|
|
|
266
114
|
async function fetchHuggingFaceModels(
|
|
267
115
|
apiKey?: string,
|
|
268
116
|
): Promise<ProviderModelConfig[]> {
|
|
269
|
-
// Hugging Face has a public model list, no auth required for listing
|
|
270
|
-
// But with auth we get better rate limits
|
|
271
117
|
const headers: Record<string, string> = {
|
|
272
118
|
"Content-Type": "application/json",
|
|
273
119
|
};
|
|
@@ -275,56 +121,59 @@ async function fetchHuggingFaceModels(
|
|
|
275
121
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
276
122
|
}
|
|
277
123
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{ headers },
|
|
282
|
-
3,
|
|
283
|
-
1000,
|
|
284
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
124
|
+
const response = await fetch(
|
|
125
|
+
"https://api-inference.huggingface.co/models?pipeline_tag=text-generation&limit=50",
|
|
126
|
+
{ headers, signal: AbortSignal.timeout(1_000) },
|
|
285
127
|
);
|
|
286
128
|
|
|
287
129
|
if (!response.ok) {
|
|
288
130
|
throw new Error(`Hugging Face API error: ${response.status}`);
|
|
289
131
|
}
|
|
290
132
|
|
|
291
|
-
const
|
|
133
|
+
const body = (await response.json()) as Array<{
|
|
292
134
|
id: string;
|
|
293
135
|
modelId?: string;
|
|
294
136
|
}>;
|
|
295
137
|
|
|
296
|
-
const models = Array.isArray(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
name: (m.id || m.modelId || "unknown").split("/").pop() || "Unknown",
|
|
138
|
+
const models = Array.isArray(body) ? body.slice(0, 50) : [];
|
|
139
|
+
return models.map((m): ProviderModelConfig => {
|
|
140
|
+
const id = m.id || m.modelId || "unknown";
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
name: id.split("/").pop() || "Unknown",
|
|
303
144
|
reasoning: false,
|
|
304
145
|
input: ["text"],
|
|
305
|
-
cost: {
|
|
306
|
-
input: 0,
|
|
307
|
-
output: 0,
|
|
308
|
-
cacheRead: 0,
|
|
309
|
-
cacheWrite: 0,
|
|
310
|
-
},
|
|
146
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
311
147
|
contextWindow: 4096,
|
|
312
148
|
maxTokens: 2048,
|
|
313
|
-
}
|
|
314
|
-
);
|
|
149
|
+
};
|
|
150
|
+
});
|
|
315
151
|
}
|
|
316
152
|
|
|
317
153
|
// =============================================================================
|
|
318
|
-
// Provider
|
|
154
|
+
// Provider Definitions
|
|
319
155
|
// =============================================================================
|
|
320
156
|
|
|
321
|
-
|
|
157
|
+
interface DynamicProviderDef {
|
|
158
|
+
providerId: string;
|
|
159
|
+
getApiKey: () => string | undefined;
|
|
160
|
+
baseUrl: string;
|
|
161
|
+
api: "openai-completions" | "mistral-conversations" | "anthropic-messages";
|
|
162
|
+
defaultShowPaid: boolean;
|
|
163
|
+
/** Optional per-provider compat overrides (e.g., DeepSeek proxy). */
|
|
164
|
+
compat?: ProviderModelConfig["compat"];
|
|
165
|
+
/** Per-model field defaults when the API doesn't expose them. */
|
|
166
|
+
modelDefaults?: Partial<ProviderModelConfig>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
322
170
|
{
|
|
323
171
|
providerId: "mistral",
|
|
324
172
|
getApiKey: getMistralApiKey,
|
|
325
173
|
baseUrl: "https://api.mistral.ai/v1",
|
|
326
174
|
api: "openai-completions",
|
|
327
175
|
defaultShowPaid: false,
|
|
176
|
+
modelDefaults: { contextWindow: 32_768, maxTokens: 16_384 },
|
|
328
177
|
},
|
|
329
178
|
{
|
|
330
179
|
providerId: "groq",
|
|
@@ -347,87 +196,190 @@ const DYNAMIC_PROVIDERS: Omit<DynamicProviderConfig, "fetchModels">[] = [
|
|
|
347
196
|
api: "openai-completions",
|
|
348
197
|
defaultShowPaid: false,
|
|
349
198
|
},
|
|
350
|
-
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
// =============================================================================
|
|
202
|
+
// Discovery + Registration per Provider
|
|
203
|
+
// =============================================================================
|
|
204
|
+
|
|
205
|
+
async function discoverAndRegister(
|
|
206
|
+
pi: ExtensionAPI,
|
|
207
|
+
config: DynamicProviderDef,
|
|
208
|
+
apiKey: string,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
let allModels: ProviderModelConfig[];
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
allModels = await fetchModelsFromEndpoint({
|
|
214
|
+
baseUrl: config.baseUrl,
|
|
215
|
+
apiKey,
|
|
216
|
+
compat: config.compat,
|
|
217
|
+
modelDefaults: config.modelDefaults,
|
|
218
|
+
timeoutMs: 1_000,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Apply DeepSeek proxy compat to matching models
|
|
222
|
+
allModels = allModels.map((m) => ({
|
|
223
|
+
...m,
|
|
224
|
+
compat: getProxyModelCompat(m) ?? m.compat,
|
|
225
|
+
}));
|
|
226
|
+
} catch {
|
|
227
|
+
_logger.info(
|
|
228
|
+
`[dynamic] ${config.providerId}: discovery failed, Pi keeps its defaults`,
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await registerProvider(pi, config, allModels, apiKey);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function discoverAndRegisterHF(
|
|
237
|
+
pi: ExtensionAPI,
|
|
238
|
+
apiKey: string,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
const config: DynamicProviderDef = {
|
|
351
241
|
providerId: "huggingface",
|
|
352
242
|
getApiKey: getHfToken,
|
|
353
243
|
baseUrl: "https://api-inference.huggingface.co",
|
|
354
244
|
api: "openai-completions",
|
|
355
245
|
defaultShowPaid: false,
|
|
356
|
-
}
|
|
357
|
-
];
|
|
246
|
+
};
|
|
358
247
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
248
|
+
let allModels: ProviderModelConfig[];
|
|
249
|
+
try {
|
|
250
|
+
allModels = await fetchHuggingFaceModels(apiKey);
|
|
251
|
+
} catch {
|
|
252
|
+
_logger.info(
|
|
253
|
+
"[dynamic] huggingface: discovery failed, Pi keeps its defaults",
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await registerProvider(pi, config, allModels, apiKey);
|
|
259
|
+
}
|
|
370
260
|
|
|
371
261
|
// =============================================================================
|
|
372
|
-
//
|
|
262
|
+
// Registration Logic (sets up toggles, commands, status bar)
|
|
373
263
|
// =============================================================================
|
|
374
264
|
|
|
375
|
-
|
|
265
|
+
async function registerProvider(
|
|
376
266
|
pi: ExtensionAPI,
|
|
267
|
+
config: DynamicProviderDef,
|
|
268
|
+
allModels: ProviderModelConfig[],
|
|
269
|
+
apiKey: string,
|
|
377
270
|
): Promise<void> {
|
|
378
|
-
|
|
271
|
+
const freeModels = allModels.filter((m) =>
|
|
272
|
+
isFreeModel({ ...m, provider: config.providerId }, allModels),
|
|
273
|
+
);
|
|
379
274
|
|
|
380
|
-
|
|
381
|
-
|
|
275
|
+
_logger.info(
|
|
276
|
+
`[dynamic] ${config.providerId}: ${allModels.length} total, ${freeModels.length} free`,
|
|
277
|
+
);
|
|
382
278
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
279
|
+
// Re-register function: called by toggle and initial apply
|
|
280
|
+
const reRegister = (models: ProviderModelConfig[]) => {
|
|
281
|
+
pi.registerProvider(config.providerId, {
|
|
282
|
+
baseUrl: config.baseUrl,
|
|
283
|
+
apiKey,
|
|
284
|
+
api: config.api,
|
|
285
|
+
models: enhanceWithCI(models, config.providerId),
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Toggle state
|
|
290
|
+
const toggleState = createToggleState({
|
|
291
|
+
providerId: config.providerId,
|
|
292
|
+
initialShowPaid: config.defaultShowPaid,
|
|
293
|
+
initialModels: { free: freeModels, all: allModels },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Toggle command
|
|
297
|
+
pi.registerCommand(`toggle-${config.providerId}`, {
|
|
298
|
+
description: `Toggle between free and all ${config.providerId} models`,
|
|
299
|
+
handler: async (_args, ctx) => {
|
|
300
|
+
const applied = toggleState.toggle(reRegister);
|
|
301
|
+
ctx.ui.notify(
|
|
302
|
+
applied.mode === "all"
|
|
303
|
+
? `${config.providerId}: showing all ${allModels.length} models`
|
|
304
|
+
: `${config.providerId}: showing ${freeModels.length} free models`,
|
|
305
|
+
"info",
|
|
386
306
|
);
|
|
387
|
-
|
|
388
|
-
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Global toggle
|
|
311
|
+
registerWithGlobalToggle(
|
|
312
|
+
config.providerId,
|
|
313
|
+
{ free: freeModels, all: allModels },
|
|
314
|
+
reRegister,
|
|
315
|
+
true,
|
|
316
|
+
);
|
|
389
317
|
|
|
390
|
-
|
|
391
|
-
|
|
318
|
+
// Status bar
|
|
319
|
+
const pid = config.providerId;
|
|
320
|
+
pi.on("model_select", (_event, ctx) => {
|
|
321
|
+
if (_event.model?.provider !== pid) {
|
|
322
|
+
ctx.ui.setStatus(`${pid}-status`, undefined);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const f = freeModels.length;
|
|
326
|
+
const t = allModels.length;
|
|
327
|
+
const p = t - f;
|
|
328
|
+
const mode = toggleState.getCurrentMode();
|
|
329
|
+
const status =
|
|
330
|
+
p === 0
|
|
331
|
+
? `${pid}: ${f} free models`
|
|
332
|
+
: mode === "all"
|
|
333
|
+
? `${pid}: ${t} models (free + paid)`
|
|
334
|
+
: `${pid}: ${f} free \u00b7 ${p} paid`;
|
|
335
|
+
ctx.ui.setStatus(`${pid}-status`, `${status} 🔑`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Register models (this swaps in our discovered models over Pi's defaults)
|
|
339
|
+
toggleState.applyCurrent(reRegister);
|
|
340
|
+
_logger.info(`[dynamic] ${config.providerId}: registered`);
|
|
341
|
+
}
|
|
392
342
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// Main Entry — Fire-and-Forget
|
|
345
|
+
// =============================================================================
|
|
396
346
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Kick off model discovery for all configured providers.
|
|
349
|
+
* Runs each fetch concurrently with a 1s timeout so the worst-case
|
|
350
|
+
* wall time is ~1s, not `n * 1s`. Extension init never blocks.
|
|
351
|
+
*
|
|
352
|
+
* Pi's built-in defaults serve until discovery completes and this
|
|
353
|
+
* function replaces them via pi.registerProvider().
|
|
354
|
+
*/
|
|
355
|
+
export async function setupDynamicBuiltInProviders(
|
|
356
|
+
pi: ExtensionAPI,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const fetchers: Promise<void>[] = [];
|
|
400
359
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
api: config.api,
|
|
407
|
-
models: enhanceWithCI(models, config.providerId),
|
|
408
|
-
});
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
// Register with global toggle
|
|
412
|
-
registerWithGlobalToggle(
|
|
413
|
-
config.providerId,
|
|
414
|
-
{ free: freeModels, all: allModels },
|
|
415
|
-
reRegister,
|
|
416
|
-
true, // hasKey
|
|
417
|
-
);
|
|
360
|
+
for (const config of DYNAMIC_PROVIDERS) {
|
|
361
|
+
const apiKey = config.getApiKey();
|
|
362
|
+
if (!apiKey) continue;
|
|
363
|
+
fetchers.push(discoverAndRegister(pi, config, apiKey));
|
|
364
|
+
}
|
|
418
365
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
_logger.info(`[dynamic] ${config.providerId}: registered successfully`);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
_logger.error(
|
|
426
|
-
`[dynamic] Failed to setup ${config.providerId}`,
|
|
427
|
-
error instanceof Error
|
|
428
|
-
? { error: error.message }
|
|
429
|
-
: { error: String(error) },
|
|
430
|
-
);
|
|
431
|
-
}
|
|
366
|
+
const hfKey = getHfToken();
|
|
367
|
+
if (hfKey) {
|
|
368
|
+
fetchers.push(discoverAndRegisterHF(pi, hfKey));
|
|
432
369
|
}
|
|
370
|
+
|
|
371
|
+
if (fetchers.length === 0) return;
|
|
372
|
+
|
|
373
|
+
_logger.info(
|
|
374
|
+
`[dynamic] Kicking off discovery for ${fetchers.length} providers (1s timeout each, concurrent)...`,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// Fire-and-forget: log results, never block init
|
|
378
|
+
void Promise.allSettled(fetchers).then((results) => {
|
|
379
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
380
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
381
|
+
_logger.info(
|
|
382
|
+
`[dynamic] Discovery complete: ${succeeded} succeeded, ${failed} failed/rejected`,
|
|
383
|
+
);
|
|
384
|
+
});
|
|
433
385
|
}
|