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