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.
- package/CHANGELOG.md +179 -3
- package/README.md +495 -393
- package/config.ts +46 -54
- package/constants.ts +6 -0
- package/index.ts +39 -12
- package/lib/built-in-toggle.ts +63 -43
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +46 -0
- package/lib/registry.ts +193 -144
- package/lib/toggle-state.ts +86 -0
- package/lib/types.ts +101 -108
- package/package.json +8 -8
- package/provider-failover/benchmark-lookup.ts +637 -247
- package/provider-helper.ts +279 -260
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline-models.ts +129 -128
- package/providers/cline/cline.ts +311 -298
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +259 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo-models.ts +2 -1
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +476 -152
- package/providers/ollama/ollama.ts +130 -7
- package/providers/opencode-session.ts +3 -4
- package/providers/qwen/qwen-models.ts +101 -101
- package/providers/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +64 -55
- package/provider-factory.ts +0 -207
- package/providers/cloudflare/cloudflare.ts +0 -368
- package/providers/modal/modal.ts +0 -44
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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";
|