pi-free 2.2.4 → 2.2.6
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 +24 -0
- package/README.md +1 -1
- package/banner.svg +1 -1
- package/config.ts +58 -29
- package/constants.ts +1 -4
- package/index.ts +30 -24
- package/lib/built-in-toggle.ts +185 -18
- package/lib/provider-cache.ts +22 -0
- package/package.json +74 -74
- package/provider-failover/benchmark-lookup.ts +4 -1
- package/provider-failover/benchmarks-chunk-0.ts +570 -570
- package/provider-failover/benchmarks-chunk-1.ts +676 -676
- package/provider-failover/benchmarks-chunk-2.ts +673 -673
- package/provider-failover/benchmarks-chunk-3.ts +680 -680
- package/provider-failover/benchmarks-chunk-4.ts +683 -683
- package/provider-failover/benchmarks-chunk-5.ts +816 -474
- package/provider-helper.ts +64 -0
- package/providers/bai/bai.ts +8 -2
- package/providers/crofai/crofai.ts +8 -2
- package/providers/deepinfra/deepinfra.ts +8 -2
- package/providers/dynamic-built-in/index.ts +36 -26
- package/providers/kilo/kilo.ts +41 -22
- package/providers/novita/novita.ts +8 -2
- package/providers/openmodel/openmodel.ts +8 -2
- package/providers/qoder/models.ts +63 -175
- package/providers/qoder/qoder.ts +49 -84
- package/providers/qoder/stream.ts +182 -274
- package/providers/qoder/transform.ts +5 -2
- package/providers/routeway/routeway.ts +8 -2
- package/providers/sambanova/sambanova.ts +12 -6
- package/providers/together/together.ts +8 -2
- package/providers/tokenrouter/tokenrouter.ts +8 -2
- package/providers/zenmux/zenmux.ts +8 -2
- package/providers/codestral/codestral.ts +0 -128
- package/providers/qoder/encoding.ts +0 -48
package/provider-helper.ts
CHANGED
|
@@ -12,6 +12,12 @@ import type {
|
|
|
12
12
|
ProviderModelConfig,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { saveConfig } from "./config.ts";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_PROVIDER_CACHE_TTL_MS,
|
|
17
|
+
isProviderCacheFresh,
|
|
18
|
+
loadProviderCache,
|
|
19
|
+
saveProviderCacheGuarded,
|
|
20
|
+
} from "./lib/provider-cache.ts";
|
|
15
21
|
import { createLogger } from "./lib/logger.ts";
|
|
16
22
|
import type { ModelsDevEnrichedMetadata } from "./lib/types.ts";
|
|
17
23
|
import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-lookup.ts";
|
|
@@ -101,6 +107,64 @@ export function enhanceWithCI(
|
|
|
101
107
|
}));
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Cache-first model loader for network-fetching providers.
|
|
112
|
+
*
|
|
113
|
+
* - If a fresh, non-empty disk cache exists, return it immediately (no network).
|
|
114
|
+
* - Otherwise fetch; persist the result unless it looks like a degenerate /
|
|
115
|
+
* transiently-shrunk response (poisoning guard), so a flaky API can't wipe a
|
|
116
|
+
* good cached list for the TTL window.
|
|
117
|
+
* - On fetch error, fall back to a stale cache entry if one exists.
|
|
118
|
+
*/
|
|
119
|
+
export async function loadCachedOrFetchModels(
|
|
120
|
+
providerId: string,
|
|
121
|
+
fetcher: () => Promise<ProviderModelConfig[]>,
|
|
122
|
+
options?: { ttlMs?: number },
|
|
123
|
+
): Promise<ProviderModelConfig[]> {
|
|
124
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_PROVIDER_CACHE_TTL_MS;
|
|
125
|
+
const cached = loadProviderCache(providerId);
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
cached &&
|
|
129
|
+
cached.length > 0 &&
|
|
130
|
+
isProviderCacheFresh(providerId, ttlMs)
|
|
131
|
+
) {
|
|
132
|
+
return cached;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let fetched: ProviderModelConfig[] = [];
|
|
136
|
+
try {
|
|
137
|
+
fetched = await fetcher();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// Network/discovery failure: keep serving whatever cache we have so the
|
|
140
|
+
// provider still registers models instead of going empty.
|
|
141
|
+
if (cached && cached.length > 0) {
|
|
142
|
+
_logger.info(
|
|
143
|
+
`[${providerId}] fetch failed; serving ${cached.length} cached models`,
|
|
144
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
145
|
+
);
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Persist the fresh list unless it looks like a degenerate /
|
|
152
|
+
// transiently-shrunk response (poisoning guard, centralized in provider-cache),
|
|
153
|
+
// so a flaky API can't wipe a good cached list for the TTL window.
|
|
154
|
+
if (fetched.length > 0) {
|
|
155
|
+
saveProviderCacheGuarded(providerId, fetched).catch((err) => {
|
|
156
|
+
_logger.error(`[${providerId}] failed to persist provider cache`, {
|
|
157
|
+
error: err instanceof Error ? err.message : String(err),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
} else if (cached && cached.length > 0) {
|
|
161
|
+
// Empty fetch but we have a cache: keep serving cache.
|
|
162
|
+
return cached;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return fetched;
|
|
166
|
+
}
|
|
167
|
+
|
|
104
168
|
/**
|
|
105
169
|
* Register an OpenAI-compatible provider with standard headers.
|
|
106
170
|
* Reduces boilerplate across providers that use the OpenAI API format.
|
package/providers/bai/bai.ts
CHANGED
|
@@ -40,7 +40,11 @@ import {
|
|
|
40
40
|
} from "../../lib/provider-compat.ts";
|
|
41
41
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
42
42
|
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
createReRegister,
|
|
45
|
+
loadCachedOrFetchModels,
|
|
46
|
+
setupProvider,
|
|
47
|
+
} from "../../provider-helper.ts";
|
|
44
48
|
|
|
45
49
|
const _logger = createLogger("bai");
|
|
46
50
|
|
|
@@ -176,7 +180,9 @@ export default async function baiProvider(pi: ExtensionAPI) {
|
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
const allModels = await
|
|
183
|
+
const allModels = await loadCachedOrFetchModels(PROVIDER_BAI, () =>
|
|
184
|
+
fetchBaiModels(apiKey),
|
|
185
|
+
);
|
|
180
186
|
|
|
181
187
|
if (allModels.length === 0) {
|
|
182
188
|
// Either the API failed (already logged inside fetchBaiModels) or
|
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
} from "../../lib/provider-compat.ts";
|
|
36
36
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
37
37
|
import { fetchWithRetry } from "../../lib/util.ts";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
createReRegister,
|
|
40
|
+
loadCachedOrFetchModels,
|
|
41
|
+
setupProvider,
|
|
42
|
+
} from "../../provider-helper.ts";
|
|
39
43
|
|
|
40
44
|
const _logger = createLogger("crofai");
|
|
41
45
|
|
|
@@ -147,7 +151,9 @@ export default async function crofaiProvider(pi: ExtensionAPI) {
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// Fetch models
|
|
150
|
-
const allModels = await
|
|
154
|
+
const allModels = await loadCachedOrFetchModels(PROVIDER_CROFAI, () =>
|
|
155
|
+
fetchCrofaiModels(apiKey),
|
|
156
|
+
);
|
|
151
157
|
|
|
152
158
|
if (allModels.length === 0) {
|
|
153
159
|
_logger.warn("[crofai] No models available");
|
|
@@ -49,7 +49,11 @@ import { createProviderProbe } from "../../lib/provider-probe.ts";
|
|
|
49
49
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
50
50
|
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
51
51
|
import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
createReRegister,
|
|
54
|
+
loadCachedOrFetchModels,
|
|
55
|
+
setupProvider,
|
|
56
|
+
} from "../../provider-helper.ts";
|
|
53
57
|
|
|
54
58
|
const _logger = createLogger("deepinfra");
|
|
55
59
|
|
|
@@ -163,7 +167,9 @@ export default async function deepinfraProvider(pi: ExtensionAPI) {
|
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// Fetch models
|
|
166
|
-
const allModels = await
|
|
170
|
+
const allModels = await loadCachedOrFetchModels(PROVIDER_DEEPINFRA, () =>
|
|
171
|
+
fetchDeepinfraModels(apiKey),
|
|
172
|
+
);
|
|
167
173
|
|
|
168
174
|
if (allModels.length === 0) {
|
|
169
175
|
_logger.warn("[deepinfra] No chat models available");
|
|
@@ -55,7 +55,7 @@ import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
|
55
55
|
import { fetchWithTimeout } from "../../lib/util.ts";
|
|
56
56
|
import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
|
|
57
57
|
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
58
|
-
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
58
|
+
import { enhanceWithCI, loadCachedOrFetchModels } from "../../provider-helper.ts";
|
|
59
59
|
import {
|
|
60
60
|
OPENCODE_DYNAMIC_API,
|
|
61
61
|
createOpenCodeSessionTracker,
|
|
@@ -311,38 +311,48 @@ async function discoverAndRegister(
|
|
|
311
311
|
config: DynamicProviderDef,
|
|
312
312
|
apiKey: string,
|
|
313
313
|
): Promise<void> {
|
|
314
|
-
|
|
314
|
+
// Use the shared cache-first loader so dynamic providers (e.g. always-discovered
|
|
315
|
+
// FastRouter) don't block startup on a network fetch. The helper serves a fresh
|
|
316
|
+
// cache, falls back to stale cache on failure/empty fetch, and persists the
|
|
317
|
+
// result for next startup.
|
|
318
|
+
const models = await loadCachedOrFetchModels(
|
|
319
|
+
config.providerId,
|
|
320
|
+
async () => {
|
|
321
|
+
let allModels: ProviderModelConfig[];
|
|
322
|
+
if (config.fetchModels) {
|
|
323
|
+
allModels = await config.fetchModels(apiKey);
|
|
324
|
+
} else {
|
|
325
|
+
allModels = await fetchModelsFromEndpoint({
|
|
326
|
+
providerId: config.providerId,
|
|
327
|
+
baseUrl: config.baseUrl,
|
|
328
|
+
apiKey,
|
|
329
|
+
compat: config.compat,
|
|
330
|
+
modelDefaults: config.modelDefaults,
|
|
331
|
+
timeoutMs: DEFAULT_FETCH_TIMEOUT_MS,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
315
334
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
allModels
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
});
|
|
328
|
-
}
|
|
335
|
+
// Apply DeepSeek proxy compat to matching models. OpenCode headers are
|
|
336
|
+
// injected per request by createOpenCodeStreamSimple(), not stored here.
|
|
337
|
+
return allModels.map((m) => ({
|
|
338
|
+
...m,
|
|
339
|
+
api: isOpenCodeProvider(config.providerId)
|
|
340
|
+
? OPENCODE_DYNAMIC_API
|
|
341
|
+
: m.api,
|
|
342
|
+
compat: getProxyModelCompat(m) ?? m.compat,
|
|
343
|
+
}));
|
|
344
|
+
},
|
|
345
|
+
);
|
|
329
346
|
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
allModels = allModels.map((m) => ({
|
|
333
|
-
...m,
|
|
334
|
-
api: isOpenCodeProvider(config.providerId) ? OPENCODE_DYNAMIC_API : m.api,
|
|
335
|
-
compat: getProxyModelCompat(m) ?? m.compat,
|
|
336
|
-
}));
|
|
337
|
-
} catch (error) {
|
|
347
|
+
if (models.length === 0) {
|
|
348
|
+
// No usable models and no fallback cache: leave Pi's built-in defaults.
|
|
338
349
|
_logger.info(
|
|
339
|
-
`[dynamic] ${config.providerId}:
|
|
340
|
-
{ error: error instanceof Error ? error.message : String(error) },
|
|
350
|
+
`[dynamic] ${config.providerId}: no models available; Pi keeps its defaults`,
|
|
341
351
|
);
|
|
342
352
|
return;
|
|
343
353
|
}
|
|
344
354
|
|
|
345
|
-
await registerProvider(pi, config,
|
|
355
|
+
await registerProvider(pi, config, models, apiKey);
|
|
346
356
|
}
|
|
347
357
|
|
|
348
358
|
// =============================================================================
|
package/providers/kilo/kilo.ts
CHANGED
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
} from "../../config.ts";
|
|
27
27
|
import { URL_KILO_TOS } from "../../constants.ts";
|
|
28
28
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
29
|
+
import { saveProviderCacheGuarded } from "../../lib/provider-cache.ts";
|
|
29
30
|
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
30
31
|
import { cleanModelName, logWarning } from "../../lib/util.ts";
|
|
31
32
|
import {
|
|
32
33
|
createCtxReRegister,
|
|
33
34
|
createReRegister,
|
|
34
35
|
enhanceWithCI,
|
|
36
|
+
loadCachedOrFetchModels,
|
|
35
37
|
type StoredModels,
|
|
36
38
|
} from "../../provider-helper.ts";
|
|
37
39
|
import { loginKilo, refreshKiloToken } from "./kilo-auth.ts";
|
|
@@ -176,27 +178,27 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
176
178
|
// Resolve API key (env var or ~/.pi/free.json)
|
|
177
179
|
const kiloApiKey = getKiloApiKey();
|
|
178
180
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
181
|
+
// Cache-first at startup via the shared helper (mirrors the other fetchers):
|
|
182
|
+
// serve a fresh cache instantly, else fetch (full → free-only fallback),
|
|
183
|
+
// else fall back to a stale cache. The session_start handler below keeps
|
|
184
|
+
// the cache fresh.
|
|
185
|
+
let allModels: ProviderModelConfig[] = await loadCachedOrFetchModels(
|
|
186
|
+
PROVIDER_KILO,
|
|
187
|
+
async () => {
|
|
188
|
+
try {
|
|
189
|
+
return await fetchKiloModels({ token: kiloApiKey, freeOnly: false });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logWarning("kilo", "Failed to fetch models at startup", error);
|
|
192
|
+
return fetchKiloModels({ freeOnly: true }).catch((err) => {
|
|
193
|
+
logWarning("kilo", "Failed to fetch free models at startup", err);
|
|
194
|
+
return [];
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
let freeModels: ProviderModelConfig[] = allModels.filter((m) =>
|
|
200
|
+
isFreeModel({ ...m, provider: PROVIDER_KILO }, allModels),
|
|
201
|
+
);
|
|
200
202
|
|
|
201
203
|
// State tracking
|
|
202
204
|
const kiloShowPaid = getKiloShowPaid();
|
|
@@ -204,6 +206,14 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
204
206
|
let showPaidModels = kiloShowPaid;
|
|
205
207
|
let currentModels = kiloShowPaid && !kiloFreeOnly ? allModels : freeModels;
|
|
206
208
|
|
|
209
|
+
if (currentModels.length === 0) {
|
|
210
|
+
logWarning(
|
|
211
|
+
"kilo",
|
|
212
|
+
"No Kilo models available at startup; provider will be empty until models can be fetched",
|
|
213
|
+
undefined,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
// Shared model storage for global toggle
|
|
208
218
|
const stored: StoredModels = { free: freeModels, all: allModels };
|
|
209
219
|
|
|
@@ -295,7 +305,7 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
295
305
|
"User-Agent": "pi-free-providers",
|
|
296
306
|
},
|
|
297
307
|
models: enhanceWithCI(modelsWithCompat),
|
|
298
|
-
...(
|
|
308
|
+
...(kiloApiKey ? {} : { oauth: oauthConfig }),
|
|
299
309
|
});
|
|
300
310
|
|
|
301
311
|
// Registration complete - models registered silently (use LOG_LEVEL=info to see details)
|
|
@@ -449,6 +459,15 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
449
459
|
);
|
|
450
460
|
stored.free = freeModels;
|
|
451
461
|
|
|
462
|
+
// Persist refreshed models to disk cache for fast next startup
|
|
463
|
+
saveProviderCacheGuarded(PROVIDER_KILO, allModels).catch((err) => {
|
|
464
|
+
logWarning(
|
|
465
|
+
"kilo",
|
|
466
|
+
"Failed to persist refreshed models to cache",
|
|
467
|
+
err,
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
452
471
|
// Update global toggle registration
|
|
453
472
|
const baseCtxReRegister = createCtxReRegister(ctx as any, {
|
|
454
473
|
...KILO_PROVIDER_CONFIG,
|
|
@@ -41,7 +41,11 @@ import { createProviderProbe } from "../../lib/provider-probe.ts";
|
|
|
41
41
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
42
42
|
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
43
43
|
import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
createReRegister,
|
|
46
|
+
loadCachedOrFetchModels,
|
|
47
|
+
setupProvider,
|
|
48
|
+
} from "../../provider-helper.ts";
|
|
45
49
|
|
|
46
50
|
const _logger = createLogger("novita");
|
|
47
51
|
|
|
@@ -157,7 +161,9 @@ export default async function novitaProvider(pi: ExtensionAPI) {
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
// Fetch models
|
|
160
|
-
const allModels = await
|
|
164
|
+
const allModels = await loadCachedOrFetchModels(PROVIDER_NOVITA, () =>
|
|
165
|
+
fetchNovitaModels(apiKey),
|
|
166
|
+
);
|
|
161
167
|
|
|
162
168
|
if (allModels.length === 0) {
|
|
163
169
|
_logger.warn("[novita] No chat models available");
|
|
@@ -45,7 +45,11 @@ import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
|
45
45
|
import { isLikelyReasoningModel } from "../../lib/provider-compat.ts";
|
|
46
46
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
47
47
|
import { fetchWithRetry } from "../../lib/util.ts";
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
createReRegister,
|
|
50
|
+
loadCachedOrFetchModels,
|
|
51
|
+
setupProvider,
|
|
52
|
+
} from "../../provider-helper.ts";
|
|
49
53
|
|
|
50
54
|
const _logger = createLogger("openmodel");
|
|
51
55
|
|
|
@@ -465,7 +469,9 @@ export default async function openmodelProvider(pi: ExtensionAPI) {
|
|
|
465
469
|
return;
|
|
466
470
|
}
|
|
467
471
|
|
|
468
|
-
const allModels = await
|
|
472
|
+
const allModels = await loadCachedOrFetchModels(PROVIDER_OPENMODEL, () =>
|
|
473
|
+
fetchOpenModelModels(apiKey),
|
|
474
|
+
);
|
|
469
475
|
|
|
470
476
|
if (allModels.length === 0) {
|
|
471
477
|
_logger.warn(
|