pi-free 2.0.15 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +64 -79
  3. package/banner.svg +21 -36
  4. package/config.ts +123 -9
  5. package/constants.ts +3 -9
  6. package/index.ts +14 -15
  7. package/lib/built-in-toggle.ts +29 -16
  8. package/lib/json-persistence.ts +90 -22
  9. package/lib/logger.ts +21 -12
  10. package/lib/model-detection.ts +2 -12
  11. package/lib/model-enhancer.ts +11 -2
  12. package/lib/model-metadata.ts +387 -0
  13. package/lib/open-browser.ts +74 -24
  14. package/lib/paths.ts +90 -0
  15. package/lib/probe-cache.ts +19 -19
  16. package/lib/provider-cache.ts +74 -28
  17. package/lib/provider-compat.ts +53 -37
  18. package/lib/provider-probe.ts +188 -0
  19. package/lib/registry.ts +1 -5
  20. package/lib/session-start-metrics.ts +46 -0
  21. package/lib/telemetry.ts +115 -86
  22. package/lib/types.ts +22 -2
  23. package/lib/util.ts +80 -21
  24. package/package.json +7 -2
  25. package/provider-failover/benchmark-lookup.ts +17 -5
  26. package/provider-helper.ts +11 -2
  27. package/providers/cline/cline-models.ts +7 -1
  28. package/providers/cline/cline-xml-bridge.ts +974 -0
  29. package/providers/cline/cline.ts +67 -176
  30. package/providers/crofai/crofai.ts +6 -1
  31. package/providers/deepinfra/deepinfra.ts +69 -2
  32. package/providers/dynamic-built-in/index.ts +237 -2
  33. package/providers/kilo/kilo-models.ts +3 -1
  34. package/providers/kilo/kilo.ts +268 -41
  35. package/providers/model-fetcher.ts +18 -55
  36. package/providers/novita/novita.ts +69 -2
  37. package/providers/ollama/ollama.ts +48 -24
  38. package/providers/opencode-session.ts +67 -2
  39. package/providers/routeway/routeway.ts +25 -17
  40. package/providers/sambanova/sambanova.ts +67 -1
  41. package/providers/together/together.ts +69 -2
  42. package/providers/tokenrouter/tokenrouter.ts +378 -0
  43. package/providers/zenmux/zenmux.ts +6 -1
  44. package/scripts/check-extensions.mjs +32 -16
  45. package/providers/nvidia/nvidia.ts +0 -510
package/lib/telemetry.ts CHANGED
@@ -7,10 +7,9 @@
7
7
  * Provides a real-world performance signal alongside static CI benchmarks.
8
8
  */
9
9
 
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
10
  import { createLogger } from "./logger.ts";
11
+ import { resolveSafeDataFile } from "./paths.ts";
12
+ import { createJSONStore } from "./json-persistence.ts";
14
13
 
15
14
  const _logger = createLogger("telemetry");
16
15
 
