pi-free 2.0.14 → 2.1.0
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 +90 -0
- package/README.md +64 -78
- package/banner.svg +21 -36
- package/config.ts +123 -9
- package/constants.ts +3 -9
- package/index.ts +14 -15
- package/lib/built-in-toggle.ts +29 -16
- package/lib/json-persistence.ts +90 -22
- package/lib/logger.ts +21 -12
- package/lib/model-detection.ts +2 -12
- package/lib/model-enhancer.ts +11 -2
- package/lib/model-metadata.ts +387 -0
- package/lib/open-browser.ts +74 -24
- package/lib/paths.ts +90 -0
- package/lib/probe-cache.ts +19 -19
- package/lib/provider-cache.ts +74 -28
- package/lib/provider-compat.ts +58 -9
- package/lib/provider-probe.ts +188 -0
- package/lib/registry.ts +1 -5
- package/lib/session-start-metrics.ts +46 -0
- package/lib/telemetry.ts +115 -86
- package/lib/types.ts +22 -2
- package/lib/util.ts +80 -21
- package/package.json +7 -2
- package/provider-failover/benchmark-lookup.ts +17 -5
- package/provider-helper.ts +11 -2
- package/providers/cline/cline-models.ts +12 -2
- package/providers/cline/cline-xml-bridge.ts +974 -0
- package/providers/cline/cline.ts +67 -176
- package/providers/crofai/crofai.ts +6 -1
- package/providers/deepinfra/deepinfra.ts +69 -2
- package/providers/dynamic-built-in/index.ts +237 -2
- package/providers/kilo/kilo-models.ts +3 -1
- package/providers/kilo/kilo.ts +268 -41
- package/providers/model-fetcher.ts +18 -55
- package/providers/novita/novita.ts +69 -2
- package/providers/ollama/ollama.ts +48 -24
- package/providers/opencode-session.ts +67 -2
- package/providers/routeway/routeway.ts +188 -2
- package/providers/sambanova/sambanova.ts +67 -1
- package/providers/together/together.ts +69 -2
- package/providers/tokenrouter/tokenrouter.ts +378 -0
- package/providers/zenmux/zenmux.ts +6 -1
- package/scripts/check-extensions.mjs +32 -16
- package/providers/nvidia/nvidia.ts +0 -504
|
@@ -42,8 +42,16 @@ import {
|
|
|
42
42
|
} from "../../config.ts";
|
|
43
43
|
import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
|
|
44
44
|
import { createLogger } from "../../lib/logger.ts";
|
|
45
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
45
46
|
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
47
|
+
import {
|
|
48
|
+
getModelsDueForProbe,
|
|
49
|
+
recordModelProbeResults,
|
|
50
|
+
} from "../../lib/probe-cache.ts";
|
|
46
51
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
52
|
+
import { updateConfig } from "../../config.ts";
|
|
53
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
54
|
+
import { fetchWithTimeout } from "../../lib/util.ts";
|
|
47
55
|
import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
|
|
48
56
|
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
49
57
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
@@ -56,6 +64,8 @@ import {
|
|
|
56
64
|
|
|
57
65
|
const _logger = createLogger("dynamic-built-in");
|
|
58
66
|
|
|
67
|
+
const OPENCODE_PROBE_TIMEOUT_MS = 15_000;
|
|
68
|
+
|
|
59
69
|
// OpenCode headers must be regenerated for every LLM request.
|
|
60
70
|
const _opencodeSession = createOpenCodeSessionTracker();
|
|
61
71
|
|
|
@@ -64,6 +74,7 @@ const _opencodeSession = createOpenCodeSessionTracker();
|
|
|
64
74
|
// =============================================================================
|
|
65
75
|
|
|
66
76
|
interface FetchModelsOptions {
|
|
77
|
+
providerId: string;
|
|
67
78
|
baseUrl: string;
|
|
68
79
|
apiKey: string;
|
|
69
80
|
compat?: ProviderModelConfig["compat"];
|
|
@@ -103,7 +114,7 @@ async function fetchModelsFromEndpoint(
|
|
|
103
114
|
? body
|
|
104
115
|
: (body.data ?? []);
|
|
105
116
|
|
|
106
|
-
|
|
117
|
+
const models = rawModels.map((m) => {
|
|
107
118
|
const id = String(m.id ?? "");
|
|
108
119
|
const inputModalities = m.input_modalities as string[] | undefined;
|
|
109
120
|
return {
|
|
@@ -115,7 +126,9 @@ async function fetchModelsFromEndpoint(
|
|
|
115
126
|
: (["text"] as const),
|
|
116
127
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
117
128
|
contextWindow:
|
|
118
|
-
((m.
|
|
129
|
+
((m.context_length ??
|
|
130
|
+
m.max_context_length ??
|
|
131
|
+
m.context_window) as number) ??
|
|
119
132
|
opts.modelDefaults?.contextWindow ??
|
|
120
133
|
128_000,
|
|
121
134
|
maxTokens:
|
|
@@ -127,6 +140,10 @@ async function fetchModelsFromEndpoint(
|
|
|
127
140
|
...(opts.compat ? { compat: opts.compat } : {}),
|
|
128
141
|
} satisfies ProviderModelConfig & { _pricingKnown?: boolean };
|
|
129
142
|
});
|
|
143
|
+
|
|
144
|
+
return await safeEnrichModelsWithModelsDev(models, {
|
|
145
|
+
providerId: opts.providerId,
|
|
146
|
+
});
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
// =============================================================================
|
|
@@ -191,6 +208,16 @@ interface DynamicProviderDef {
|
|
|
191
208
|
* When not provided, fetchModelsFromEndpoint is used (no pricing, _pricingKnown=false).
|
|
192
209
|
*/
|
|
193
210
|
fetchModels?: (apiKey: string) => Promise<ProviderModelConfig[]>;
|
|
211
|
+
/**
|
|
212
|
+
* Optional probe support for providers whose free model status expires.
|
|
213
|
+
*/
|
|
214
|
+
probe?: {
|
|
215
|
+
run: (
|
|
216
|
+
apiKey: string,
|
|
217
|
+
models: ProviderModelConfig[],
|
|
218
|
+
options?: { useCache?: boolean },
|
|
219
|
+
) => Promise<string[]>;
|
|
220
|
+
};
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
@@ -230,6 +257,15 @@ const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
|
230
257
|
api: OPENCODE_DYNAMIC_API,
|
|
231
258
|
defaultShowPaid: getOpencodeShowPaid,
|
|
232
259
|
// OpenCode API returns no pricing — _pricingKnown=false, name-based detection
|
|
260
|
+
probe: {
|
|
261
|
+
run: (apiKey, models) =>
|
|
262
|
+
runOpenCodeProbe(
|
|
263
|
+
"opencode",
|
|
264
|
+
apiKey,
|
|
265
|
+
"https://opencode.ai/zen/v1",
|
|
266
|
+
models,
|
|
267
|
+
),
|
|
268
|
+
},
|
|
233
269
|
},
|
|
234
270
|
{
|
|
235
271
|
providerId: "opencode-go",
|
|
@@ -238,6 +274,15 @@ const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
|
238
274
|
api: OPENCODE_DYNAMIC_API,
|
|
239
275
|
defaultShowPaid: getOpencodeShowPaid,
|
|
240
276
|
// OpenCode Go uses the same OPENCODE_API_KEY and per-request headers
|
|
277
|
+
probe: {
|
|
278
|
+
run: (apiKey, models) =>
|
|
279
|
+
runOpenCodeProbe(
|
|
280
|
+
"opencode-go",
|
|
281
|
+
apiKey,
|
|
282
|
+
"https://opencode.ai/zen/go/v1",
|
|
283
|
+
models,
|
|
284
|
+
),
|
|
285
|
+
},
|
|
241
286
|
},
|
|
242
287
|
{
|
|
243
288
|
providerId: "openrouter",
|
|
@@ -248,6 +293,7 @@ const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
|
248
293
|
// OpenRouter returns full pricing — use its dedicated fetcher
|
|
249
294
|
fetchModels: (apiKey) =>
|
|
250
295
|
fetchOpenRouterCompatibleModels({
|
|
296
|
+
providerId: "openrouter",
|
|
251
297
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
252
298
|
apiKey,
|
|
253
299
|
freeOnly: false,
|
|
@@ -271,6 +317,7 @@ async function discoverAndRegister(
|
|
|
271
317
|
allModels = await config.fetchModels(apiKey);
|
|
272
318
|
} else {
|
|
273
319
|
allModels = await fetchModelsFromEndpoint({
|
|
320
|
+
providerId: config.providerId,
|
|
274
321
|
baseUrl: config.baseUrl,
|
|
275
322
|
apiKey,
|
|
276
323
|
compat: config.compat,
|
|
@@ -297,6 +344,107 @@ async function discoverAndRegister(
|
|
|
297
344
|
await registerProvider(pi, config, allModels, apiKey);
|
|
298
345
|
}
|
|
299
346
|
|
|
347
|
+
// =============================================================================
|
|
348
|
+
// OpenCode Probe
|
|
349
|
+
// =============================================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Probe a single OpenCode model with a minimal chat request.
|
|
353
|
+
*
|
|
354
|
+
* OpenCode expired free promotions return 401 with a body like:
|
|
355
|
+
* { error: { message: "Free promotion has ended" } }
|
|
356
|
+
*
|
|
357
|
+
* We treat 401 and 403 as "broken" for free models, since those codes mean
|
|
358
|
+
* the model is no longer accessible under the current credentials. 404 is
|
|
359
|
+
* also broken. 429 means the model is reachable but rate-limited (ok).
|
|
360
|
+
*/
|
|
361
|
+
async function probeOpenCodeModel(
|
|
362
|
+
apiKey: string,
|
|
363
|
+
baseUrl: string,
|
|
364
|
+
modelId: string,
|
|
365
|
+
): Promise<"ok" | "broken" | "unknown"> {
|
|
366
|
+
try {
|
|
367
|
+
const response = await fetchWithTimeout(
|
|
368
|
+
`${baseUrl}/chat/completions`,
|
|
369
|
+
{
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: {
|
|
372
|
+
Authorization: `Bearer ${apiKey}`,
|
|
373
|
+
"Content-Type": "application/json",
|
|
374
|
+
"User-Agent": "opencode/1.15.5",
|
|
375
|
+
"x-opencode-client": "cli",
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
model: modelId,
|
|
379
|
+
messages: [{ role: "user", content: "hi" }],
|
|
380
|
+
max_tokens: 1,
|
|
381
|
+
}),
|
|
382
|
+
},
|
|
383
|
+
OPENCODE_PROBE_TIMEOUT_MS,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (response.status === 401 || response.status === 403) return "broken";
|
|
387
|
+
if (response.status === 404) return "broken";
|
|
388
|
+
if (response.status === 429) return "ok";
|
|
389
|
+
if (response.ok) return "ok";
|
|
390
|
+
return "ok";
|
|
391
|
+
} catch {
|
|
392
|
+
return "unknown";
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function runOpenCodeProbe(
|
|
397
|
+
providerId: string,
|
|
398
|
+
apiKey: string,
|
|
399
|
+
baseUrl: string,
|
|
400
|
+
models: ProviderModelConfig[],
|
|
401
|
+
options: { useCache?: boolean } = {},
|
|
402
|
+
): Promise<string[]> {
|
|
403
|
+
const freeModels = models.filter((m) =>
|
|
404
|
+
isFreeModel({ ...m, provider: providerId }, models),
|
|
405
|
+
);
|
|
406
|
+
const modelIdsToProbe = options.useCache
|
|
407
|
+
? new Set(
|
|
408
|
+
getModelsDueForProbe(
|
|
409
|
+
providerId,
|
|
410
|
+
freeModels.map((m) => m.id),
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
: undefined;
|
|
414
|
+
const probeCandidates = modelIdsToProbe
|
|
415
|
+
? freeModels.filter((m) => modelIdsToProbe.has(m.id))
|
|
416
|
+
: freeModels;
|
|
417
|
+
|
|
418
|
+
if (probeCandidates.length === 0) {
|
|
419
|
+
_logger.info(`Auto-probe: ${providerId} probe cache is fresh`);
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const broken: string[] = [];
|
|
424
|
+
const cacheableResults: Array<{ modelId: string; status: "ok" | "broken" }> =
|
|
425
|
+
[];
|
|
426
|
+
const batchSize = 5;
|
|
427
|
+
|
|
428
|
+
for (let i = 0; i < probeCandidates.length; i += batchSize) {
|
|
429
|
+
const batch = probeCandidates.slice(i, i + batchSize);
|
|
430
|
+
const results = await Promise.all(
|
|
431
|
+
batch.map(async (m) => {
|
|
432
|
+
const status = await probeOpenCodeModel(apiKey, baseUrl, m.id);
|
|
433
|
+
return { id: m.id, status };
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
for (const r of results) {
|
|
437
|
+
if (r.status === "broken") broken.push(r.id);
|
|
438
|
+
if (r.status !== "unknown") {
|
|
439
|
+
cacheableResults.push({ modelId: r.id, status: r.status });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await recordModelProbeResults(providerId, cacheableResults);
|
|
445
|
+
return broken;
|
|
446
|
+
}
|
|
447
|
+
|
|
300
448
|
async function discoverAndRegisterHF(
|
|
301
449
|
pi: ExtensionAPI,
|
|
302
450
|
apiKey: string,
|
|
@@ -354,6 +502,35 @@ async function registerProvider(
|
|
|
354
502
|
});
|
|
355
503
|
};
|
|
356
504
|
|
|
505
|
+
const stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] } = {
|
|
506
|
+
free: freeModels,
|
|
507
|
+
all: allModels,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Hide broken free models in config and update the active model list.
|
|
512
|
+
*/
|
|
513
|
+
async function hideBrokenModels(brokenIds: string[]): Promise<void> {
|
|
514
|
+
if (brokenIds.length === 0) return;
|
|
515
|
+
|
|
516
|
+
await updateConfig((cfg) => {
|
|
517
|
+
const existingHidden = new Set(cfg.hidden_models ?? []);
|
|
518
|
+
for (const id of brokenIds) {
|
|
519
|
+
existingHidden.add(`${config.providerId}/${id}`);
|
|
520
|
+
}
|
|
521
|
+
return { hidden_models: Array.from(existingHidden) };
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
stored.all = stored.all.filter((m) => !brokenIds.includes(m.id));
|
|
525
|
+
stored.free = stored.free.filter((m) => !brokenIds.includes(m.id));
|
|
526
|
+
toggleState.setModels(stored);
|
|
527
|
+
toggleState.applyCurrent(reRegister);
|
|
528
|
+
|
|
529
|
+
_logger.info(
|
|
530
|
+
`[dynamic] ${config.providerId}: hidden ${brokenIds.length} broken models`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
357
534
|
// Toggle state
|
|
358
535
|
const toggleState = createToggleState({
|
|
359
536
|
providerId: config.providerId,
|
|
@@ -408,6 +585,63 @@ async function registerProvider(
|
|
|
408
585
|
|
|
409
586
|
// Register models (this swaps in our discovered models over Pi's defaults)
|
|
410
587
|
toggleState.applyCurrent(reRegister);
|
|
588
|
+
|
|
589
|
+
// ── Probe command for providers whose free model status expires ─────
|
|
590
|
+
if (config.probe) {
|
|
591
|
+
pi.registerCommand(`probe-${config.providerId}`, {
|
|
592
|
+
description: `Test ${config.providerId} free models for expired promotions`,
|
|
593
|
+
handler: async (_args, ctx) => {
|
|
594
|
+
const modelsToTest =
|
|
595
|
+
toggleState.getCurrentMode() === "all" ? stored.all : stored.free;
|
|
596
|
+
ctx.ui.notify(
|
|
597
|
+
`Probing ${modelsToTest.length} ${config.providerId} models…`,
|
|
598
|
+
"info",
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const broken = await config.probe!.run(apiKey, modelsToTest, {
|
|
602
|
+
useCache: false,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (broken.length === 0) {
|
|
606
|
+
ctx.ui.notify(
|
|
607
|
+
`All ${config.providerId} models are accessible ✅`,
|
|
608
|
+
"info",
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
ctx.ui.notify(
|
|
614
|
+
`Found ${broken.length} expired free models:\n${broken.join("\n")}`,
|
|
615
|
+
"warning",
|
|
616
|
+
);
|
|
617
|
+
await hideBrokenModels(broken);
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ── Lazy auto-probe on first session_start ───────────────────────
|
|
622
|
+
let _autoProbeDone = false;
|
|
623
|
+
pi.on(
|
|
624
|
+
"session_start",
|
|
625
|
+
wrapSessionStartHandler(`${config.providerId}-auto-probe`, async () => {
|
|
626
|
+
if (_autoProbeDone) return;
|
|
627
|
+
_autoProbeDone = true;
|
|
628
|
+
_logger.info(
|
|
629
|
+
`Starting lazy auto-probe of ${config.providerId} free models...`,
|
|
630
|
+
);
|
|
631
|
+
try {
|
|
632
|
+
const broken = await config.probe!.run(apiKey, stored.free, {
|
|
633
|
+
useCache: true,
|
|
634
|
+
});
|
|
635
|
+
await hideBrokenModels(broken);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
_logger.warn("Auto-probe failed", {
|
|
638
|
+
error: err instanceof Error ? err.message : String(err),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
411
645
|
_logger.info(`[dynamic] ${config.providerId}: registered`);
|
|
412
646
|
}
|
|
413
647
|
|
|
@@ -457,6 +691,7 @@ export async function setupDynamicBuiltInProviders(
|
|
|
457
691
|
defaultShowPaid: getFastrouterShowPaid,
|
|
458
692
|
fetchModels: () =>
|
|
459
693
|
fetchOpenRouterCompatibleModels({
|
|
694
|
+
providerId: "fastrouter",
|
|
460
695
|
baseUrl: "https://api.fastrouter.ai/api/v1",
|
|
461
696
|
apiKey: fastrouterApiKey,
|
|
462
697
|
freeOnly: false,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { applyHidden } from "../../config.ts";
|
|
6
6
|
import { PROVIDER_KILO } from "../../constants.ts";
|
|
7
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
|
|
8
9
|
|
|
9
10
|
const KILO_API_BASE = process.env.KILO_API_URL || "https://api.kilo.ai";
|
|
@@ -16,8 +17,9 @@ export const KILO_GATEWAY_BASE = `${KILO_API_BASE}/api/gateway`;
|
|
|
16
17
|
export async function fetchKiloModels(options?: {
|
|
17
18
|
token?: string;
|
|
18
19
|
freeOnly?: boolean;
|
|
19
|
-
}): Promise<
|
|
20
|
+
}): Promise<ProviderModelConfig[]> {
|
|
20
21
|
const models = await fetchOpenRouterCompatibleModels({
|
|
22
|
+
providerId: PROVIDER_KILO,
|
|
21
23
|
baseUrl: KILO_GATEWAY_BASE,
|
|
22
24
|
apiKey: options?.token,
|
|
23
25
|
freeOnly: options?.freeOnly,
|