pi-free 2.2.2 → 2.2.4

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/lib/telemetry.ts CHANGED
@@ -105,67 +105,59 @@ const _store = createJSONStore<TelemetryStore>(TELEMETRY_FILE, {
105
105
  // =============================================================================
106
106
 
107
107
  function deriveModelTelemetry(
108
- _modelKey: string,
109
108
  entries: TelemetryEntry[],
110
109
  ): ModelTelemetry {
111
110
  const recent = entries.slice(-MAX_RECENT_CALLS);
111
+
112
+ let successCalls = 0;
113
+ let totalTokensFromSuccessful = 0;
114
+ let totalLatencyFromSuccessful = 0;
115
+ let totalTokens = 0;
116
+ let totalPromptTokens = 0;
117
+ let totalCompletionTokens = 0;
118
+ let totalLatencyMs = 0;
119
+ let totalCost = 0;
120
+
121
+ for (const e of entries) {
122
+ totalTokens += e.totalTokens;
123
+ totalPromptTokens += e.promptTokens;
124
+ totalCompletionTokens += e.completionTokens;
125
+ totalLatencyMs += e.latencyMs;
126
+ totalCost += e.cost;
127
+ if (e.success) {
128
+ successCalls++;
129
+ totalTokensFromSuccessful += e.totalTokens;
130
+ totalLatencyFromSuccessful += e.latencyMs;
131
+ }
132
+ }
133
+
112
134
  const totalCalls = entries.length;
113
- const successCalls = entries.filter((e) => e.success).length;
114
- const errorCalls = totalCalls - successCalls;
115
-
116
- const stats = entries.reduce(
117
- (acc, e) => {
118
- acc.totalTokens += e.totalTokens;
119
- acc.totalPromptTokens += e.promptTokens;
120
- acc.totalCompletionTokens += e.completionTokens;
121
- acc.totalLatencyMs += e.latencyMs;
122
- acc.totalCost += e.cost;
123
- return acc;
124
- },
125
- {
126
- totalTokens: 0,
127
- totalPromptTokens: 0,
128
- totalCompletionTokens: 0,
129
- totalLatencyMs: 0,
130
- totalCost: 0,
131
- },
132
- );
133
-
134
- const totalSuccessEntries = entries.filter((e) => e.success);
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
135
 
144
136
  return {
145
137
  totalCalls,
146
138
  successCalls,
147
- errorCalls,
148
- totalTokens: stats.totalTokens,
149
- totalPromptTokens: stats.totalPromptTokens,
150
- totalCompletionTokens: stats.totalCompletionTokens,
151
- totalLatencyMs: stats.totalLatencyMs,
152
- totalCost: stats.totalCost,
139
+ errorCalls: totalCalls - successCalls,
140
+ totalTokens,
141
+ totalPromptTokens,
142
+ totalCompletionTokens,
143
+ totalLatencyMs,
144
+ totalCost,
153
145
  avgLatencyMs:
154
- totalSuccessEntries.length > 0
155
- ? Math.round(totalLatencyFromSuccessful / totalSuccessEntries.length)
146
+ successCalls > 0
147
+ ? Math.round(totalLatencyFromSuccessful / successCalls)
156
148
  : 0,
157
149
  avgTokensPerSecond:
158
150
  totalLatencyFromSuccessful > 0
159
- ? parseFloat(
151
+ ? Number.parseFloat(
160
152
  (
161
153
  totalTokensFromSuccessful /
162
154
  (totalLatencyFromSuccessful / 1000)
163
155
  ).toFixed(1),
164
- )
156
+ )
165
157
  : 0,
166
158
  successRate:
167
159
  totalCalls > 0
168
- ? parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
160
+ ? Number.parseFloat(((successCalls / totalCalls) * 100).toFixed(1))
169
161
  : 0,
170
162
  recentCalls: recent,
171
163
  };
@@ -186,7 +178,7 @@ async function addEntry(entry: TelemetryEntry): Promise<void> {
186
178
  ...store,
187
179
  models: {
188
180
  ...store.models,
189
- [modelKey]: deriveModelTelemetry(modelKey, pruned),
181
+ [modelKey]: deriveModelTelemetry(pruned),
190
182
  },
191
183
  lastUpdated: Date.now(),
192
184
  };
@@ -310,7 +302,7 @@ export async function recordModelCall(
310
302
  const totalTokens = usage.totalTokens || usage.input + usage.output;
311
303
  const tokensPerSecond =
312
304
  latencyMs > 0
313
- ? parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
305
+ ? Number.parseFloat((totalTokens / (latencyMs / 1000)).toFixed(1))
314
306
  : 0;
315
307
 
316
308
  const entry: TelemetryEntry = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "type": "module",
5
5
  "description": "AI model providers for Pi with free model filtering and dynamic model fetching",
6
6
  "keywords": [
@@ -52,7 +52,8 @@
52
52
  "test": "vitest",
53
53
  "test:ui": "vitest --ui",
54
54
  "test:run": "vitest run",
55
- "smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts"
55
+ "smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts",
56
+ "smoke:openmodel": "tsx scripts/smoke-openmodel-wire-format.ts"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "@earendil-works/pi-ai": "^0.79.8",
@@ -551,23 +551,35 @@ function tryDirectSubstringMatch(
551
551
  modelId: string,
552
552
  modelName: string,
553
553
  ): HardcodedBenchmark | null {
554
+ // Collect ALL substring matches, then return the LONGEST key. This
555
+ // prevents short general keys (e.g. "mistral-medium-3") from shadowing
556
+ // longer specific keys (e.g. "mistral-medium-3.5") when a provider
557
+ // uses a different separator convention in the model ID.
558
+ let bestKey: string | null = null;
559
+ let bestData: HardcodedBenchmark | null = null;
554
560
  for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
555
561
  string,
556
562
  HardcodedBenchmark,
557
563
  ][]) {
558
564
  if (search.includes(key.toLowerCase())) {
559
- logDebug({
560
- provider,
561
- modelId,
562
- modelName,
563
- action: "match",
564
- strategy: "direct-substring",
565
- matchKey: key,
566
- codingIndex: data.codingIndex,
567
- });
568
- return data;
565
+ if (bestKey === null || key.length > bestKey.length) {
566
+ bestKey = key;
567
+ bestData = data;
568
+ }
569
569
  }
570
570
  }
571
+ if (bestKey !== null && bestData !== null) {
572
+ logDebug({
573
+ provider,
574
+ modelId,
575
+ modelName,
576
+ action: "match",
577
+ strategy: "direct-substring",
578
+ matchKey: bestKey,
579
+ codingIndex: bestData.codingIndex,
580
+ });
581
+ return bestData;
582
+ }
571
583
  return null;
572
584
  }
573
585
 
@@ -685,14 +697,17 @@ export function findHardcodedBenchmark(
685
697
 
686
698
  logDebug({ provider, modelId, modelName, action: "attempt" });
687
699
 
688
- // 1. Direct substring match
689
- const direct = tryDirectSubstringMatch(search, provider, modelId, modelName);
690
- if (direct) return direct;
691
-
692
- // 2. Variant alias matching
700
+ // 1. Variant alias matching (human-curated, runs first so deliberate
701
+ // aliases for separator/suffix mismatches can override generic substring
702
+ // matches).
693
703
  const variant = tryVariantAliasMatch(search, provider, modelId, modelName);
694
704
  if (variant) return variant;
695
705
 
706
+ // 2. Direct substring match (longest-key wins, so "minimax-m2.5" beats
707
+ // "minimax-m2" when both could match).
708
+ const direct = tryDirectSubstringMatch(search, provider, modelId, modelName);
709
+ if (direct) return direct;
710
+
696
711
  // 3. Provider-specific normalization
697
712
  const { result: normalizedResult, normalized } = tryProviderNormalizedMatch(
698
713
  modelId,
@@ -13,14 +13,11 @@ import type {
13
13
  } from "@earendil-works/pi-coding-agent";
14
14
  import { saveConfig } from "./config.ts";
15
15
  import { createLogger } from "./lib/logger.ts";
16
+ import type { ModelsDevEnrichedMetadata } from "./lib/types.ts";
16
17
  import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-lookup.ts";
17
18
 
18
19
  const _logger = createLogger("provider-helper");
19
20
 
20
- type ModelsDevEnrichedMetadata = {
21
- modelsDev?: Parameters<typeof enhanceModelNameWithCodingIndex>[3];
22
- };
23
-
24
21
  // =============================================================================
25
22
  // Types
26
23
  // =============================================================================
@@ -66,6 +63,14 @@ export interface OpenAICompatibleConfig {
66
63
  baseUrl: string;
67
64
  /** Environment variable name for the API key */
68
65
  apiKey: string;
66
+ /**
67
+ * Wire API to use. Defaults to `"openai-completions"` for backward
68
+ * compatibility with the 17 existing providers that pass through
69
+ * this helper without setting it. Set to `"anthropic-messages"`
70
+ * for Anthropic-protocol gateways (e.g. OpenModel). The pi-ai
71
+ * runtime dispatches to the right client based on this value.
72
+ */
73
+ api?: "openai-completions" | "anthropic-messages";
69
74
  /** Additional headers to include */
70
75
  headers?: Record<string, string>;
71
76
  /** OAuth configuration (optional) */
@@ -105,12 +110,19 @@ export function registerOpenAICompatible(
105
110
  config: OpenAICompatibleConfig,
106
111
  models: ProviderModelConfig[],
107
112
  ): void {
108
- const { providerId, baseUrl, apiKey, headers, oauth } = config;
113
+ const {
114
+ providerId,
115
+ baseUrl,
116
+ apiKey,
117
+ api = "openai-completions",
118
+ headers,
119
+ oauth,
120
+ } = config;
109
121
 
110
122
  pi.registerProvider(providerId, {
111
123
  baseUrl,
112
124
  apiKey,
113
- api: "openai-completions" as const,
125
+ api,
114
126
  headers: {
115
127
  "User-Agent": "pi-free-providers",
116
128
  ...headers,
@@ -143,13 +155,20 @@ export function createCtxReRegister(
143
155
  },
144
156
  config: OpenAICompatibleConfig,
145
157
  ): (models: ProviderModelConfig[]) => void {
146
- const { providerId, baseUrl, apiKey, headers, oauth } = config;
158
+ const {
159
+ providerId,
160
+ baseUrl,
161
+ apiKey,
162
+ api = "openai-completions",
163
+ headers,
164
+ oauth,
165
+ } = config;
147
166
 
148
167
  return (models: ProviderModelConfig[]) => {
149
168
  ctx.modelRegistry.registerProvider(providerId, {
150
169
  baseUrl,
151
170
  apiKey,
152
- api: "openai-completions" as const,
171
+ api,
153
172
  headers: {
154
173
  "User-Agent": "pi-free-providers",
155
174
  ...headers,