pi-free 2.0.1 → 2.0.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.
@@ -1,247 +1,637 @@
1
- /**
2
- * Benchmark lookup logic — extracted from hardcoded-benchmarks.ts
3
- * for maintainability (the data file is ~10k lines of JSON-like entries).
4
- *
5
- * This module re-exports everything consumers currently import from
6
- * hardcoded-benchmarks, so you can switch imports to this file without
7
- * breaking anything.
8
- */
9
-
10
- import {
11
- HARDCODED_BENCHMARKS,
12
- type HardcodedBenchmark,
13
- } from "./hardcoded-benchmarks.ts";
14
-
15
- // Re-export the type and data so callers can migrate imports here
16
- export { HARDCODED_BENCHMARKS, type HardcodedBenchmark };
17
-
18
- // =============================================================================
19
- // Prefix fallback helpers
20
- // =============================================================================
21
-
22
- /**
23
- * Segments that indicate a variant of the same base model
24
- * (effort level, reasoning mode, date, preview) — NOT a fundamentally different model.
25
- * Used to filter prefix matches so we don't cross model boundaries
26
- * (e.g. gpt-4o → gpt-4o-mini is wrong, but gpt-4o → gpt-4o-aug-24 is fine).
27
- */
28
- const VARIANT_QUALIFIER_SEGMENTS = new Set([
29
- "reasoning",
30
- "non-reasoning",
31
- "high",
32
- "low",
33
- "medium",
34
- "xhigh",
35
- "preview",
36
- "adaptive",
37
- "fast",
38
- ]);
39
-
40
- /**
41
- * Check if a segment is a variant qualifier rather than a different model identifier.
42
- * Accepts effort levels, reasoning modes, date codes, size specifiers, and version numbers.
43
- */
44
- function isVariantQualifier(segment: string): boolean {
45
- if (VARIANT_QUALIFIER_SEGMENTS.has(segment)) return true;
46
- // Date codes like "0528", "20250514"
47
- if (/^\d{4,8}$/.test(segment)) return true;
48
- // Month names (from date suffixes like "may-25", "mar-24")
49
- if (/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$/.test(segment))
50
- return true;
51
- // Size specifiers like "70b", "8b", "a35b", "a3b" (MoE notation)
52
- if (/^a?\d+(\.\d+)?b$/i.test(segment)) return true;
53
- // Version numbers like "v3.2", "v2.5", "v1"
54
- if (/^v\d+(\.\d+)?$/.test(segment)) return true;
55
- // Two-digit year like "25", "24"
56
- if (/^\d{2}$/.test(segment)) return true;
57
- // Special variant suffixes
58
- if (segment === "speciale" || segment === "chatgpt" || segment === "latest")
59
- return true;
60
- return false;
61
- }
62
-
63
- /**
64
- * Normalize model ID by reordering size tokens to match AA convention.
65
- * Converts "70b-instruct" → "instruct-70b", "405b-chat" → "chat-405b".
66
- * AA uses instruct-70b order while providers often use 70b-instruct.
67
- */
68
- function normalizeSizeTokenOrder(id: string): string {
69
- return id.replace(/(\d+(?:\.\d+)?b)-(instruct|chat)/gi, "$2-$1");
70
- }
71
-
72
- /**
73
- * Extract the base model ID from a provider model ID.
74
- * Strips provider prefix ("openai/"), :free suffix, date suffixes, and version suffixes.
75
- */
76
- function extractBaseModelId(modelId: string): string {
77
- return modelId
78
- .toLowerCase()
79
- .replace(/^[^/]+\//, "") // Strip provider prefix like "openai/"
80
- .replace(/:free$/, "") // Strip :free suffix
81
- .replace(/-\d{8}$/, "") // Strip date suffixes like -20250514
82
- .replace(/-v\d+(\.\d+)?$/, "") // Strip version suffixes like -v1.1
83
- .replace(/-\d{3,}$/, "") // Strip numeric suffixes like -001, -2603
84
- .replace(/-it$/, "") // Strip -it suffix (Gemma convention for "instruct")
85
- .replace(/-fp\d+$/, "") // Strip -fp8, -fp16 suffixes
86
- .replace(/-bf\d+$/, "") // Strip -bf16 suffixes
87
- .trim();
88
- }
89
-
90
- /**
91
- * Find the best benchmark variant by prefix matching.
92
- * Given a base model ID, finds all benchmark keys that are variants of it
93
- * (same base model with effort/reasoning/date qualifiers) and returns the
94
- * variant with the highest codingIndex.
95
- */
96
- function findBestVariantByPrefix(baseId: string): HardcodedBenchmark | null {
97
- const prefixKey = baseId + "-";
98
- const candidates: { key: string; data: HardcodedBenchmark }[] = [];
99
-
100
- for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
101
- string,
102
- HardcodedBenchmark,
103
- ][]) {
104
- // Exact match
105
- if (key === baseId) {
106
- if (data.codingIndex !== undefined) return data;
107
- continue;
108
- }
109
-
110
- // Prefix match: key starts with baseId + "-"
111
- if (key.startsWith(prefixKey)) {
112
- // Check that the first segment after the prefix is a qualifier
113
- // (prevents gpt-4o → gpt-4o-mini cross-model matches)
114
- const remainder = key.slice(prefixKey.length);
115
- const firstSegment = remainder.split("-")[0]!;
116
- if (isVariantQualifier(firstSegment)) {
117
- candidates.push({ key, data });
118
- }
119
- }
120
- }
121
-
122
- if (candidates.length === 0) return null;
123
-
124
- // Pick the candidate with the highest codingIndex
125
- // If tied or no CI, use normalizedScore as tiebreaker
126
- candidates.sort((a, b) => {
127
- const ciA = a.data.codingIndex ?? -1;
128
- const ciB = b.data.codingIndex ?? -1;
129
- if (ciB !== ciA) return ciB - ciA;
130
- return (b.data.normalizedScore ?? 0) - (a.data.normalizedScore ?? 0);
131
- });
132
-
133
- // Only return if the best candidate has a codingIndex
134
- if (candidates[0]!.data.codingIndex !== undefined) {
135
- return candidates[0]!.data;
136
- }
137
-
138
- return null;
139
- }
140
-
141
- // =============================================================================
142
- // Main lookup
143
- // =============================================================================
144
-
145
- export function findHardcodedBenchmark(
146
- modelName: string,
147
- modelId: string,
148
- ): HardcodedBenchmark | null {
149
- const search = `${modelName} ${modelId}`.toLowerCase();
150
-
151
- // 1. Direct lookup — check if any benchmark key is a substring of the search
152
- for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
153
- string,
154
- HardcodedBenchmark,
155
- ][]) {
156
- if (search.includes(key.toLowerCase())) {
157
- return data;
158
- }
159
- }
160
-
161
- // 2. Variant matching — aliases for models with different naming conventions
162
- const variants: Record<string, string[]> = {
163
- "gpt-4o-aug-24": ["gpt-4o", "gpt-4-o"],
164
- "gpt-4": ["gpt-4", "gpt4"],
165
- "claude-3.5-sonnet-oct-24": [
166
- "claude-3.5-sonnet",
167
- "claude-3-5-sonnet",
168
- "sonnet-3.5",
169
- ],
170
- "claude-3-opus": ["claude-3-opus", "opus-3"],
171
- "llama-3.1-instruct-405b": [
172
- "llama-3.1-405b",
173
- "llama3.1-405b",
174
- "llama-405b",
175
- ],
176
- "llama-3.1-instruct-70b": ["llama-3.1-70b", "llama3.1-70b", "llama-70b"],
177
- "gemini-1.5-pro": ["gemini-1.5-pro", "gemini1.5-pro", "gemini-pro-1.5"],
178
- "qwen2.5-instruct-72b": ["qwen2.5-72b", "qwen-2.5-72b"],
179
- "deepseek-v3.2-non-reasoning": [
180
- "deepseek-v3",
181
- "deepseekv3",
182
- "deepseek-chat",
183
- ],
184
- "mimo-v2-pro": ["mimo-v2-pro", "mimo-v2-pro-free", "mimo-pro"],
185
- "mimo-v2-omni": ["mimo-v2-omni", "mimo-v2-omni-free", "mimo-omni"],
186
- "mimo-v2-flash": ["mimo-v2-flash", "mimo-v2-flash-free", "mimo-flash"],
187
- "big-pickle": ["big-pickle", "bigpickle"],
188
- "minimax-m2.5": ["minimax-m2.5", "minimax-m2.5-free", "minimax-m25"],
189
- "nvidia-nemotron-3-super-120b-a12b-reasoning": [
190
- "nemotron-3-super",
191
- "nemotron-3-super-free",
192
- "nemotron-super",
193
- "nemotron-3",
194
- ],
195
- };
196
-
197
- for (const [canonical, names] of Object.entries(variants)) {
198
- if (names.some((n) => search.includes(n.toLowerCase()))) {
199
- return HARDCODED_BENCHMARKS[canonical] || null;
200
- }
201
- }
202
-
203
- // 3. Prefix fallback — extract base model ID and find best variant
204
- // Handles cases where benchmark keys have variant suffixes
205
- // (reasoning/non-reasoning, effort levels, dates) that the model ID lacks
206
- const baseId = extractBaseModelId(modelId);
207
- if (baseId) {
208
- let best = findBestVariantByPrefix(baseId);
209
- if (best) return best;
210
-
211
- // 3b. Try with word-order normalization
212
- // (e.g., llama-3.3-70b-instruct → llama-3.3-instruct-70b)
213
- const normalizedId = normalizeSizeTokenOrder(baseId);
214
- if (normalizedId !== baseId) {
215
- best = findBestVariantByPrefix(normalizedId);
216
- if (best) return best;
217
- }
218
- }
219
-
220
- return null;
221
- }
222
-
223
- /**
224
- * Get score from hardcoded data
225
- */
226
- export function getHardcodedScore(
227
- modelName: string,
228
- modelId: string,
229
- ): number | null {
230
- const benchmark = findHardcodedBenchmark(modelName, modelId);
231
- return benchmark?.normalizedScore ?? null;
232
- }
233
-
234
- /**
235
- * Enhance model name with Coding Index score
236
- * Returns model name with CI score appended if available
237
- */
238
- export function enhanceModelNameWithCodingIndex(
239
- modelName: string,
240
- modelId: string,
241
- ): string {
242
- const benchmark = findHardcodedBenchmark(modelName, modelId);
243
- if (benchmark?.codingIndex !== undefined) {
244
- return `${modelName} [CI: ${benchmark.codingIndex.toFixed(1)}]`;
245
- }
246
- return modelName;
247
- }
1
+ /**
2
+ * Benchmark lookup logic — extracted from hardcoded-benchmarks.ts
3
+ * for maintainability (the data file is ~10k lines of JSON-like entries).
4
+ *
5
+ * This module re-exports everything consumers currently import from
6
+ * hardcoded-benchmarks, so you can switch imports to this file without
7
+ * breaking anything.
8
+ *
9
+ * ENHANCED: Added debug logging and provider-specific normalizers
10
+ */
11
+
12
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import {
16
+ HARDCODED_BENCHMARKS,
17
+ type HardcodedBenchmark,
18
+ } from "./hardcoded-benchmarks.ts";
19
+
20
+ // Re-export the type and data so callers can migrate imports here
21
+ export { HARDCODED_BENCHMARKS, type HardcodedBenchmark };
22
+
23
+ // =============================================================================
24
+ // Debug Logging
25
+ // =============================================================================
26
+
27
+ const LOG_DIR = join(homedir(), ".pi");
28
+ const LOG_FILE = join(LOG_DIR, "modelmatch.log");
29
+ let debugEnabled = true;
30
+
31
+ /**
32
+ * Enable/disable debug logging
33
+ */
34
+ export function setDebugLogging(enabled: boolean): void {
35
+ debugEnabled = enabled;
36
+ }
37
+
38
+ /**
39
+ * Log a message to the modelmatch.log file
40
+ */
41
+ function logDebug(entry: {
42
+ provider?: string;
43
+ modelId: string;
44
+ modelName: string;
45
+ action: "attempt" | "match" | "miss" | "normalized";
46
+ strategy?: string;
47
+ normalizedId?: string;
48
+ matchKey?: string;
49
+ codingIndex?: number;
50
+ details?: string;
51
+ }): void {
52
+ if (!debugEnabled) return;
53
+
54
+ try {
55
+ // Ensure log directory exists
56
+ if (!existsSync(LOG_DIR)) {
57
+ mkdirSync(LOG_DIR, { recursive: true });
58
+ }
59
+
60
+ // Initialize log file with header if it doesn't exist
61
+ if (!existsSync(LOG_FILE)) {
62
+ writeFileSync(
63
+ LOG_FILE,
64
+ "timestamp|provider|modelId|modelName|action|strategy|normalizedId|matchKey|codingIndex|details\n",
65
+ );
66
+ }
67
+
68
+ const timestamp = new Date().toISOString();
69
+ const line = [
70
+ timestamp,
71
+ entry.provider || "unknown",
72
+ entry.modelId,
73
+ entry.modelName,
74
+ entry.action,
75
+ entry.strategy || "",
76
+ entry.normalizedId || "",
77
+ entry.matchKey || "",
78
+ entry.codingIndex !== undefined ? entry.codingIndex.toFixed(1) : "",
79
+ entry.details || "",
80
+ ]
81
+ .map((f) => f.replace(/[\\|]/g, "\\$&")) // Escape backslashes and pipes
82
+ .join("|");
83
+
84
+ appendFileSync(LOG_FILE, `${line}\n`);
85
+ } catch {
86
+ // Silently fail - don't break functionality for logging issues
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get the path to the log file for user reference
92
+ */
93
+ export function getMatchLogPath(): string {
94
+ return LOG_FILE;
95
+ }
96
+
97
+ /**
98
+ * Clear the match log
99
+ */
100
+ export function clearMatchLog(): void {
101
+ try {
102
+ if (existsSync(LOG_FILE)) {
103
+ writeFileSync(
104
+ LOG_FILE,
105
+ "timestamp|provider|modelId|modelName|action|strategy|normalizedId|matchKey|codingIndex|details\n",
106
+ );
107
+ }
108
+ } catch {
109
+ // Ignore errors
110
+ }
111
+ }
112
+
113
+ // =============================================================================
114
+ // Provider-Specific Normalizers
115
+ // =============================================================================
116
+
117
+ /**
118
+ * Apply provider-specific ID normalization to handle naming conventions
119
+ */
120
+ function applyProviderNormalization(
121
+ modelId: string,
122
+ provider?: string,
123
+ ): { normalized: string; strategy: string } {
124
+ let normalized = modelId.toLowerCase();
125
+ const strategies: string[] = [];
126
+
127
+ // Provider-specific prefix stripping
128
+ if (provider === "nvidia") {
129
+ // NVIDIA uses prefixes like meta/, mistralai/, microsoft/, qwen/
130
+ const prefixMatch = normalized.match(
131
+ /^(meta|mistralai|microsoft|qwen|nvidia|ibm|google|ai21labs|bigcode|databricks|deepseek-ai|01-ai|adept|aisingapore|baai|bytedance|luma|stabilityai|fireworks|upstage|voyage|snowflake|recursal|kdan|unity|cloudflare|fblgit|nttdata|dito|nousresearch|espressomodels|ftmsh|huggingface|isolationai|pinglab|functionnetwork|huggingfaceh4|mcw|shutterstock)[^/]*\//,
132
+ );
133
+ if (prefixMatch) {
134
+ normalized = normalized.replace(/^[^/]+\//, "");
135
+ strategies.push("strip-nvidia-prefix");
136
+ }
137
+ }
138
+
139
+ if (provider === "cloudflare") {
140
+ // Cloudflare uses @cf/namespace/model format
141
+ if (normalized.startsWith("@cf/")) {
142
+ normalized = normalized.replace(/^@cf\/[^/]+\//, "");
143
+ strategies.push("strip-cf-namespace");
144
+ }
145
+ }
146
+
147
+ // Provider-agnostic normalization
148
+ // Strip :free suffix (common in OpenRouter)
149
+ if (normalized.includes(":free")) {
150
+ normalized = normalized.replace(/:free$/, "");
151
+ strategies.push("strip-free-suffix");
152
+ }
153
+
154
+ // Handle Ollama format (model:tag)
155
+ if (provider === "ollama" && normalized.includes(":")) {
156
+ normalized = normalized.replace(/:/g, "-");
157
+ strategies.push("ollama-colon-to-dash");
158
+ }
159
+
160
+ // Handle Groq suffixes
161
+ if (provider === "groq") {
162
+ if (/-\d+$/.test(normalized)) {
163
+ // Strip numeric suffixes like -32768, -131072
164
+ normalized = normalized.replace(/-\d+$/, "");
165
+ strategies.push("strip-groq-numeric-suffix");
166
+ }
167
+ if (normalized.includes("-versatile")) {
168
+ normalized = normalized.replace(/-versatile$/, "");
169
+ strategies.push("strip-groq-versatile");
170
+ }
171
+ }
172
+
173
+ // Handle Cerebras format (llama3.1-8b -> llama-3.1-8b)
174
+ if (provider === "cerebras") {
175
+ if (/^llama\d/.test(normalized)) {
176
+ normalized = normalized.replace(/^llama(\d)/, "llama-$1");
177
+ strategies.push("cerebras-llama-dash");
178
+ }
179
+ // Add instruct if missing for llama models
180
+ if (
181
+ /^llama-[\d.]+-\d+b$/.test(normalized) &&
182
+ !normalized.includes("instruct")
183
+ ) {
184
+ normalized = normalized.replace(/^(llama-[\d.]+-\d+b)/, "$1-instruct");
185
+ strategies.push("add-instruct-suffix");
186
+ }
187
+ }
188
+
189
+ // Handle Mistral -latest suffix
190
+ if (provider === "mistral" && normalized.includes("-latest")) {
191
+ normalized = normalized.replace(/-latest$/, "");
192
+ strategies.push("strip-mistral-latest");
193
+ }
194
+
195
+ // Strip common suffixes that aren't in benchmark keys
196
+ const suffixesToStrip = [
197
+ /-\d{8}$/, // Date suffixes like -20250514
198
+ /-v\d+(\.\d+)?$/, // Version suffixes like -v1.1
199
+ /-\d{3,}$/, // Numeric suffixes like -001, -2603
200
+ /-it$/, // -it (Gemma convention)
201
+ /-fp\d+$/, // -fp8, -fp16
202
+ /-bf\d+$/, // -bf16
203
+ /-preview$/, // -preview
204
+ /-exp$/, // -exp (experimental)
205
+ /-instruct-0\.\d+$/, // HuggingFace revision tags
206
+ ];
207
+
208
+ for (const pattern of suffixesToStrip) {
209
+ if (pattern.test(normalized)) {
210
+ normalized = normalized.replace(pattern, "");
211
+ strategies.push(
212
+ `strip-${pattern.source.replace(/[\\^$.*+?()[\]{}|]/g, "").slice(0, 10)}`,
213
+ );
214
+ }
215
+ }
216
+
217
+ return {
218
+ normalized,
219
+ strategy: strategies.join(","),
220
+ };
221
+ }
222
+
223
+ // =============================================================================
224
+ // Prefix fallback helpers
225
+ // =============================================================================
226
+
227
+ /**
228
+ * Segments that indicate a variant of the same base model
229
+ * (effort level, reasoning mode, date, preview) NOT a fundamentally different model.
230
+ * Used to filter prefix matches so we don't cross model boundaries
231
+ * (e.g. gpt-4o → gpt-4o-mini is wrong, but gpt-4o → gpt-4o-aug-24 is fine).
232
+ */
233
+ const VARIANT_QUALIFIER_SEGMENTS = new Set([
234
+ "reasoning",
235
+ "non-reasoning",
236
+ "high",
237
+ "low",
238
+ "medium",
239
+ "xhigh",
240
+ "preview",
241
+ "adaptive",
242
+ "fast",
243
+ ]);
244
+
245
+ /**
246
+ * Check if a segment is a variant qualifier rather than a different model identifier.
247
+ * Accepts effort levels, reasoning modes, date codes, size specifiers, and version numbers.
248
+ */
249
+ function isVariantQualifier(segment: string): boolean {
250
+ if (VARIANT_QUALIFIER_SEGMENTS.has(segment)) return true;
251
+ // Date codes like "0528", "20250514"
252
+ if (/^\d{4,8}$/.test(segment)) return true;
253
+ // Month names (from date suffixes like "may-25", "mar-24")
254
+ if (/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$/.test(segment))
255
+ return true;
256
+ // Size specifiers like "70b", "8b", "a35b", "a3b" (MoE notation)
257
+ if (/^a?\d+(\.\d+)?b$/i.test(segment)) return true;
258
+ // Version numbers like "v3.2", "v2.5", "v1"
259
+ if (/^v\d+(\.\d+)?$/.test(segment)) return true;
260
+ // Two-digit year like "25", "24"
261
+ if (/^\d{2}$/.test(segment)) return true;
262
+ // Special variant suffixes
263
+ if (segment === "speciale" || segment === "chatgpt" || segment === "latest")
264
+ return true;
265
+ return false;
266
+ }
267
+
268
+ /**
269
+ * Normalize model ID by reordering size tokens to match AA convention.
270
+ * Converts "70b-instruct" → "instruct-70b", "405b-chat" → "chat-405b".
271
+ * AA uses instruct-70b order while providers often use 70b-instruct.
272
+ */
273
+ function normalizeSizeTokenOrder(id: string): string {
274
+ return id.replace(/(\d+(?:\.\d+)?b)-(instruct|chat)/gi, "$2-$1");
275
+ }
276
+
277
+ /**
278
+ * Extract the base model ID from a provider model ID.
279
+ * Strips ALL provider prefixes ("openai/", "@cf/meta/", "@cf/qwen/"), :free suffix, date suffixes, and version suffixes.
280
+ */
281
+ function extractBaseModelId(modelId: string): string {
282
+ return modelId
283
+ .toLowerCase()
284
+ .replace(/^.*\//, "") // Strip ALL path prefixes - keep only last segment
285
+ .replace(/:free$/, "") // Strip :free suffix
286
+ .replace(/-\d{8}$/, "") // Strip date suffixes like -20250514
287
+ .replace(/-v\d+(\.\d+)?$/, "") // Strip version suffixes like -v1.1
288
+ .replace(/-\d{3,}$/, "") // Strip numeric suffixes like -001, -2603
289
+ .replace(/-it$/, "") // Strip -it suffix (Gemma convention for "instruct")
290
+ .replace(/-fp\d+$/, "") // Strip -fp8, -fp16 suffixes
291
+ .replace(/-bf\d+$/, "") // Strip -bf16 suffixes
292
+ .trim();
293
+ }
294
+
295
+ /**
296
+ * Find the best benchmark variant by prefix matching.
297
+ * Given a base model ID, finds all benchmark keys that are variants of it
298
+ * (same base model with effort/reasoning/date qualifiers) and returns the
299
+ * variant with the highest codingIndex.
300
+ */
301
+ function findBestVariantByPrefix(
302
+ baseId: string,
303
+ provider?: string,
304
+ originalId?: string,
305
+ ): HardcodedBenchmark | null {
306
+ const prefixKey = baseId + "-";
307
+ const candidates: { key: string; data: HardcodedBenchmark }[] = [];
308
+
309
+ for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
310
+ string,
311
+ HardcodedBenchmark,
312
+ ][]) {
313
+ // Exact match
314
+ if (key === baseId) {
315
+ if (data.codingIndex !== undefined) {
316
+ logDebug({
317
+ provider,
318
+ modelId: originalId || baseId,
319
+ modelName: "",
320
+ action: "match",
321
+ strategy: "exact-prefix-match",
322
+ matchKey: key,
323
+ codingIndex: data.codingIndex,
324
+ });
325
+ return data;
326
+ }
327
+ continue;
328
+ }
329
+
330
+ // Prefix match: key starts with baseId + "-"
331
+ if (key.startsWith(prefixKey)) {
332
+ // Check that the first segment after the prefix is a qualifier
333
+ // (prevents gpt-4o → gpt-4o-mini cross-model matches)
334
+ const remainder = key.slice(prefixKey.length);
335
+ const firstSegment = remainder.split("-")[0]!;
336
+ if (isVariantQualifier(firstSegment)) {
337
+ candidates.push({ key, data });
338
+ }
339
+ }
340
+ }
341
+
342
+ if (candidates.length === 0) return null;
343
+
344
+ // Pick the candidate with the highest codingIndex
345
+ // If tied or no CI, use normalizedScore as tiebreaker
346
+ candidates.sort((a, b) => {
347
+ const ciA = a.data.codingIndex ?? -1;
348
+ const ciB = b.data.codingIndex ?? -1;
349
+ if (ciB !== ciA) return ciB - ciA;
350
+ return (b.data.normalizedScore ?? 0) - (a.data.normalizedScore ?? 0);
351
+ });
352
+
353
+ // Only return if the best candidate has a codingIndex
354
+ if (candidates[0]!.data.codingIndex !== undefined) {
355
+ logDebug({
356
+ provider,
357
+ modelId: originalId || baseId,
358
+ modelName: "",
359
+ action: "match",
360
+ strategy: "variant-prefix-match",
361
+ normalizedId: baseId,
362
+ matchKey: candidates[0]!.key,
363
+ codingIndex: candidates[0]!.data.codingIndex,
364
+ details: `${candidates.length} candidates`,
365
+ });
366
+ return candidates[0]!.data;
367
+ }
368
+
369
+ return null;
370
+ }
371
+
372
+ // =============================================================================
373
+ // Main lookup
374
+ // =============================================================================
375
+
376
+ export function findHardcodedBenchmark(
377
+ modelName: string,
378
+ modelId: string,
379
+ provider?: string,
380
+ ): HardcodedBenchmark | null {
381
+ const search = `${modelName} ${modelId}`.toLowerCase();
382
+
383
+ logDebug({
384
+ provider,
385
+ modelId,
386
+ modelName,
387
+ action: "attempt",
388
+ });
389
+
390
+ // 1. Direct lookup — check if any benchmark key is a substring of the search
391
+ for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
392
+ string,
393
+ HardcodedBenchmark,
394
+ ][]) {
395
+ if (search.includes(key.toLowerCase())) {
396
+ logDebug({
397
+ provider,
398
+ modelId,
399
+ modelName,
400
+ action: "match",
401
+ strategy: "direct-substring",
402
+ matchKey: key,
403
+ codingIndex: data.codingIndex,
404
+ });
405
+ return data;
406
+ }
407
+ }
408
+
409
+ // 2. Variant matching — aliases for models with different naming conventions
410
+ const variants: Record<string, string[]> = {
411
+ "gpt-4o-aug-24": ["gpt-4o", "gpt-4-o"],
412
+ "gpt-4": ["gpt-4", "gpt4"],
413
+ "claude-3.5-sonnet-oct-24": [
414
+ "claude-3.5-sonnet",
415
+ "claude-3-5-sonnet",
416
+ "sonnet-3.5",
417
+ ],
418
+ "claude-3-opus": ["claude-3-opus", "opus-3"],
419
+ "llama-3.1-instruct-405b": [
420
+ "llama-3.1-405b",
421
+ "llama3.1-405b",
422
+ "llama-405b",
423
+ ],
424
+ "llama-3.1-instruct-70b": ["llama-3.1-70b", "llama3.1-70b", "llama-70b"],
425
+ "gemini-1.5-pro": ["gemini-1.5-pro", "gemini1.5-pro", "gemini-pro-1.5"],
426
+ "qwen2.5-instruct-72b": ["qwen2.5-72b", "qwen-2.5-72b"],
427
+ "deepseek-v3.2-non-reasoning": [
428
+ "deepseek-v3",
429
+ "deepseekv3",
430
+ "deepseek-chat",
431
+ ],
432
+ "mimo-v2-pro": ["mimo-v2-pro", "mimo-v2-pro-free", "mimo-pro"],
433
+ "mimo-v2-omni": ["mimo-v2-omni", "mimo-v2-omni-free", "mimo-omni"],
434
+ "mimo-v2-flash": ["mimo-v2-flash", "mimo-v2-flash-free", "mimo-flash"],
435
+ "big-pickle": ["big-pickle", "bigpickle"],
436
+ "minimax-m2.5": ["minimax-m2.5", "minimax-m2.5-free", "minimax-m25"],
437
+ "nvidia-nemotron-3-super-120b-a12b-reasoning": [
438
+ "nemotron-3-super",
439
+ "nemotron-3-super-free",
440
+ "nemotron-super",
441
+ "nemotron-3",
442
+ ],
443
+ };
444
+
445
+ for (const [canonical, names] of Object.entries(variants)) {
446
+ if (names.some((n) => search.includes(n.toLowerCase()))) {
447
+ const data = HARDCODED_BENCHMARKS[canonical];
448
+ if (data) {
449
+ logDebug({
450
+ provider,
451
+ modelId,
452
+ modelName,
453
+ action: "match",
454
+ strategy: "variant-alias",
455
+ matchKey: canonical,
456
+ codingIndex: data.codingIndex,
457
+ });
458
+ return data;
459
+ }
460
+ }
461
+ }
462
+
463
+ // 3. Provider-specific normalization
464
+ const { normalized: providerNormalized, strategy: providerStrategy } =
465
+ applyProviderNormalization(modelId, provider);
466
+
467
+ if (providerNormalized !== modelId.toLowerCase()) {
468
+ logDebug({
469
+ provider,
470
+ modelId,
471
+ modelName,
472
+ action: "normalized",
473
+ strategy: providerStrategy,
474
+ normalizedId: providerNormalized,
475
+ });
476
+
477
+ // Try exact match on normalized ID
478
+ for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
479
+ string,
480
+ HardcodedBenchmark,
481
+ ][]) {
482
+ if (providerNormalized.includes(key.toLowerCase())) {
483
+ logDebug({
484
+ provider,
485
+ modelId,
486
+ modelName,
487
+ action: "match",
488
+ strategy: `provider-normalized:${providerStrategy}`,
489
+ matchKey: key,
490
+ codingIndex: data.codingIndex,
491
+ });
492
+ return data;
493
+ }
494
+ }
495
+ }
496
+
497
+ // 4. Prefix fallback — extract base model ID and find best variant
498
+ // Handles cases where benchmark keys have variant suffixes
499
+ // (reasoning/non-reasoning, effort levels, dates) that the model ID lacks
500
+ const baseId = extractBaseModelId(providerNormalized);
501
+ if (baseId) {
502
+ let best = findBestVariantByPrefix(baseId, provider, modelId);
503
+ if (best) return best;
504
+
505
+ // 4b. Try with word-order normalization
506
+ // (e.g., llama-3.3-70b-instruct → llama-3.3-instruct-70b)
507
+ const normalizedId = normalizeSizeTokenOrder(baseId);
508
+ if (normalizedId !== baseId) {
509
+ logDebug({
510
+ provider,
511
+ modelId,
512
+ modelName,
513
+ action: "normalized",
514
+ strategy: "size-token-reorder",
515
+ normalizedId: normalizedId,
516
+ });
517
+ best = findBestVariantByPrefix(normalizedId, provider, modelId);
518
+ if (best) return best;
519
+ }
520
+ }
521
+
522
+ // No match found
523
+ logDebug({
524
+ provider,
525
+ modelId,
526
+ modelName,
527
+ action: "miss",
528
+ strategy: "all-strategies-failed",
529
+ normalizedId: baseId || providerNormalized,
530
+ details: `Final normalized: ${baseId || providerNormalized}`,
531
+ });
532
+
533
+ return null;
534
+ }
535
+
536
+ /**
537
+ * Get score from hardcoded data
538
+ */
539
+ export function getHardcodedScore(
540
+ modelName: string,
541
+ modelId: string,
542
+ provider?: string,
543
+ ): number | null {
544
+ const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
545
+ return benchmark?.normalizedScore ?? null;
546
+ }
547
+
548
+ /**
549
+ * Enhance model name with Coding Index score
550
+ * Returns model name with CI score appended if available
551
+ */
552
+ export function enhanceModelNameWithCodingIndex(
553
+ modelName: string,
554
+ modelId: string,
555
+ provider?: string,
556
+ ): string {
557
+ const benchmark = findHardcodedBenchmark(modelName, modelId, provider);
558
+ if (benchmark?.codingIndex !== undefined) {
559
+ return `${modelName} [CI: ${benchmark.codingIndex.toFixed(1)}]`;
560
+ }
561
+ return modelName;
562
+ }
563
+
564
+ // =============================================================================
565
+ // Stats and Reporting
566
+ // =============================================================================
567
+
568
+ /**
569
+ * Get statistics about model matching from the current session
570
+ * Note: This reads the log file and computes stats
571
+ */
572
+ export function getMatchingStats(): {
573
+ totalAttempts: number;
574
+ matches: number;
575
+ misses: number;
576
+ matchRate: number;
577
+ byProvider: Record<
578
+ string,
579
+ { attempts: number; matches: number; misses: number }
580
+ >;
581
+ } {
582
+ const stats = {
583
+ totalAttempts: 0,
584
+ matches: 0,
585
+ misses: 0,
586
+ matchRate: 0,
587
+ byProvider: {} as Record<
588
+ string,
589
+ { attempts: number; matches: number; misses: number }
590
+ >,
591
+ };
592
+
593
+ try {
594
+ if (!existsSync(LOG_FILE)) {
595
+ return stats;
596
+ }
597
+
598
+ const content = readFileSync(LOG_FILE, "utf-8");
599
+ const lines = content.split("\n").slice(1); // Skip header
600
+
601
+ for (const line of lines) {
602
+ if (!line.trim()) continue;
603
+ const parts = line.split("|");
604
+ if (parts.length < 5) continue;
605
+
606
+ const provider = parts[1] || "unknown";
607
+ const action = parts[4];
608
+
609
+ if (!stats.byProvider[provider]) {
610
+ stats.byProvider[provider] = { attempts: 0, matches: 0, misses: 0 };
611
+ }
612
+
613
+ if (action === "attempt") {
614
+ stats.totalAttempts++;
615
+ stats.byProvider[provider].attempts++;
616
+ } else if (action === "match") {
617
+ stats.matches++;
618
+ stats.byProvider[provider].matches++;
619
+ } else if (action === "miss") {
620
+ stats.misses++;
621
+ stats.byProvider[provider].misses++;
622
+ }
623
+ }
624
+
625
+ stats.matchRate =
626
+ stats.totalAttempts > 0
627
+ ? Math.round((stats.matches / (stats.matches + stats.misses)) * 100)
628
+ : 0;
629
+ } catch {
630
+ // Return empty stats on error
631
+ }
632
+
633
+ return stats;
634
+ }
635
+
636
+ // Need to import readFileSync for stats
637
+ import { readFileSync } from "node:fs";