@@ -71,55 +70,44 @@ export interface TelemetryStore {
71
70
  // Constants
72
71
  // =============================================================================
73
72
 
74
- const TELEMETRY_DIR = join(homedir(), ".pi");
75
- const TELEMETRY_FILE = join(TELEMETRY_DIR, "free-telemetry.json");
73
+ const TELEMETRY_FILE = resolveSafeDataFile(
74
+ process.env.PI_FREE_TELEMETRY_FILE,
75
+ "free-telemetry.json",
76
+ );
76
77
  const MAX_RECENT_CALLS = 50;
77
78
 
78
- // In-flight tracking: keyed by "provider/model", value is start timestamp
79
+ // In-flight tracking: keyed by "provider/model", value is start timestamp.
80
+ // TTL: 1 hour — anything older is stale (the matching recordModelCall
81
+ // never fired, e.g. the agent was killed mid-call) and gets reaped
82
+ // on the next startModelCall/recordModelCall.
79
83
  const _inFlight = new Map<string, number>();
84
+ const _IN_FLIGHT_TTL_MS = 60 * 60 * 1000;
80
85
 
81
- // =============================================================================
82
- // Storage
83
- // =============================================================================
84
-
85
- function ensureDir(): void {
86
- if (!existsSync(TELEMETRY_DIR)) {
87
- mkdirSync(TELEMETRY_DIR, { recursive: true });
88
- }
89
- }
90
-
91
- function loadStore(): TelemetryStore {
92
- try {
93
- if (!existsSync(TELEMETRY_FILE)) {
94
- return { models: {}, lastUpdated: Date.now() };
86
+ function reapStaleInFlight(now: number): void {
87
+ for (const [key, start] of _inFlight) {
88
+ if (now - start > _IN_FLIGHT_TTL_MS) {
89
+ _inFlight.delete(key);
95
90
  }
96
- const raw = readFileSync(TELEMETRY_FILE, "utf-8");
97
- return JSON.parse(raw) as TelemetryStore;
98
- } catch (err) {
99
- _logger.warn("Failed to load telemetry store, resetting", {
100
- error: String(err),
101
- });
102
- return { models: {}, lastUpdated: Date.now() };
103
91
  }
104
92
  }
105
93
 
106
- function saveStore(store: TelemetryStore): void {
107
- try {
108
- ensureDir();
109
- store.lastUpdated = Date.now();
110
- writeFileSync(TELEMETRY_FILE, JSON.stringify(store, null, 2), "utf-8");
111
- } catch (err) {
112
- _logger.warn("Failed to save telemetry store", {
113
- error: String(err),
114
- });
115
- }
116
- }
94
+ // =============================================================================
95
+ // Storage
96
+ // =============================================================================
97
+
98
+ const _store = createJSONStore<TelemetryStore>(TELEMETRY_FILE, {
99
+ models: {},
100
+ lastUpdated: Date.now(),
101
+ });
117
102
 
118
103
  // =============================================================================
119
104
  // Entry management
120
105
  // =============================================================================
121
106
 
122
- function deriveModelTelemetry(modelKey: string, entries: TelemetryEntry[]): ModelTelemetry {
107
+ function deriveModelTelemetry(
108
+ _modelKey: string,
109
+ entries: TelemetryEntry[],
110
+ ): ModelTelemetry {
123
111
  const recent = entries.slice(-MAX_RECENT_CALLS);
124
112
  const totalCalls = entries.length;
125
113
  const successCalls = entries.filter((e) => e.success).length;
@@ -134,12 +122,24 @@ function deriveModelTelemetry(modelKey: string, entries: TelemetryEntry[]): Mode
134
122
  acc.totalCost += e.cost;
135
123
  return acc;
136
124
  },
137
- { totalTokens: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalLatencyMs: 0, totalCost: 0 },
125
+ {
126
+ totalTokens: 0,
127
+ totalPromptTokens: 0,
128
+ totalCompletionTokens: 0,
129
+ totalLatencyMs: 0,
130
+ totalCost: 0,
131
+ },
138
132
  );
139
133
 
140
134
  const totalSuccessEntries = entries.filter((e) => e.success);
141
- const totalTokensFromSuccessful = totalSuccessEntries.reduce((s, e) => s + e.totalTokens, 0);
142
- const totalLatencyFromSuccessful = totalSuccessEntries.reduce((s, e) => s + e.latencyMs, 0);
135
+ const totalTokensFromSuccessful = totalSuccessEntries.reduce(
136
+ (s, e) => s + e.totalTokens,
137
+ 0,
138
+ );
139
+ const totalLatencyFromSuccessful = totalSuccessEntries.reduce(
140
+ (s, e) => s + e.latencyMs,
141
+ 0,
142
+ );
143
143
 
144
144
  return {
145
145
  totalCalls,
@@ -150,31 +150,47 @@ function deriveModelTelemetry(modelKey: string, entries: TelemetryEntry[]): Mode
150
150
  totalCompletionTokens: stats.totalCompletionTokens,
151
151
  totalLatencyMs: stats.totalLatencyMs,
152
152
  totalCost: stats.totalCost,
153
- avgLatencyMs: totalSuccessEntries.length > 0
154
- ? Math.round(totalLatencyFromSuccessful / totalSuccessEntries.length)
155
- : 0,
156
- avgTokensPerSecond: totalLatencyFromSuccessful > 0
157
- ? parseFloat((totalTokensFromSuccessful / (totalLatencyFromSuccessful / 1000)).toFixed(1))
158
- : 0,
159
- successRate: totalCalls > 0
160
- ? parseFloat((successCalls / totalCalls * 100).toFixed(1))
161
- : 0,
153
+ avgLatencyMs:
154
+ totalSuccessEntries.length > 0
155
+ ? Math.round(totalLatencyFromSuccessful / totalSuccessEntries.length)
156
+ : 0,
157
+ avgTokensPerSecond:
158
+ totalLatencyFromSuccessful > 0
159
+ ? parseFloat(
160
+ (
161
+ totalTokensFromSuccessful /
162
+ (totalLatencyFromSuccessful / 1000)
163
+ ).toFixed(1),
164
+ )
165
+ : 0,
166
+ successRate:
167
+ totalCalls > 0
168
+ ? parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
169
+ : 0,
162
170
  recentCalls: recent,
163
171
  };
164
172
  }
165
173
 
166
- function addEntry(entry: TelemetryEntry): void {
167
- const store = loadStore();
168
- const modelKey = `${entry.provider}/${entry.model}`;
169
-
170
- const existing: TelemetryEntry[] = store.models[modelKey]?.recentCalls ?? [];
171
- existing.push(entry);
172
-
173
- // Keep only last MAX_RECENT_CALLS * 2 in raw storage (we derive stats from these)
174
- const pruned = existing.slice(-MAX_RECENT_CALLS * 2);
175
-
176
- store.models[modelKey] = deriveModelTelemetry(modelKey, pruned);
177
- saveStore(store);
174
+ async function addEntry(entry: TelemetryEntry): Promise<void> {
175
+ await _store.update((store) => {
176
+ const modelKey = `${entry.provider}/${entry.model}`;
177
+
178
+ const existing: TelemetryEntry[] =
179
+ store.models[modelKey]?.recentCalls ?? [];
180
+ existing.push(entry);
181
+
182
+ // Keep only last MAX_RECENT_CALLS * 2 in raw storage (we derive stats from these)
183
+ const pruned = existing.slice(-MAX_RECENT_CALLS * 2);
184
+
185
+ return {
186
+ ...store,
187
+ models: {
188
+ ...store.models,
189
+ [modelKey]: deriveModelTelemetry(modelKey, pruned),
190
+ },
191
+ lastUpdated: Date.now(),
192
+ };
193
+ });
178
194
  }
179
195
 
180
196
  // =============================================================================
@@ -185,23 +201,27 @@ function addEntry(entry: TelemetryEntry): void {
185
201
  * Get telemetry for all tracked models.
186
202
  */
187
203
  export function getAllTelemetry(): Record<string, ModelTelemetry> {
188
- const store = loadStore();
189
- return store.models;
204
+ return _store.load().models;
190
205
  }
191
206
 
192
207
  /**
193
208
  * Get telemetry for a specific provider/model combination.
194
209
  */
195
- export function getModelTelemetry(provider: string, model: string): ModelTelemetry | null {
196
- const store = loadStore();
197
- return store.models[`${provider}/${model}`] ?? null;
210
+ export function getModelTelemetry(
211
+ provider: string,
212
+ model: string,
213
+ ): ModelTelemetry | null {
214
+ return _store.load().models[`${provider}/${model}`] ?? null;
198
215
  }
199
216
 
200
217
  /**
201
218
  * Format a model's telemetry as a human-readable string (for status bar / /model list).
202
219
  * Returns undefined if no telemetry data is available.
203
220
  */
204
- export function formatModelTelemetry(provider: string, model: string): string | undefined {
221
+ export function formatModelTelemetry(
222
+ provider: string,
223
+ model: string,
224
+ ): string | undefined {
205
225
  const telemetry = getModelTelemetry(provider, model);
206
226
  if (!telemetry || telemetry.totalCalls === 0) return undefined;
207
227
 
@@ -230,7 +250,7 @@ export function getProviderTelemetry(provider: string): {
230
250
  totalCost: number;
231
251
  models: number;
232
252
  } {
233
- const store = loadStore();
253
+ const store = _store.load();
234
254
  let totalCalls = 0;
235
255
  let totalCost = 0;
236
256
  let models = 0;
@@ -252,7 +272,16 @@ export function getProviderTelemetry(provider: string): {
252
272
  */
253
273
  export function startModelCall(provider: string, model: string): void {
254
274
  const key = `${provider}/${model}`;
255
- _inFlight.set(key, Date.now());
275
+ const now = Date.now();
276
+ reapStaleInFlight(now);
277
+ _inFlight.set(key, now);
278
+ }
279
+
280
+ /** Options for {@link recordModelCall} */
281
+ export interface RecordModelCallOptions {
282
+ success: boolean;
283
+ stopReason?: string;
284
+ errorMessage?: string;
256
285
  }
257
286
 
258
287
  /**
@@ -263,28 +292,26 @@ export function startModelCall(provider: string, model: string): void {
263
292
  * @param model - The model ID
264
293
  * @param usage - Token usage { input, output, totalTokens }
265
294
  * @param cost - Cost in USD
266
- * @param success - Whether the call succeeded
267
- * @param stopReason - The stop reason (e.g. "stop", "error")
268
- * @param errorMessage - Error message if failed
295
+ * @param options - Options object ({@link RecordModelCallOptions})
269
296
  */
270
- export function recordModelCall(
297
+ export async function recordModelCall(
271
298
  provider: string,
272
299
  model: string,
273
300
  usage: { input: number; output: number; totalTokens: number },
274
301
  cost: number,
275
- success: boolean,
276
- stopReason?: string,
277
- errorMessage?: string,
278
- ): void {
302
+ options: RecordModelCallOptions,
303
+ ): Promise<void> {
304
+ const { success, stopReason, errorMessage } = options;
279
305
  const key = `${provider}/${model}`;
280
306
  const startTime = _inFlight.get(key) ?? Date.now();
281
307
  const latencyMs = Date.now() - startTime;
282
308
  _inFlight.delete(key);
283
309
 
284
310
  const totalTokens = usage.totalTokens || usage.input + usage.output;
285
- const tokensPerSecond = latencyMs > 0
286
- ? parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
287
- : 0;
311
+ const tokensPerSecond =
312
+ latencyMs > 0
313
+ ? parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
314
+ : 0;
288
315
 
289
316
  const entry: TelemetryEntry = {
290
317
  timestamp: Date.now(),
@@ -301,7 +328,7 @@ export function recordModelCall(
301
328
  ...(errorMessage ? { error: errorMessage } : {}),
302
329
  };
303
330
 
304
- addEntry(entry);
331
+ await addEntry(entry);
305
332
 
306
333
  _logger.info(`Telemetry: ${provider}/${model}`, {
307
334
  latencyMs,
@@ -315,9 +342,11 @@ export function recordModelCall(
315
342
  /**
316
343
  * Clear all telemetry data.
317
344
  */
318
- export function clearTelemetry(): void {
319
- const store: TelemetryStore = { models: {}, lastUpdated: Date.now() };
320
- saveStore(store);
345
+ export async function clearTelemetry(): Promise<void> {
346
+ await _store.update(() => ({
347
+ models: {},
348
+ lastUpdated: Date.now(),
349
+ }));
321
350
  }
322
351
 
323
352
  /**
package/lib/types.ts CHANGED
@@ -14,6 +14,19 @@ export interface CostConfig {
14
14
  cacheWrite: number;
15
15
  }
16
16
 
17
+ export interface ModelIdentity {
18
+ id: string;
19
+ name?: string;
20
+ family?: string;
21
+ provider?: string;
22
+ }
23
+
24
+ export type ModelMatchHints = Partial<ModelIdentity>;
25
+
26
+ export interface ModelsDevEnrichedMetadata {
27
+ modelsDev?: ModelMatchHints;
28
+ }
29
+
17
30
  export interface ProviderModelConfig {
18
31
  id: string;
19
32
  name: string;
@@ -35,6 +48,13 @@ export interface ModelsDevCost {
35
48
  cache_write?: number;
36
49
  }
37
50
 
51
+ export interface ModelsDevReasoningOption {
52
+ type: "effort" | "toggle" | "budget_tokens";
53
+ values?: string[];
54
+ min?: number;
55
+ max?: number;
56
+ }
57
+
38
58
  export interface ModelsDevLimit {
39
59
  context: number;
40
60
  output: number;
@@ -45,10 +65,10 @@ export interface ModelsDevModalities {
45
65
  output?: string[];
46
66
  }
47
67
 
48
- export interface ModelsDevModel {
49
- id: string;
68
+ export interface ModelsDevModel extends ModelIdentity {
50
69
  name: string;
51
70
  reasoning: boolean;
71
+ reasoning_options?: ModelsDevReasoningOption[];
52
72
  cost?: ModelsDevCost;
53
73
  limit: ModelsDevLimit;
54
74
  modalities?: ModelsDevModalities;
package/lib/util.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "./logger.ts";
2
+ import { safeEnrichModelsWithModelsDev } from "./model-metadata.ts";
2
3
  import {
3
4
  getProxyModelCompat,
4
5
  isLikelyReasoningModel,
@@ -6,12 +7,40 @@ import {
6
7
  import type { ProviderModelConfig as PiProviderModelConfig } from "@earendil-works/pi-coding-agent";
7
8
  import type { ProviderModelConfig } from "./types.ts";
8
9
 
10
+ /**
11
+ * Optional callbacks that providers can pass to
12
+ * `fetchOpenAICompatibleModels` to override default reasoning/compat
13
+ * detection logic. Keeping these as injected dependencies (rather
14
+ * than hard-coding `isLikelyReasoningModel` / `getProxyModelCompat`)
15
+ * lets `lib/util.ts` stay decoupled from `lib/provider-compat.ts`.
16
+ */
17
+ export interface OpenAIModelCallbacks {
18
+ /**
19
+ * Determine whether a model is a reasoning model.
20
+ * If omitted, defaults to `isLikelyReasoningModel` from provider-compat.
21
+ */
22
+ detectReasoning?: (model: { id: string; name?: string }) => boolean;
23
+ /**
24
+ * Determine proxy-compat overrides for a model.
25
+ * If omitted, defaults to `getProxyModelCompat` from provider-compat.
26
+ */
27
+ getProxyCompat?: (model: {
28
+ id: string;
29
+ name?: string;
30
+ }) => PiProviderModelConfig["compat"] | undefined;
31
+ }
32
+
9
33
  const _logger = createLogger("util");
10
34
 
11
35
  // =============================================================================
12
36
  // Shared Utilities
13
37
  // =============================================================================
14
38
 
39
+ /** Async sleep helper — avoids creating anonymous functions in loops */
40
+ export function sleep(ms: number): Promise<void> {
41
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
+ }
43
+
15
44
  /**
16
45
  * Log a warning message for provider operations
17
46
  */
@@ -74,7 +103,7 @@ export async function fetchWithRetry(
74
103
  if (response.status >= 500) {
75
104
  lastError = new Error(`Server error ${response.status}`);
76
105
  if (i < retries - 1) {
77
- await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
106
+ await sleep(delayMs * (i + 1));
78
107
  continue;
79
108
  }
80
109
  // Last retry exhausted - throw the error
@@ -85,7 +114,7 @@ export async function fetchWithRetry(
85
114
  } catch (error) {
86
115
  lastError = error;
87
116
  if (i < retries - 1) {
88
- await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
117
+ await sleep(delayMs * (i + 1));
89
118
  }
90
119
  }
91
120
  }
@@ -310,16 +339,22 @@ export function cleanModelName(name: string): string {
310
339
  // Handle patterns like "Provider : Model Name" or "Provider / Model Name"
311
340
  const colonIdx = name.indexOf(":");
312
341
  const slashIdx = name.indexOf("/");
313
- const idx =
314
- colonIdx === -1
315
- ? slashIdx
316
- : slashIdx === -1
317
- ? colonIdx
318
- : Math.min(colonIdx, slashIdx);
319
- if (idx > 0) {
320
- return name.slice(idx + 1).trim();
342
+ let idx = -1;
343
+ if (colonIdx === -1 && slashIdx === -1) {
344
+ // Neither found — return trimmed name as-is
345
+ return name.trim();
321
346
  }
322
- return name.trim();
347
+ if (colonIdx === -1) {
348
+ // Only slash found
349
+ idx = slashIdx;
350
+ } else if (slashIdx === -1) {
351
+ // Only colon found
352
+ idx = colonIdx;
353
+ } else {
354
+ // Both found — use the earliest
355
+ idx = Math.min(colonIdx, slashIdx);
356
+ }
357
+ return name.slice(idx + 1).trim();
323
358
  }
324
359
 
325
360
  // =============================================================================
@@ -335,31 +370,51 @@ export function mapOpenRouterModel(m: {
335
370
  name: string;
336
371
  context_length?: number;
337
372
  max_completion_tokens?: number | null;
338
- top_provider?: { max_completion_tokens?: number | null };
339
- pricing?: { prompt?: string | null; completion?: string | null };
373
+ top_provider?: {
374
+ context_length?: number | null;
375
+ max_completion_tokens?: number | null;
376
+ };
377
+ pricing?: {
378
+ prompt?: string | null;
379
+ completion?: string | null;
380
+ input_cache_read?: string | null;
381
+ input_cache_write?: string | null;
382
+ };
340
383
  architecture?: {
341
384
  input_modalities?: string[] | null;
342
385
  output_modalities?: string[] | null;
343
386
  };
387
+ supported_parameters?: string[] | null;
344
388
  isFree?: boolean;
345
389
  }): ProviderModelConfig {
346
390
  const promptPrice = Number.parseFloat(m.pricing?.prompt ?? "0");
347
391
  const completionPrice = Number.parseFloat(m.pricing?.completion ?? "0");
392
+ const cacheReadPrice = Number.parseFloat(
393
+ m.pricing?.input_cache_read ?? "0",
394
+ );
395
+ const cacheWritePrice = Number.parseFloat(
396
+ m.pricing?.input_cache_write ?? "0",
397
+ );
398
+ const supportedParameters = m.supported_parameters ?? [];
399
+ const reasoning =
400
+ supportedParameters.includes("reasoning") ||
401
+ supportedParameters.includes("reasoning_effort");
348
402
 
349
403
  return {
350
404
  id: m.id,
351
405
  name: cleanModelName(m.name),
352
- reasoning: false, // OpenRouter doesn't expose reasoning flag directly
406
+ reasoning,
407
+ ...(reasoning && { thinkingLevelMap: { off: "none" } }),
353
408
  input: m.architecture?.input_modalities?.includes("image")
354
409
  ? (["text", "image"] as const)
355
410
  : (["text"] as const),
356
411
  cost: {
357
412
  input: promptPrice,
358
413
  output: completionPrice,
359
- cacheRead: 0,
360
- cacheWrite: 0,
414
+ cacheRead: cacheReadPrice,
415
+ cacheWrite: cacheWritePrice,
361
416
  },
362
- contextWindow: m.context_length ?? 4096,
417
+ contextWindow: m.context_length ?? m.top_provider?.context_length ?? 4096,
363
418
  maxTokens:
364
419
  m.max_completion_tokens ?? m.top_provider?.max_completion_tokens ?? 4096,
365
420
  _pricingKnown: true,
@@ -433,8 +488,11 @@ export async function fetchOpenAICompatibleModels(
433
488
  baseUrl: string,
434
489
  apiKey: string,
435
490
  defaults: OpenAIModelDefaults = {},
491
+ callbacks: OpenAIModelCallbacks = {},
436
492
  ): Promise<PiProviderModelConfig[]> {
437
493
  const logger = createLogger(providerId);
494
+ const detectReasoning = callbacks.detectReasoning ?? isLikelyReasoningModel;
495
+ const getCompat = callbacks.getProxyCompat ?? getProxyModelCompat;
438
496
 
439
497
  logger.info(`[${providerId}] Fetching models...`);
440
498
 
@@ -463,7 +521,7 @@ export async function fetchOpenAICompatibleModels(
463
521
 
464
522
  logger.info(`[${providerId}] Fetched ${models.length} models`);
465
523
 
466
- return models
524
+ const mapped = models
467
525
  .filter((m) => m.id)
468
526
  .map((m): PiProviderModelConfig => {
469
527
  const name = m.id.split("/").pop() || m.id;
@@ -484,8 +542,7 @@ export async function fetchOpenAICompatibleModels(
484
542
  4_096;
485
543
 
486
544
  // Use per-model reasoning flag if the API provides it
487
- const reasoning =
488
- m.reasoning ?? isLikelyReasoningModel({ id: m.id, name });
545
+ const reasoning = m.reasoning ?? detectReasoning({ id: m.id, name });
489
546
 
490
547
  // Use per-model input_modalities if the API provides it
491
548
  const hasVision = m.input_modalities?.includes("image") ?? false;
@@ -521,10 +578,12 @@ export async function fetchOpenAICompatibleModels(
521
578
  },
522
579
  contextWindow,
523
580
  maxTokens,
524
- compat: getProxyModelCompat({ id: m.id, name }),
581
+ compat: getCompat({ id: m.id, name }),
525
582
  _pricingKnown: hasApiPricing,
526
583
  } as PiProviderModelConfig & { _pricingKnown?: boolean };
527
584
  });
585
+
586
+ return await safeEnrichModelsWithModelsDev(mapped, { providerId });
528
587
  } catch (error) {
529
588
  logger.error(`[${providerId}] Failed to fetch models:`, {
530
589
  error: error instanceof Error ? error.message : String(error),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "2.0.15",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "AI model providers for Pi with free model filtering and dynamic model fetching",
6
6
  "keywords": [
@@ -44,10 +44,15 @@
44
44
  "scripts/check-extensions.mjs"
45
45
  ],
46
46
  "scripts": {
47
+ "audit:prod": "npm audit --omit=dev --audit-level=high",
47
48
  "check": "node scripts/check-extensions.mjs",
49
+ "check:lockfile": "node scripts/check-lockfile-sync.mjs",
50
+ "check:tarball": "node scripts/check-tarball.mjs",
51
+ "lint": "tsc --noEmit",
48
52
  "test": "vitest",
49
53
  "test:ui": "vitest --ui",
50
- "test:run": "vitest run"
54
+ "test:run": "vitest run",
55
+ "smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts"
51
56
  },
52
57
  "peerDependencies": {
53
58
  "@earendil-works/pi-ai": "*",
@@ -12,6 +12,7 @@
12
12
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
+ import type { ModelMatchHints } from "../lib/types.ts";
15
16
  import {
16
17
  HARDCODED_BENCHMARKS,
17
18
  type HardcodedBenchmark,
@@ -677,6 +678,7 @@ export function findHardcodedBenchmark(
677
678
  modelName: string,
678
679
  modelId: string,
679
680
  provider?: string,
681
+ hints?: ModelMatchHints,
680
682
  ): HardcodedBenchmark | null {
681
683
  // Normalize: convert colons to dashes (Ollama model:tag format)
682
684
  const search = `${modelName} ${modelId}`.toLowerCase().replaceAll(":", "-");
@@ -699,9 +701,17 @@ export function findHardcodedBenchmark(
699
701
  );
700
702
  if (normalizedResult) return normalizedResult;
701
703
 
702
- // 4. Prefix fallback with base model extraction
703
- const prefix = tryPrefixFallback(normalized, provider, modelId, modelName);
704
- if (prefix) return prefix;
704
+ // 4. Prefix fallback with base model extraction. Also try models.dev
705
+ // canonical IDs/names when available for opaque gateway model IDs.
706
+ const prefixCandidates = [normalized, hints?.id, hints?.name]
707
+ .map((candidate) =>
708
+ (candidate?.trim() ?? "").toLowerCase().replaceAll(/[\s_:]+/g, "-"),
709
+ )
710
+ .filter(Boolean);
711
+ for (const candidate of prefixCandidates) {
712
+ const prefix = tryPrefixFallback(candidate, provider, modelId, modelName);
713
+ if (prefix) return prefix;
714
+ }
705
715
 
706
716
  // No match found
707
717
  logDebug({
@@ -724,8 +734,9 @@ export function getHardcodedScore(
724
734
  modelName: string,
725
735
  modelId: string,
726
736
  provider?: string,
737
+ hints?: ModelMatchHints,
727
738
  ): number | null {
728
- const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
739
+ const benchmark = findHardcodedBenchmark(modelName, modelId, provider, hints);
729
740
  return benchmark?.codingIndex ?? null;
730
741
  }
731
742
 
@@ -737,8 +748,9 @@ export function enhanceModelNameWithCodingIndex(
737
748
  modelName: string,
738
749
  modelId: string,
739
750
  provider?: string,
751
+ hints?: ModelMatchHints,
740
752
  ): string {
741
- const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
753
+ const benchmark = findHardcodedBenchmark(modelName, modelId, provider, hints);
742
754
  if (benchmark?.codingIndex !== undefined) {
743
755
  return `${modelName} [CI: ${benchmark.codingIndex.toFixed(1)}]`;
744
756
  }
@@ -17,6 +17,10 @@ import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-l
17
17
 
18
18
  const _logger = createLogger("provider-helper");
19
19
 
20
+ type ModelsDevEnrichedMetadata = {
21
+ modelsDev?: Parameters<typeof enhanceModelNameWithCodingIndex>[3];
22
+ };
23
+
20
24
  // =============================================================================
21
25
  // Types
22
26
  // =============================================================================
@@ -78,12 +82,17 @@ export interface OpenAICompatibleConfig {
78
82
  * Use this for direct provider registration (not through setupProvider)
79
83
  */
80
84
  export function enhanceWithCI(
81
- models: ProviderModelConfig[],
85
+ models: Array<ProviderModelConfig & ModelsDevEnrichedMetadata>,
82
86
  providerId?: string,
83
87
  ): ProviderModelConfig[] {
84
88
  return models.map((m) => ({
85
89
  ...m,
86
- name: enhanceModelNameWithCodingIndex(m.name, m.id, providerId),
90
+ name: enhanceModelNameWithCodingIndex(
91
+ m.name,
92
+ m.id,
93
+ providerId,
94
+ m.modelsDev,
95
+ ),
87
96
  }));
88
97
  }
89
98