pi-free 2.2.5 → 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.
@@ -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.
@@ -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 { createReRegister, setupProvider } from "../../provider-helper.ts";
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 fetchBaiModels(apiKey);
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 { createReRegister, setupProvider } from "../../provider-helper.ts";
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 fetchCrofaiModels(apiKey);
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 { createReRegister, setupProvider } from "../../provider-helper.ts";
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 fetchDeepinfraModels(apiKey);
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
- let allModels: ProviderModelConfig[];
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
- try {
317
- if (config.fetchModels) {
318
- allModels = await config.fetchModels(apiKey);
319
- } else {
320
- allModels = await fetchModelsFromEndpoint({
321
- providerId: config.providerId,
322
- baseUrl: config.baseUrl,
323
- apiKey,
324
- compat: config.compat,
325
- modelDefaults: config.modelDefaults,
326
- timeoutMs: DEFAULT_FETCH_TIMEOUT_MS,
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
- // Apply DeepSeek proxy compat to matching models. OpenCode headers are
331
- // injected per request by createOpenCodeStreamSimple(), not stored here.
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}: discovery failed, Pi keeps its defaults`,
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, allModels, apiKey);
355
+ await registerProvider(pi, config, models, apiKey);
346
356
  }
347
357
 
348
358
  // =============================================================================
@@ -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
- // Try to fetch ALL models at startup (like Cline/OpenRouter)
180
- // With API key: returns all models; without: returns free-only
181
- let allModels: ProviderModelConfig[] = [];
182
- let freeModels: ProviderModelConfig[] = [];
183
-
184
- try {
185
- // Fetch all models (returns free-only if no auth, all if auth available)
186
- allModels = await fetchKiloModels({ token: kiloApiKey, freeOnly: false });
187
- // Derive free list using isFreeModel with allModels for detection
188
- freeModels = allModels.filter((m) =>
189
- isFreeModel({ ...m, provider: PROVIDER_KILO }, allModels),
190
- );
191
- } catch (error) {
192
- logWarning("kilo", "Failed to fetch models at startup", error);
193
- // Fallback: try to fetch just free models
194
- try {
195
- freeModels = await fetchKiloModels({ freeOnly: true });
196
- } catch (e) {
197
- logWarning("kilo", "Failed to fetch free models", e);
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
- ...(!!kiloApiKey ? {} : { oauth: oauthConfig }),
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 { createReRegister, setupProvider } from "../../provider-helper.ts";
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 fetchNovitaModels(apiKey);
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 { createReRegister, setupProvider } from "../../provider-helper.ts";
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 fetchOpenModelModels(apiKey);
472
+ const allModels = await loadCachedOrFetchModels(PROVIDER_OPENMODEL, () =>
473
+ fetchOpenModelModels(apiKey),
474
+ );
469
475
 
470
476
  if (allModels.length === 0) {
471
477
  _logger.warn(
@@ -45,7 +45,11 @@ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
45
45
  import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
46
46
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
47
47
  import { fetchWithTimeout } from "../../lib/util.ts";
48
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
48
+ import {
49
+ createReRegister,
50
+ loadCachedOrFetchModels,
51
+ setupProvider,
52
+ } from "../../provider-helper.ts";
49
53
 
50
54
  const _logger = createLogger("routeway");
51
55
 
@@ -303,7 +307,9 @@ export default async function routewayProvider(pi: ExtensionAPI) {
303
307
  return;
304
308
  }
305
309
 
306
- const allModels = await fetchRoutewayModels(apiKey);
310
+ const allModels = await loadCachedOrFetchModels(PROVIDER_ROUTEWAY, () =>
311
+ fetchRoutewayModels(apiKey),
312
+ );
307
313
 
308
314
  if (allModels.length === 0) {
309
315
  _logger.warn("[routeway] No chat models available");
@@ -38,7 +38,11 @@ import {
38
38
  fetchOpenAICompatibleModels,
39
39
  fetchWithTimeout,
40
40
  } from "../../lib/util.ts";
41
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
41
+ import {
42
+ createReRegister,
43
+ loadCachedOrFetchModels,
44
+ setupProvider,
45
+ } from "../../provider-helper.ts";
42
46
 
43
47
  const _logger = createLogger("sambanova");
44
48
 
@@ -57,11 +61,13 @@ export default async function sambanovaProvider(pi: ExtensionAPI) {
57
61
  }
58
62
 
59
63
  // Fetch models via shared OpenAI-compatible helper
60
- const allModels = await fetchOpenAICompatibleModels(
61
- "sambanova",
62
- BASE_URL_SAMBANOVA,
63
- apiKey,
64
- { maxTokens: 8_192 },
64
+ const allModels = await loadCachedOrFetchModels(PROVIDER_SAMBANOVA, () =>
65
+ fetchOpenAICompatibleModels(
66
+ "sambanova",
67
+ BASE_URL_SAMBANOVA,
68
+ apiKey,
69
+ { maxTokens: 8_192 },
70
+ ),
65
71
  );
66
72
 
67
73
  if (allModels.length === 0) {
@@ -50,7 +50,11 @@ import { createProviderProbe } from "../../lib/provider-probe.ts";
50
50
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
51
51
  import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
52
52
  import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
53
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
53
+ import {
54
+ createReRegister,
55
+ loadCachedOrFetchModels,
56
+ setupProvider,
57
+ } from "../../provider-helper.ts";
54
58
 
55
59
  const _logger = createLogger("together");
56
60
 
@@ -150,7 +154,9 @@ export default async function togetherProvider(pi: ExtensionAPI) {
150
154
  }
151
155
 
152
156
  // Fetch models
153
- const allModels = await fetchTogetherModels(apiKey);
157
+ const allModels = await loadCachedOrFetchModels(PROVIDER_TOGETHER, () =>
158
+ fetchTogetherModels(apiKey),
159
+ );
154
160
 
155
161
  if (allModels.length === 0) {
156
162
  _logger.warn("[together] No chat models available");
@@ -49,7 +49,11 @@ import {
49
49
  } from "../../lib/provider-compat.ts";
50
50
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
51
51
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
52
- import { enhanceWithCI, setupProvider } from "../../provider-helper.ts";
52
+ import {
53
+ enhanceWithCI,
54
+ loadCachedOrFetchModels,
55
+ setupProvider,
56
+ } from "../../provider-helper.ts";
53
57
 
54
58
  const _logger = createLogger("tokenrouter");
55
59
 
@@ -564,7 +568,9 @@ export default async function tokenRouterProvider(pi: ExtensionAPI) {
564
568
  return;
565
569
  }
566
570
 
567
- const allModels = await fetchTokenRouterModels(apiKey);
571
+ const allModels = await loadCachedOrFetchModels(PROVIDER_TOKENROUTER, () =>
572
+ fetchTokenRouterModels(apiKey),
573
+ );
568
574
 
569
575
  if (allModels.length === 0) {
570
576
  _logger.warn("[tokenrouter] No text chat models available");
@@ -31,7 +31,11 @@ import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
31
31
  import { getProxyModelCompat } from "../../lib/provider-compat.ts";
32
32
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
33
33
  import { fetchWithRetry } from "../../lib/util.ts";
34
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
34
+ import {
35
+ createReRegister,
36
+ loadCachedOrFetchModels,
37
+ setupProvider,
38
+ } from "../../provider-helper.ts";
35
39
 
36
40
  const _logger = createLogger("zenmux");
37
41
 
@@ -146,7 +150,9 @@ export default async function zenmuxProvider(pi: ExtensionAPI) {
146
150
  }
147
151
 
148
152
  // Fetch models
149
- const allModels = await fetchZenmuxModels(apiKey);
153
+ const allModels = await loadCachedOrFetchModels(PROVIDER_ZENMUX, () =>
154
+ fetchZenmuxModels(apiKey),
155
+ );
150
156
 
151
157
  if (allModels.length === 0) {
152
158
  _logger.warn("[zenmux] No models available");
@@ -1,128 +0,0 @@
1
- /**
2
- * Codestral Provider Extension
3
- *
4
- * Codestral is Mistral AI's code-focused model. This provider registers it
5
- * through the Codestral-specific endpoint (codestral.mistral.ai) using
6
- * the Mistral SDK (api: "mistral-conversations") — separate from the built-in
7
- * "mistral" provider which uses api.mistral.ai.
8
- *
9
- * NOTE: Do NOT use api: "openai-completions" here. Codestral's API is
10
- * Mistral-format (camelCase fields, maxTokens, no stream_options/store).
11
- * The OpenAI completions adapter sends OpenAI-specific fields that Mistral
12
- * rejects with HTTP 422 "Extra inputs are not permitted".
13
- *
14
- * Free tier (Experiment plan):
15
- * - 2 req/min, 500K tokens/min, 1B tokens/month
16
- * - No credit card — phone verification only
17
- * - Sign up at https://console.mistral.ai/codestral
18
- *
19
- * Endpoints:
20
- * Chat: https://codestral.mistral.ai/v1/chat/completions
21
- * FIM: https://codestral.mistral.ai/v1/fim/completions (not used by pi)
22
- *
23
- * Setup:
24
- * 1. Get API key from https://console.mistral.ai/codestral
25
- * 2. Set CODESTRAL_API_KEY env var (or MISTRAL_API_KEY as fallback)
26
- *
27
- * Usage:
28
- * pi install git:github.com/apmantza/pi-free
29
- * # Set CODESTRAL_API_KEY env var
30
- * # Models appear in /model selector as "codestral/codestral-latest"
31
- */
32
-
33
- import type {
34
- ExtensionAPI,
35
- ProviderModelConfig,
36
- } from "@earendil-works/pi-coding-agent";
37
- import {
38
- getCodestralApiKey,
39
- getCodestralShowPaid,
40
- getMistralApiKey,
41
- } from "../../config.ts";
42
- import { BASE_URL_CODESTRAL, PROVIDER_CODESTRAL } from "../../constants.ts";
43
- import { createLogger } from "../../lib/logger.ts";
44
- import { registerWithGlobalToggle } from "../../lib/registry.ts";
45
- import { enhanceWithCI, setupProvider } from "../../provider-helper.ts";
46
-
47
- const _logger = createLogger("codestral");
48
-
49
- // =============================================================================
50
- // Model Definition
51
- // =============================================================================
52
-
53
- const CODESTRAL_MODEL: ProviderModelConfig = {
54
- id: "codestral-latest",
55
- name: "Codestral",
56
- reasoning: false,
57
- input: ["text"],
58
- cost: {
59
- input: 0.3,
60
- output: 0.9,
61
- cacheRead: 0,
62
- cacheWrite: 0,
63
- },
64
- contextWindow: 256_000,
65
- maxTokens: 4_096,
66
- };
67
-
68
- // =============================================================================
69
- // Extension Entry Point
70
- // =============================================================================
71
-
72
- export default async function codestralProvider(pi: ExtensionAPI) {
73
- // Try CODESTRAL_API_KEY first, fall back to MISTRAL_API_KEY
74
- const apiKey = getCodestralApiKey() || getMistralApiKey();
75
-
76
- if (!apiKey) {
77
- _logger.info(
78
- "[codestral] Skipping — neither CODESTRAL_API_KEY nor MISTRAL_API_KEY set",
79
- );
80
- return;
81
- }
82
-
83
- const keySource = getCodestralApiKey()
84
- ? "CODESTRAL_API_KEY"
85
- : "MISTRAL_API_KEY";
86
- _logger.info(`[codestral] Using key from ${keySource}`);
87
-
88
- const allModels = [CODESTRAL_MODEL];
89
- const freeModels = allModels; // All $0.30/$0.90 — still accessible via Experiment free tier
90
- const stored = { free: freeModels, all: allModels };
91
-
92
- // Re-register function — uses mistral-conversations API (Mistral SDK)
93
- // NOT openai-completions: Codestral uses the same API format as Mistral
94
- // and rejects OpenAI-specific fields (stream_options, store, max_completion_tokens) with 422.
95
- const reRegister = (models: typeof freeModels) => {
96
- pi.registerProvider(PROVIDER_CODESTRAL, {
97
- baseUrl: BASE_URL_CODESTRAL,
98
- apiKey,
99
- api: "mistral-conversations" as const,
100
- models: enhanceWithCI(models, PROVIDER_CODESTRAL),
101
- });
102
- };
103
-
104
- // Register with global toggle
105
- registerWithGlobalToggle(PROVIDER_CODESTRAL, stored, reRegister, true);
106
-
107
- // Setup provider (toggle command, status bar, error handling)
108
- setupProvider(
109
- pi,
110
- {
111
- providerId: PROVIDER_CODESTRAL,
112
- initialShowPaid: getCodestralShowPaid(),
113
- skipToggle: true, // Only one model, no toggle needed
114
- reRegister: (models) => {
115
- stored.free = models;
116
- stored.all = models;
117
- reRegister(models);
118
- },
119
- },
120
- stored,
121
- );
122
-
123
- // Initial registration — uses mistral-conversations API (Mistral SDK)
124
- reRegister(freeModels);
125
-
126
- _logger.info(`[codestral] Registered codestral-latest via ${keySource}`);
127
-
128
- }