pi-free 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -12
- package/README.md +44 -97
- 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-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/types.ts +101 -108
- package/lib/util.ts +256 -256
- package/package.json +8 -8
- package/provider-failover/benchmark-lookup.ts +2 -2
- package/provider-helper.ts +19 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline.ts +31 -4
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +258 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +476 -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/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +64 -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,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";
|
|
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
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Authorization: `Bearer ${apiKey}`,
|
|
61
|
-
"Content-Type": "application/json",
|
|
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
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}/models`;
|
|
62
|
+
const headers: Record<string, string> = {
|
|
63
|
+
Accept: "application/json",
|
|
64
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
138
65
|
};
|
|
139
66
|
|
|
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
|
-
);
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
headers,
|
|
69
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 1_000),
|
|
70
|
+
});
|
|
181
71
|
|
|
182
72
|
if (!response.ok) {
|
|
183
|
-
throw new Error(`
|
|
73
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
184
74
|
}
|
|
185
75
|
|
|
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
|
-
|
|
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
|
+
});
|
|
215
106
|
}
|
|
216
107
|
|
|
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
|
-
}
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// Hugging Face (special-cased: non-standard API shape)
|
|
110
|
+
// =============================================================================
|
|
265
111
|
|
|
266
112
|
async function fetchHuggingFaceModels(
|
|
267
113
|
apiKey?: string,
|
|
268
114
|
): 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
115
|
const headers: Record<string, string> = {
|
|
272
116
|
"Content-Type": "application/json",
|
|
273
117
|
};
|
|
@@ -275,56 +119,59 @@ async function fetchHuggingFaceModels(
|
|
|
275
119
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
276
120
|
}
|
|
277
121
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{ headers },
|
|
282
|
-
3,
|
|
283
|
-
1000,
|
|
284
|
-
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) },
|
|
285
125
|
);
|
|
286
126
|
|
|
287
127
|
if (!response.ok) {
|
|
288
128
|
throw new Error(`Hugging Face API error: ${response.status}`);
|
|
289
129
|
}
|
|
290
130
|
|
|
291
|
-
const
|
|
131
|
+
const body = (await response.json()) as Array<{
|
|
292
132
|
id: string;
|
|
293
133
|
modelId?: string;
|
|
294
134
|
}>;
|
|
295
135
|
|
|
296
|
-
const models = Array.isArray(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
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",
|
|
303
142
|
reasoning: false,
|
|
304
143
|
input: ["text"],
|
|
305
|
-
cost: {
|
|
306
|
-
input: 0,
|
|
307
|
-
output: 0,
|
|
308
|
-
cacheRead: 0,
|
|
309
|
-
cacheWrite: 0,
|
|
310
|
-
},
|
|
144
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
311
145
|
contextWindow: 4096,
|
|
312
146
|
maxTokens: 2048,
|
|
313
|
-
}
|
|
314
|
-
);
|
|
147
|
+
};
|
|
148
|
+
});
|
|
315
149
|
}
|
|
316
150
|
|
|
317
151
|
// =============================================================================
|
|
318
|
-
// Provider
|
|
152
|
+
// Provider Definitions
|
|
319
153
|
// =============================================================================
|
|
320
154
|
|
|
321
|
-
|
|
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[] = [
|
|
322
168
|
{
|
|
323
169
|
providerId: "mistral",
|
|
324
170
|
getApiKey: getMistralApiKey,
|
|
325
171
|
baseUrl: "https://api.mistral.ai/v1",
|
|
326
172
|
api: "openai-completions",
|
|
327
173
|
defaultShowPaid: false,
|
|
174
|
+
modelDefaults: { contextWindow: 32_768, maxTokens: 16_384 },
|
|
328
175
|
},
|
|
329
176
|
{
|
|
330
177
|
providerId: "groq",
|
|
@@ -347,87 +194,190 @@ const DYNAMIC_PROVIDERS: Omit<DynamicProviderConfig, "fetchModels">[] = [
|
|
|
347
194
|
api: "openai-completions",
|
|
348
195
|
defaultShowPaid: false,
|
|
349
196
|
},
|
|
350
|
-
|
|
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 = {
|
|
351
239
|
providerId: "huggingface",
|
|
352
240
|
getApiKey: getHfToken,
|
|
353
241
|
baseUrl: "https://api-inference.huggingface.co",
|
|
354
242
|
api: "openai-completions",
|
|
355
243
|
defaultShowPaid: false,
|
|
356
|
-
}
|
|
357
|
-
];
|
|
244
|
+
};
|
|
358
245
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
258
|
|
|
371
259
|
// =============================================================================
|
|
372
|
-
//
|
|
260
|
+
// Registration Logic (sets up toggles, commands, status bar)
|
|
373
261
|
// =============================================================================
|
|
374
262
|
|
|
375
|
-
|
|
263
|
+
async function registerProvider(
|
|
376
264
|
pi: ExtensionAPI,
|
|
265
|
+
config: DynamicProviderDef,
|
|
266
|
+
allModels: ProviderModelConfig[],
|
|
267
|
+
apiKey: string,
|
|
377
268
|
): Promise<void> {
|
|
378
|
-
|
|
269
|
+
const freeModels = allModels.filter((m) =>
|
|
270
|
+
isFreeModel({ ...m, provider: config.providerId }, allModels),
|
|
271
|
+
);
|
|
379
272
|
|
|
380
|
-
|
|
381
|
-
|
|
273
|
+
_logger.info(
|
|
274
|
+
`[dynamic] ${config.providerId}: ${allModels.length} total, ${freeModels.length} free`,
|
|
275
|
+
);
|
|
382
276
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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",
|
|
386
304
|
);
|
|
387
|
-
|
|
388
|
-
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Global toggle
|
|
309
|
+
registerWithGlobalToggle(
|
|
310
|
+
config.providerId,
|
|
311
|
+
{ free: freeModels, all: allModels },
|
|
312
|
+
reRegister,
|
|
313
|
+
true,
|
|
314
|
+
);
|
|
389
315
|
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
}
|
|
392
340
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
341
|
+
// =============================================================================
|
|
342
|
+
// Main Entry — Fire-and-Forget
|
|
343
|
+
// =============================================================================
|
|
396
344
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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>[] = [];
|
|
400
357
|
|
|
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
|
-
);
|
|
358
|
+
for (const config of DYNAMIC_PROVIDERS) {
|
|
359
|
+
const apiKey = config.getApiKey();
|
|
360
|
+
if (!apiKey) continue;
|
|
361
|
+
fetchers.push(discoverAndRegister(pi, config, apiKey));
|
|
362
|
+
}
|
|
418
363
|
|
|
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
|
-
}
|
|
364
|
+
const hfKey = getHfToken();
|
|
365
|
+
if (hfKey) {
|
|
366
|
+
fetchers.push(discoverAndRegisterHF(pi, hfKey));
|
|
432
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
|
+
});
|
|
433
383
|
}
|