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.
@@ -10,6 +10,7 @@ import type {
10
10
  AssistantMessageEventStream,
11
11
  Context,
12
12
  Model,
13
+ ProviderHeaders,
13
14
  SimpleStreamOptions,
14
15
  } from "@earendil-works/pi-ai";
15
16
  import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
@@ -83,8 +84,8 @@ export type OpenCodeSessionTracker = ReturnType<
83
84
 
84
85
  export function createOpenCodeHeaders(
85
86
  tracker: OpenCodeSessionTracker,
86
- existingHeaders?: Record<string, string>,
87
- ): Record<string, string> {
87
+ existingHeaders?: ProviderHeaders,
88
+ ): ProviderHeaders {
88
89
  return {
89
90
  ...existingHeaders,
90
91
  ...OPENCODE_STATIC_HEADERS,
@@ -115,17 +116,30 @@ type StreamSimpleFn<TApi extends Api> = (
115
116
  options?: SimpleStreamOptions,
116
117
  ) => AssistantMessageEventStream;
117
118
 
118
- type AnthropicStreamModule = {
119
- streamSimpleAnthropic: StreamSimpleFn<"anthropic-messages">;
119
+ type StreamSimpleModule<TApi extends Api> = {
120
+ streamSimple?: StreamSimpleFn<TApi>;
121
+ [key: string]: unknown;
120
122
  };
121
123
 
122
- type OpenAICompletionsStreamModule = {
123
- streamSimpleOpenAICompletions: StreamSimpleFn<"openai-completions">;
124
- };
124
+ type AnthropicStreamModule = StreamSimpleModule<"anthropic-messages">;
125
+ type OpenAICompletionsStreamModule = StreamSimpleModule<"openai-completions">;
126
+
127
+ function getStreamSimple<TApi extends Api>(
128
+ module: StreamSimpleModule<TApi>,
129
+ legacyExport: string,
130
+ ): StreamSimpleFn<TApi> {
131
+ const streamSimple = module.streamSimple ?? module[legacyExport];
132
+ if (typeof streamSimple !== "function") {
133
+ throw new Error(
134
+ `Pi AI module does not export ${legacyExport} or streamSimple`,
135
+ );
136
+ }
137
+ return streamSimple as StreamSimpleFn<TApi>;
138
+ }
125
139
 
126
140
  const piAiSubpathCache = new Map<string, Promise<unknown>>();
127
141
 
128
- async function importPiAiSubpath<T>(subpath: string): Promise<T> {
142
+ function importPiAiSubpath<T>(subpath: string): Promise<T> {
129
143
  const specifier = `@earendil-works/pi-ai/${subpath}`;
130
144
  const cached = piAiSubpathCache.get(specifier) as Promise<T> | undefined;
131
145
  if (cached) return cached;
@@ -157,6 +171,9 @@ async function importPiAiRootFallback<T>(
157
171
  ): Promise<T | undefined> {
158
172
  const subpath = specifier.replace("@earendil-works/pi-ai/", "");
159
173
  const requiredExport: Record<string, string> = {
174
+ "api/anthropic-messages": "streamSimpleAnthropic",
175
+ "api/openai-completions": "streamSimpleOpenAICompletions",
176
+ // Keep compatibility with pre-0.80 Pi AI packages.
160
177
  anthropic: "streamSimpleAnthropic",
161
178
  "openai-completions": "streamSimpleOpenAICompletions",
162
179
  };
@@ -199,6 +216,37 @@ function findPiAiPackageDir(requireBase: string): string | undefined {
199
216
  return undefined;
200
217
  }
201
218
 
219
+ function resolvePiAiExportTarget(
220
+ exportsMap: Record<string, unknown> | undefined,
221
+ subpath: string,
222
+ ): string | undefined {
223
+ if (!exportsMap) return undefined;
224
+
225
+ const getTarget = (entry: unknown): string | undefined => {
226
+ if (typeof entry === "string") return entry;
227
+ if (!entry || typeof entry !== "object") return undefined;
228
+ const conditions = entry as Record<string, unknown>;
229
+ const target = conditions.import ?? conditions.default;
230
+ return typeof target === "string" ? target : undefined;
231
+ };
232
+
233
+ const exactTarget = getTarget(exportsMap[`./${subpath}`]);
234
+ if (exactTarget) return exactTarget;
235
+
236
+ for (const [pattern, entry] of Object.entries(exportsMap)) {
237
+ if (!pattern.endsWith("/*")) continue;
238
+ const prefix = pattern.slice(2, -1);
239
+ if (subpath.startsWith(prefix)) {
240
+ const target = getTarget(entry);
241
+ if (target) {
242
+ return target.replaceAll("*", subpath.slice(prefix.length));
243
+ }
244
+ }
245
+ }
246
+
247
+ return undefined;
248
+ }
249
+
202
250
  function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
203
251
  const subpath = specifier.replace("@earendil-works/pi-ai/", "");
204
252
  const candidates = [process.argv[1], import.meta.url].filter(
@@ -212,11 +260,8 @@ function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
212
260
  const pkg = JSON.parse(
213
261
  readFileSync(join(pkgDir, "package.json"), "utf-8"),
214
262
  );
215
- const exportEntry = pkg.exports?.[`./${subpath}`];
216
- const targetPath = exportEntry?.import ?? exportEntry?.default;
217
- if (typeof targetPath === "string") {
218
- return join(pkgDir, targetPath);
219
- }
263
+ const targetPath = resolvePiAiExportTarget(pkg.exports, subpath);
264
+ if (targetPath) return join(pkgDir, targetPath);
220
265
  } catch {
221
266
  // Try the next resolution base.
222
267
  }
@@ -367,8 +412,12 @@ export function createOpenCodeStreamSimple(
367
412
  void (async () => {
368
413
  try {
369
414
  if (isAnthropicOpenCodeEndpoint(model)) {
370
- const { streamSimpleAnthropic } =
371
- await importPiAiSubpath<AnthropicStreamModule>("anthropic");
415
+ const streamSimpleAnthropic = getStreamSimple(
416
+ await importPiAiSubpath<AnthropicStreamModule>(
417
+ "api/anthropic-messages",
418
+ ),
419
+ "streamSimpleAnthropic",
420
+ );
372
421
  await pipeStream(
373
422
  stream,
374
423
  streamSimpleAnthropic(
@@ -383,10 +432,12 @@ export function createOpenCodeStreamSimple(
383
432
  return;
384
433
  }
385
434
 
386
- const { streamSimpleOpenAICompletions } =
435
+ const streamSimpleOpenAICompletions = getStreamSimple(
387
436
  await importPiAiSubpath<OpenAICompletionsStreamModule>(
388
- "openai-completions",
389
- );
437
+ "api/openai-completions",
438
+ ),
439
+ "streamSimpleOpenAICompletions",
440
+ );
390
441
  await pipeStream(
391
442
  stream,
392
443
  streamSimpleOpenAICompletions(
@@ -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");
@@ -26,10 +26,8 @@ import type {
26
26
  SimpleStreamOptions,
27
27
  ThinkingContent,
28
28
  } from "@earendil-works/pi-ai";
29
- import {
30
- createAssistantMessageEventStream,
31
- streamSimpleOpenAICompletions,
32
- } from "@earendil-works/pi-ai";
29
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
30
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/compat";
33
31
  import {
34
32
  getTokenrouterApiKey,
35
33
  getTokenrouterShowPaid,
@@ -49,9 +47,14 @@ import {
49
47
  } from "../../lib/provider-compat.ts";
50
48
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
51
49
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
52
- import { enhanceWithCI, setupProvider } from "../../provider-helper.ts";
50
+ import {
51
+ enhanceWithCI,
52
+ loadCachedOrFetchModels,
53
+ setupProvider,
54
+ } from "../../provider-helper.ts";
53
55
 
54
56
  const _logger = createLogger("tokenrouter");
57
+ const streamSimpleOpenAICompletions = openAICompletionsApi().streamSimple;
55
58
 
56
59
  // =============================================================================
57
60
  // Reasoning cleanup
@@ -331,7 +334,7 @@ function createTokenRouterOpenAIStream(
331
334
  ): AssistantMessageEventStream {
332
335
  const forcePatch = isTokenRouterMinimaxModel(model.id);
333
336
  return streamSimpleOpenAICompletions(
334
- { ...model, api: "openai-completions" },
337
+ { ...model, api: "openai-completions" } as Model<"openai-completions">,
335
338
  context,
336
339
  {
337
340
  ...options,
@@ -564,7 +567,9 @@ export default async function tokenRouterProvider(pi: ExtensionAPI) {
564
567
  return;
565
568
  }
566
569
 
567
- const allModels = await fetchTokenRouterModels(apiKey);
570
+ const allModels = await loadCachedOrFetchModels(PROVIDER_TOKENROUTER, () =>
571
+ fetchTokenRouterModels(apiKey),
572
+ );
568
573
 
569
574
  if (allModels.length === 0) {
570
575
  _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
- }