pi-free 2.2.5 → 2.2.7
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 +31 -0
- package/README.md +3 -2
- package/banner.svg +1 -1
- package/config.ts +821 -779
- package/constants.ts +3 -3
- package/index.ts +29 -24
- package/lib/built-in-toggle.ts +426 -427
- 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/anyapi/anyapi.ts +270 -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 +42 -28
- package/providers/novita/novita.ts +8 -2
- package/providers/opencode-session.ts +69 -18
- package/providers/openmodel/openmodel.ts +8 -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 +12 -7
- package/providers/zenmux/zenmux.ts +8 -2
- package/providers/codestral/codestral.ts +0 -128
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.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnyAPI provider extension.
|
|
3
|
+
*
|
|
4
|
+
* AnyAPI is an OpenAI-compatible gateway with a free plan and a catalog of
|
|
5
|
+
* explicitly free models. It exposes the catalog at /v1/models and routes
|
|
6
|
+
* chat requests through /v1/chat/completions.
|
|
7
|
+
*
|
|
8
|
+
* Setup:
|
|
9
|
+
* ANYAPI_API_KEY=...
|
|
10
|
+
* # or add anyapi_api_key to ~/.pi/free.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ProviderModelConfig,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
applyHidden,
|
|
19
|
+
getAnyapiApiKey,
|
|
20
|
+
getAnyapiShowPaid,
|
|
21
|
+
} from "../../config.ts";
|
|
22
|
+
import {
|
|
23
|
+
BASE_URL_ANYAPI,
|
|
24
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
25
|
+
PROVIDER_ANYAPI,
|
|
26
|
+
} from "../../constants.ts";
|
|
27
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
28
|
+
import { loadProviderCache } from "../../lib/provider-cache.ts";
|
|
29
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
30
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
31
|
+
import { fetchWithRetry, mapOpenRouterModel } from "../../lib/util.ts";
|
|
32
|
+
import {
|
|
33
|
+
createReRegister,
|
|
34
|
+
loadCachedOrFetchModels,
|
|
35
|
+
setupProvider,
|
|
36
|
+
} from "../../provider-helper.ts";
|
|
37
|
+
|
|
38
|
+
const _logger = createLogger("anyapi");
|
|
39
|
+
|
|
40
|
+
interface AnyApiModel {
|
|
41
|
+
id: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
context_length?: number;
|
|
44
|
+
max_completion_tokens?: number | null;
|
|
45
|
+
top_provider?: {
|
|
46
|
+
context_length?: number | null;
|
|
47
|
+
max_completion_tokens?: number | null;
|
|
48
|
+
};
|
|
49
|
+
pricing?: {
|
|
50
|
+
prompt?: string | number | null;
|
|
51
|
+
completion?: string | number | null;
|
|
52
|
+
input_cache_read?: string | number | null;
|
|
53
|
+
input_cache_write?: string | number | null;
|
|
54
|
+
};
|
|
55
|
+
architecture?: {
|
|
56
|
+
input_modalities?: string[] | null;
|
|
57
|
+
output_modalities?: string[] | null;
|
|
58
|
+
};
|
|
59
|
+
supported_parameters?: string[] | null;
|
|
60
|
+
tags?: string[];
|
|
61
|
+
isFree?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ANYAPI_METADATA_VERSION = 1;
|
|
65
|
+
|
|
66
|
+
type AnyApiProviderModel = ProviderModelConfig & {
|
|
67
|
+
_pricingKnown?: boolean;
|
|
68
|
+
_freeKnown?: boolean;
|
|
69
|
+
_isFree?: boolean;
|
|
70
|
+
_anyapiMetadataVersion?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function hasPricing(model: AnyApiModel): boolean {
|
|
74
|
+
return (
|
|
75
|
+
(model.pricing?.prompt !== null && model.pricing?.prompt !== undefined) ||
|
|
76
|
+
(model.pricing?.completion !== null &&
|
|
77
|
+
model.pricing?.completion !== undefined) ||
|
|
78
|
+
(model.pricing?.input_cache_read !== null &&
|
|
79
|
+
model.pricing?.input_cache_read !== undefined) ||
|
|
80
|
+
(model.pricing?.input_cache_write !== null &&
|
|
81
|
+
model.pricing?.input_cache_write !== undefined)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizePrice(
|
|
86
|
+
value: string | number | null | undefined,
|
|
87
|
+
): string | null {
|
|
88
|
+
return value === null || value === undefined ? null : String(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect AnyAPI's explicitly free model labels without treating every model
|
|
93
|
+
* with omitted pricing as free. The API may also expose an authoritative flag
|
|
94
|
+
* or zero pricing, both of which are handled here.
|
|
95
|
+
*/
|
|
96
|
+
export function isAnyApiFreeModel(model: AnyApiModel): boolean {
|
|
97
|
+
if (typeof model.isFree === "boolean") return model.isFree;
|
|
98
|
+
|
|
99
|
+
const label = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
100
|
+
if (/\bfree\b/.test(label)) return true;
|
|
101
|
+
|
|
102
|
+
if (!hasPricing(model)) return false;
|
|
103
|
+
const input = Number(model.pricing?.prompt);
|
|
104
|
+
const output = Number(model.pricing?.completion);
|
|
105
|
+
return input === 0 && output === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function mapAnyApiModel(model: AnyApiModel): AnyApiProviderModel {
|
|
109
|
+
const name = model.name ?? model.id;
|
|
110
|
+
const pricingKnown = hasPricing(model);
|
|
111
|
+
const freeKnown =
|
|
112
|
+
typeof model.isFree === "boolean" ||
|
|
113
|
+
/\bfree\b/i.test(`${model.id} ${name}`) ||
|
|
114
|
+
(pricingKnown && isAnyApiFreeModel(model));
|
|
115
|
+
|
|
116
|
+
const tags = model.tags ?? [];
|
|
117
|
+
const supportsReasoning =
|
|
118
|
+
tags.includes("reasoning") || tags.includes("chat_completions:reasoning");
|
|
119
|
+
const supportsVision =
|
|
120
|
+
tags.includes("vision") || tags.includes("chat_completions:vision");
|
|
121
|
+
|
|
122
|
+
const mapped = mapOpenRouterModel({
|
|
123
|
+
...model,
|
|
124
|
+
name,
|
|
125
|
+
supported_parameters:
|
|
126
|
+
model.supported_parameters ?? (supportsReasoning ? ["reasoning"] : []),
|
|
127
|
+
architecture: model.architecture ?? {
|
|
128
|
+
input_modalities: supportsVision ? ["text", "image"] : ["text"],
|
|
129
|
+
output_modalities: ["text"],
|
|
130
|
+
},
|
|
131
|
+
pricing: model.pricing
|
|
132
|
+
? {
|
|
133
|
+
prompt: normalizePrice(model.pricing.prompt),
|
|
134
|
+
completion: normalizePrice(model.pricing.completion),
|
|
135
|
+
input_cache_read: normalizePrice(model.pricing.input_cache_read),
|
|
136
|
+
input_cache_write: normalizePrice(model.pricing.input_cache_write),
|
|
137
|
+
}
|
|
138
|
+
: undefined,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...mapped,
|
|
143
|
+
_pricingKnown: pricingKnown,
|
|
144
|
+
...(freeKnown && {
|
|
145
|
+
_freeKnown: true,
|
|
146
|
+
_isFree: isAnyApiFreeModel(model),
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isTextModel(model: AnyApiModel): boolean {
|
|
152
|
+
const outputModalities = model.architecture?.output_modalities ?? [];
|
|
153
|
+
if (outputModalities.length > 0 && !outputModalities.includes("text")) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const tags = model.tags;
|
|
158
|
+
if (!tags || tags.length === 0) return true;
|
|
159
|
+
return tags.some((tag) => tag.startsWith("chat_completions:"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fetchAnyApiModels(
|
|
163
|
+
apiKey: string,
|
|
164
|
+
): Promise<AnyApiProviderModel[]> {
|
|
165
|
+
const response = await fetchWithRetry(
|
|
166
|
+
`${BASE_URL_ANYAPI}/models`,
|
|
167
|
+
{
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${apiKey}`,
|
|
170
|
+
Accept: "application/json",
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
3,
|
|
175
|
+
1000,
|
|
176
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`AnyAPI API error: ${response.status} ${response.statusText}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const json = (await response.json()) as { data?: AnyApiModel[] };
|
|
186
|
+
const models = (json.data ?? []).flatMap((model) => {
|
|
187
|
+
if (!model.id || !isTextModel(model)) return [];
|
|
188
|
+
return [mapAnyApiModel(model)];
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
_logger.info(`[anyapi] Fetched ${models.length} text models`);
|
|
192
|
+
|
|
193
|
+
// AnyAPI's /models response omits context and output limits for its
|
|
194
|
+
// catalog. Use the global models.dev catalog so canonical model IDs such
|
|
195
|
+
// as qwen/qwen3-coder:free do not fall back to the generic 4096-token
|
|
196
|
+
// defaults in mapOpenRouterModel. The AnyAPI-scoped models.dev entry only
|
|
197
|
+
// contains a small paid subset and does not cover the free catalog.
|
|
198
|
+
const enriched = await safeEnrichModelsWithModelsDev(models);
|
|
199
|
+
return applyHidden(
|
|
200
|
+
enriched.map((model) => ({
|
|
201
|
+
...model,
|
|
202
|
+
_anyapiMetadataVersion: ANYAPI_METADATA_VERSION,
|
|
203
|
+
})),
|
|
204
|
+
PROVIDER_ANYAPI,
|
|
205
|
+
) as AnyApiProviderModel[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default async function anyapiProvider(pi: ExtensionAPI) {
|
|
209
|
+
const apiKey = getAnyapiApiKey();
|
|
210
|
+
if (!apiKey) {
|
|
211
|
+
_logger.info("[anyapi] Skipping — ANYAPI_API_KEY not set.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const cachedModels = loadProviderCache(PROVIDER_ANYAPI) as
|
|
216
|
+
| AnyApiProviderModel[]
|
|
217
|
+
| undefined;
|
|
218
|
+
const needsMetadataMigration = cachedModels?.some(
|
|
219
|
+
(model) => model._anyapiMetadataVersion !== ANYAPI_METADATA_VERSION,
|
|
220
|
+
);
|
|
221
|
+
const allModels = await loadCachedOrFetchModels(
|
|
222
|
+
PROVIDER_ANYAPI,
|
|
223
|
+
() => fetchAnyApiModels(apiKey),
|
|
224
|
+
// Force one refresh for caches written before context metadata was added;
|
|
225
|
+
// subsequent warm startups continue using the normal one-hour cache.
|
|
226
|
+
needsMetadataMigration ? { ttlMs: -1 } : undefined,
|
|
227
|
+
);
|
|
228
|
+
if (allModels.length === 0) {
|
|
229
|
+
_logger.warn("[anyapi] No text models available");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const freeModels = allModels.filter((model) =>
|
|
234
|
+
isFreeModel({ ...model, provider: PROVIDER_ANYAPI }, allModels),
|
|
235
|
+
);
|
|
236
|
+
const stored = { free: freeModels, all: allModels };
|
|
237
|
+
|
|
238
|
+
_logger.info(
|
|
239
|
+
`[anyapi] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const reRegister = createReRegister(pi, {
|
|
243
|
+
providerId: PROVIDER_ANYAPI,
|
|
244
|
+
baseUrl: BASE_URL_ANYAPI,
|
|
245
|
+
apiKey,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
registerWithGlobalToggle(PROVIDER_ANYAPI, stored, reRegister, true);
|
|
249
|
+
|
|
250
|
+
setupProvider(
|
|
251
|
+
pi,
|
|
252
|
+
{
|
|
253
|
+
providerId: PROVIDER_ANYAPI,
|
|
254
|
+
initialShowPaid: getAnyapiShowPaid(),
|
|
255
|
+
hasKey: true,
|
|
256
|
+
tosUrl: "https://anyapi.ai/terms-of-service",
|
|
257
|
+
reRegister: (models, current) => {
|
|
258
|
+
if (current) {
|
|
259
|
+
stored.free = current.free;
|
|
260
|
+
stored.all = current.all;
|
|
261
|
+
}
|
|
262
|
+
reRegister(models);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
stored,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const showPaid = getAnyapiShowPaid();
|
|
269
|
+
reRegister(showPaid ? stored.all : stored.free);
|
|
270
|
+
}
|
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
|
|
|
@@ -216,12 +226,7 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
216
226
|
|
|
217
227
|
// Register with global toggle system
|
|
218
228
|
const hasKiloKey = !!kiloApiKey;
|
|
219
|
-
registerWithGlobalToggle(
|
|
220
|
-
PROVIDER_KILO,
|
|
221
|
-
stored,
|
|
222
|
-
reRegister,
|
|
223
|
-
hasKiloKey,
|
|
224
|
-
);
|
|
229
|
+
registerWithGlobalToggle(PROVIDER_KILO, stored, reRegister, hasKiloKey);
|
|
225
230
|
|
|
226
231
|
// OAuth config for Kilo
|
|
227
232
|
const oauthConfig = {
|
|
@@ -295,7 +300,7 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
295
300
|
"User-Agent": "pi-free-providers",
|
|
296
301
|
},
|
|
297
302
|
models: enhanceWithCI(modelsWithCompat),
|
|
298
|
-
...(
|
|
303
|
+
...(kiloApiKey ? {} : { oauth: oauthConfig }),
|
|
299
304
|
});
|
|
300
305
|
|
|
301
306
|
// Registration complete - models registered silently (use LOG_LEVEL=info to see details)
|
|
@@ -449,6 +454,15 @@ export default async function kiloProvider(pi: ExtensionAPI) {
|
|
|
449
454
|
);
|
|
450
455
|
stored.free = freeModels;
|
|
451
456
|
|
|
457
|
+
// Persist refreshed models to disk cache for fast next startup
|
|
458
|
+
saveProviderCacheGuarded(PROVIDER_KILO, allModels).catch((err) => {
|
|
459
|
+
logWarning(
|
|
460
|
+
"kilo",
|
|
461
|
+
"Failed to persist refreshed models to cache",
|
|
462
|
+
err,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
452
466
|
// Update global toggle registration
|
|
453
467
|
const baseCtxReRegister = createCtxReRegister(ctx as any, {
|
|
454
468
|
...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");
|