@oh-my-pi/pi-coding-agent 14.0.4 → 14.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.
- package/CHANGELOG.md +83 -0
- package/package.json +11 -8
- package/src/async/index.ts +1 -0
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/model-selection.ts +16 -13
- package/src/config/model-equivalence.ts +674 -0
- package/src/config/model-registry.ts +182 -13
- package/src/config/model-resolver.ts +203 -74
- package/src/config/settings-schema.ts +23 -0
- package/src/config/settings.ts +9 -2
- package/src/dap/session.ts +31 -39
- package/src/debug/log-formatting.ts +2 -2
- package/src/edit/modes/chunk.ts +8 -3
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +612 -97
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/lsp/client.ts +5 -3
- package/src/lsp/index.ts +4 -9
- package/src/lsp/utils.ts +26 -0
- package/src/main.ts +6 -1
- package/src/memories/index.ts +7 -6
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/controllers/command-controller.ts +18 -0
- package/src/modes/controllers/event-controller.ts +438 -426
- package/src/modes/controllers/selector-controller.ts +13 -5
- package/src/modes/theme/mermaid-cache.ts +5 -7
- package/src/priority.json +8 -0
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +15 -0
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +39 -40
- package/src/prompts/tools/read-chunk.md +13 -1
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +7 -4
- package/src/session/agent-session.ts +33 -6
- package/src/session/compaction/compaction.ts +1 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/await-tool.ts +2 -1
- package/src/tools/bash.ts +221 -56
- package/src/tools/browser.ts +84 -21
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find.ts +40 -94
- package/src/tools/gemini-image.ts +1 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/read.ts +218 -1
- package/src/tools/render-utils.ts +1 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +24 -1
- package/src/utils/image-resize.ts +73 -37
- package/src/utils/title-generator.ts +1 -1
- package/src/web/scrapers/types.ts +50 -32
- package/src/web/search/providers/codex.ts +21 -2
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { type Api, getBundledModels, getBundledProviders, type Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
export type CanonicalModelSource = "override" | "bundled" | "heuristic" | "fallback";
|
|
4
|
+
|
|
5
|
+
export interface ModelEquivalenceConfig {
|
|
6
|
+
overrides?: Record<string, string>;
|
|
7
|
+
exclude?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CanonicalModelVariant {
|
|
11
|
+
canonicalId: string;
|
|
12
|
+
selector: string;
|
|
13
|
+
model: Model<Api>;
|
|
14
|
+
source: CanonicalModelSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CanonicalModelRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
variants: CanonicalModelVariant[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CanonicalModelIndex {
|
|
24
|
+
records: CanonicalModelRecord[];
|
|
25
|
+
byId: Map<string, CanonicalModelRecord>;
|
|
26
|
+
bySelector: Map<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CanonicalReferenceData {
|
|
30
|
+
references: Map<string, Model<Api>>;
|
|
31
|
+
officialIds: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CompiledEquivalenceConfig {
|
|
35
|
+
overrides: Map<string, string>;
|
|
36
|
+
exclude: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ResolvedCanonicalModel {
|
|
40
|
+
id: string;
|
|
41
|
+
source: CanonicalModelSource;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const TRAILING_CANONICAL_MARKERS = [
|
|
45
|
+
"thinking",
|
|
46
|
+
"customtools",
|
|
47
|
+
"high",
|
|
48
|
+
"low",
|
|
49
|
+
"medium",
|
|
50
|
+
"minimal",
|
|
51
|
+
"xhigh",
|
|
52
|
+
"free",
|
|
53
|
+
"exacto",
|
|
54
|
+
"original",
|
|
55
|
+
"optimized",
|
|
56
|
+
"nvfp4",
|
|
57
|
+
"fp8",
|
|
58
|
+
"fp4",
|
|
59
|
+
"bf16",
|
|
60
|
+
"int8",
|
|
61
|
+
"int4",
|
|
62
|
+
] as const;
|
|
63
|
+
const WRAPPER_PREFIXES = ["duo-chat-"] as const;
|
|
64
|
+
const FAMILY_EXTRACTION_PATTERNS = [
|
|
65
|
+
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i,
|
|
66
|
+
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i,
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
70
|
+
if (!existing) return true;
|
|
71
|
+
if (candidate.contextWindow !== existing.contextWindow) {
|
|
72
|
+
return candidate.contextWindow > existing.contextWindow;
|
|
73
|
+
}
|
|
74
|
+
if (candidate.maxTokens !== existing.maxTokens) {
|
|
75
|
+
return candidate.maxTokens > existing.maxTokens;
|
|
76
|
+
}
|
|
77
|
+
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createCanonicalReferenceData(): CanonicalReferenceData {
|
|
81
|
+
const references = new Map<string, Model<Api>>();
|
|
82
|
+
for (const provider of getBundledProviders()) {
|
|
83
|
+
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
84
|
+
const candidate = model as Model<Api>;
|
|
85
|
+
const existing = references.get(candidate.id);
|
|
86
|
+
if (shouldReplaceReference(existing, candidate)) {
|
|
87
|
+
references.set(candidate.id, candidate);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
references,
|
|
93
|
+
officialIds: new Set(references.keys()),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeSelectorKey(selector: string): string {
|
|
98
|
+
return selector.trim().toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeCanonicalIdKey(canonicalId: string): string {
|
|
102
|
+
return canonicalId.trim().toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatCanonicalVariantSelector(model: Model<Api>): string {
|
|
106
|
+
return `${model.provider}/${model.id}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildOverrideMap(overrides: Record<string, string> | undefined): Map<string, string> {
|
|
110
|
+
const result = new Map<string, string>();
|
|
111
|
+
if (!overrides) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
for (const [selector, canonicalId] of Object.entries(overrides)) {
|
|
115
|
+
const normalizedSelector = normalizeSelectorKey(selector);
|
|
116
|
+
const normalizedCanonicalId = canonicalId.trim();
|
|
117
|
+
if (!normalizedSelector || !normalizedCanonicalId) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
result.set(normalizedSelector, normalizedCanonicalId);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildExclusionSet(exclusions: readonly string[] | undefined): Set<string> {
|
|
126
|
+
const result = new Set<string>();
|
|
127
|
+
for (const selector of exclusions ?? []) {
|
|
128
|
+
const normalized = normalizeSelectorKey(selector);
|
|
129
|
+
if (normalized) {
|
|
130
|
+
result.add(normalized);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function compileEquivalenceConfig(config: ModelEquivalenceConfig | undefined): CompiledEquivalenceConfig {
|
|
137
|
+
return {
|
|
138
|
+
overrides: buildOverrideMap(config?.overrides),
|
|
139
|
+
exclude: buildExclusionSet(config?.exclude),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addCanonicalCandidate(candidates: Set<string>, candidate: string): void {
|
|
144
|
+
const normalized = candidate.trim();
|
|
145
|
+
if (normalized) {
|
|
146
|
+
candidates.add(normalized);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stripTrailingMarker(candidate: string): string | undefined {
|
|
151
|
+
for (const marker of TRAILING_CANONICAL_MARKERS) {
|
|
152
|
+
for (const separator of ["-", ":"] as const) {
|
|
153
|
+
const suffix = `${separator}${marker}`;
|
|
154
|
+
if (candidate.toLowerCase().endsWith(suffix)) {
|
|
155
|
+
return candidate.slice(0, -suffix.length);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function lowercaseCandidate(candidate: string): string | undefined {
|
|
163
|
+
const lowercased = candidate.toLowerCase();
|
|
164
|
+
return lowercased !== candidate ? lowercased : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function stripSyntheticPrefix(candidate: string): string | undefined {
|
|
168
|
+
const stripped = candidate.replace(/^hf:/i, "");
|
|
169
|
+
return stripped !== candidate ? stripped : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function stripLatestSuffix(candidate: string): string | undefined {
|
|
173
|
+
const stripped = candidate.replace(/-latest$/i, "");
|
|
174
|
+
return stripped !== candidate ? stripped : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stripLegacyGlmTurboSuffix(candidate: string): string | undefined {
|
|
178
|
+
const stripped = candidate.replace(/^(glm-4(?:\.\d+)?v?)-turbo$/i, "$1");
|
|
179
|
+
return stripped !== candidate ? stripped : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function reorderAnthropicFamily(candidate: string): string | undefined {
|
|
183
|
+
const match = /^claude-(\d+(?:[.-]\d+)+)-(opus|sonnet|haiku)$/i.exec(candidate);
|
|
184
|
+
if (!match) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const [, version, family] = match;
|
|
188
|
+
return `claude-${family.toLowerCase()}-${version}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stripProviderVersionSuffix(candidate: string): string | undefined {
|
|
192
|
+
const stripped = candidate.replace(/-v\d+(?::\d+)?$/i, "");
|
|
193
|
+
return stripped !== candidate ? stripped : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stripDateSuffix(candidate: string): string | undefined {
|
|
197
|
+
const stripped = candidate.replace(/-\d{8}$/i, "");
|
|
198
|
+
return stripped !== candidate ? stripped : undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function insertAttachedFamilyVersionSeparator(candidate: string): string | undefined {
|
|
202
|
+
const inserted = candidate.replace(
|
|
203
|
+
/(^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder))(\d+(?:[.-]\d+)*)(?=$|[-_/.:a-z])/gi,
|
|
204
|
+
"$1$2-$3",
|
|
205
|
+
);
|
|
206
|
+
return inserted !== candidate ? inserted : undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function toggleSeriesMinorVersionSeparators(candidate: string): string[] {
|
|
210
|
+
const toggled = new Set<string>();
|
|
211
|
+
const dotToDash = candidate.replace(/(^|[/:._-])([a-z])(\d)\.(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3-$4");
|
|
212
|
+
if (dotToDash !== candidate) {
|
|
213
|
+
toggled.add(dotToDash);
|
|
214
|
+
}
|
|
215
|
+
const dashToDot = candidate.replace(/(^|[/:._-])([a-z])(\d)-(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3.$4");
|
|
216
|
+
if (dashToDot !== candidate) {
|
|
217
|
+
toggled.add(dashToDot);
|
|
218
|
+
}
|
|
219
|
+
return [...toggled];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function expandCompactSeriesMinorVersions(candidate: string): string[] {
|
|
223
|
+
const expanded = new Set<string>();
|
|
224
|
+
const compactToDash = candidate.replace(/(^|[/:._-])([a-z])(\d)(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3-$4");
|
|
225
|
+
if (compactToDash !== candidate) {
|
|
226
|
+
expanded.add(compactToDash);
|
|
227
|
+
}
|
|
228
|
+
const compactToDot = candidate.replace(/(^|[/:._-])([a-z])(\d)(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3.$4");
|
|
229
|
+
if (compactToDot !== candidate) {
|
|
230
|
+
expanded.add(compactToDot);
|
|
231
|
+
}
|
|
232
|
+
return [...expanded];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getQualifiedNamespaceSuffixes(candidate: string): string[] {
|
|
236
|
+
const results = new Set<string>();
|
|
237
|
+
for (let index = 1; index < candidate.length; index += 1) {
|
|
238
|
+
if (!/[/:.]/.test(candidate[index - 1]!)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const suffix = candidate.slice(index);
|
|
242
|
+
if (suffix.length < 4) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!/[a-z]/i.test(suffix) || !/\d/.test(suffix)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
addCanonicalCandidate(results, suffix);
|
|
249
|
+
}
|
|
250
|
+
return [...results];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractUpstreamFamilyCandidate(candidate: string): string | undefined {
|
|
254
|
+
for (const pattern of FAMILY_EXTRACTION_PATTERNS) {
|
|
255
|
+
const match = pattern.exec(candidate);
|
|
256
|
+
if (match?.[1]) {
|
|
257
|
+
return match[1];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCandidatePenalty(candidate: string): number {
|
|
264
|
+
let penalty = 0;
|
|
265
|
+
if (candidate.includes("/")) {
|
|
266
|
+
penalty += 100;
|
|
267
|
+
}
|
|
268
|
+
if (candidate.includes(":")) {
|
|
269
|
+
penalty += 40;
|
|
270
|
+
}
|
|
271
|
+
if (/-\d{8}$/i.test(candidate)) {
|
|
272
|
+
penalty += 25;
|
|
273
|
+
}
|
|
274
|
+
if (/-v\d+(?::\d+)?$/i.test(candidate)) {
|
|
275
|
+
penalty += 25;
|
|
276
|
+
}
|
|
277
|
+
if (stripTrailingMarker(candidate)) {
|
|
278
|
+
penalty += 20;
|
|
279
|
+
}
|
|
280
|
+
if (/[A-Z]/.test(candidate)) {
|
|
281
|
+
penalty += 10;
|
|
282
|
+
}
|
|
283
|
+
if (/^claude-\d/i.test(candidate)) {
|
|
284
|
+
penalty += 20;
|
|
285
|
+
}
|
|
286
|
+
if (/^claude-(?:opus|sonnet|haiku)-\d{2}(?=$|[-_a-z])/i.test(candidate)) {
|
|
287
|
+
penalty += 10;
|
|
288
|
+
}
|
|
289
|
+
if (/(?:^|[/:._-])[a-z]\d-\d(?=$|[-_/.:a-z])/i.test(candidate)) {
|
|
290
|
+
penalty += 6;
|
|
291
|
+
}
|
|
292
|
+
if (/(?:^|[-_/])\d-\d(?=$|[-_a-z])/.test(candidate) && !/^claude-(?:opus|sonnet|haiku)-\d-\d/i.test(candidate)) {
|
|
293
|
+
penalty += 4;
|
|
294
|
+
}
|
|
295
|
+
penalty += candidate.length * 0.01;
|
|
296
|
+
return penalty;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function compareCandidatePreference(left: string, right: string): number {
|
|
300
|
+
const penaltyDiff = getCandidatePenalty(left) - getCandidatePenalty(right);
|
|
301
|
+
if (penaltyDiff !== 0) {
|
|
302
|
+
return penaltyDiff;
|
|
303
|
+
}
|
|
304
|
+
if (left.length !== right.length) {
|
|
305
|
+
return left.length - right.length;
|
|
306
|
+
}
|
|
307
|
+
return left.localeCompare(right);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function selectBestOfficialCandidate(candidates: readonly string[]): string | undefined {
|
|
311
|
+
if (candidates.length === 0) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const ranked = [...new Set(candidates)].sort(compareCandidatePreference);
|
|
315
|
+
return ranked[0];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getWrapperCanonicalCandidates(candidate: string): string[] {
|
|
319
|
+
const results = new Set<string>();
|
|
320
|
+
for (const prefix of WRAPPER_PREFIXES) {
|
|
321
|
+
if (!candidate.toLowerCase().startsWith(prefix)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const stripped = candidate.slice(prefix.length);
|
|
325
|
+
addCanonicalCandidate(results, stripped);
|
|
326
|
+
if (/^(opus|sonnet|haiku)-/i.test(stripped)) {
|
|
327
|
+
addCanonicalCandidate(results, `claude-${stripped}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return [...results];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getAnthropicAliasOfficial(candidate: string, officialIds: Set<string>): string | undefined {
|
|
334
|
+
const reordered = reorderAnthropicFamily(candidate);
|
|
335
|
+
if (!reordered) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
const candidates = [reordered, ...toggleShortVersionSeparators(reordered)].filter(officialId =>
|
|
339
|
+
officialIds.has(officialId),
|
|
340
|
+
);
|
|
341
|
+
return selectBestOfficialCandidate(candidates);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function compareVersionSegments(left: readonly number[], right: readonly number[]): number {
|
|
345
|
+
const maxLength = Math.max(left.length, right.length);
|
|
346
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
347
|
+
const diff = (left[index] ?? Number.NEGATIVE_INFINITY) - (right[index] ?? Number.NEGATIVE_INFINITY);
|
|
348
|
+
if (diff !== 0) {
|
|
349
|
+
return diff;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseClaudeFamilyVersionSegments(candidate: string, prefix: string): number[] {
|
|
356
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
357
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
358
|
+
if (!normalizedCandidate.startsWith(`${normalizedPrefix}-`)) {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
const rawSuffix = normalizedCandidate.slice(normalizedPrefix.length + 1);
|
|
362
|
+
if (!rawSuffix) {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const versionSegments: number[] = [];
|
|
366
|
+
for (const token of rawSuffix.split("-")) {
|
|
367
|
+
if (!token) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (/^\d{8}$/.test(token)) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (/^\d{2}$/.test(token)) {
|
|
374
|
+
versionSegments.push(Number(token[0]), Number(token[1]));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (/^\d+(?:\.\d+)*$/.test(token)) {
|
|
378
|
+
versionSegments.push(...token.split(".").map(part => Number(part)));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
return versionSegments;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getClaudeFamilyAliasOfficial(candidate: string, officialIds: Set<string>): string | undefined {
|
|
387
|
+
const match = /^(?:anthropic\/)?(claude(?:-\d(?:[.-]\d+)?)?-(?:haiku|opus|sonnet))(?:-latest)?$/i.exec(candidate);
|
|
388
|
+
if (!match?.[1]) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const familyPrefix = match[1].toLowerCase();
|
|
392
|
+
const familyMatches = [...officialIds].filter(officialId => {
|
|
393
|
+
const normalizedOfficialId = officialId.toLowerCase();
|
|
394
|
+
return normalizedOfficialId.startsWith(`${familyPrefix}-`) || normalizedOfficialId === familyPrefix;
|
|
395
|
+
});
|
|
396
|
+
if (familyMatches.length === 0) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
return [...familyMatches].sort((left, right) => {
|
|
400
|
+
const versionDiff = compareVersionSegments(
|
|
401
|
+
parseClaudeFamilyVersionSegments(right, familyPrefix),
|
|
402
|
+
parseClaudeFamilyVersionSegments(left, familyPrefix),
|
|
403
|
+
);
|
|
404
|
+
if (versionDiff !== 0) {
|
|
405
|
+
return versionDiff;
|
|
406
|
+
}
|
|
407
|
+
const leftHasDate = /-\d{8}(?:$|-)/i.test(left);
|
|
408
|
+
const rightHasDate = /-\d{8}(?:$|-)/i.test(right);
|
|
409
|
+
if (leftHasDate !== rightHasDate) {
|
|
410
|
+
return leftHasDate ? 1 : -1;
|
|
411
|
+
}
|
|
412
|
+
const leftHasMarker = stripTrailingMarker(left) !== undefined;
|
|
413
|
+
const rightHasMarker = stripTrailingMarker(right) !== undefined;
|
|
414
|
+
if (leftHasMarker !== rightHasMarker) {
|
|
415
|
+
return leftHasMarker ? 1 : -1;
|
|
416
|
+
}
|
|
417
|
+
return compareCandidatePreference(left, right);
|
|
418
|
+
})[0];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function toggleShortVersionSeparators(candidate: string): string[] {
|
|
422
|
+
const toggled = new Set<string>();
|
|
423
|
+
const dotToDash = candidate.replace(/(^|[-_/])(\d{1,2})\.(\d{1,2})(?=$|[-_a-z])/gi, "$1$2-$3");
|
|
424
|
+
if (dotToDash !== candidate) {
|
|
425
|
+
toggled.add(dotToDash);
|
|
426
|
+
}
|
|
427
|
+
const dashToDot = candidate.replace(/(^|[-_/])(\d{1,2})-(\d{1,2})(?=$|[-_a-z])/gi, "$1$2.$3");
|
|
428
|
+
if (dashToDot !== candidate) {
|
|
429
|
+
toggled.add(dashToDot);
|
|
430
|
+
}
|
|
431
|
+
return [...toggled];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function expandCompactMinorVersions(candidate: string): string[] {
|
|
435
|
+
const expanded = new Set<string>();
|
|
436
|
+
const compactToDash = candidate.replace(/(^|[-_/])(\d)(\d)(?=$|[-_a-z])/g, "$1$2-$3");
|
|
437
|
+
if (compactToDash !== candidate) {
|
|
438
|
+
expanded.add(compactToDash);
|
|
439
|
+
}
|
|
440
|
+
const compactToDot = candidate.replace(/(^|[-_/])(\d)(\d)(?=$|[-_a-z])/g, "$1$2.$3");
|
|
441
|
+
if (compactToDot !== candidate) {
|
|
442
|
+
expanded.add(compactToDot);
|
|
443
|
+
}
|
|
444
|
+
return [...expanded];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getHeuristicCanonicalCandidates(modelId: string): string[] {
|
|
448
|
+
const candidates = new Set<string>();
|
|
449
|
+
const queue = [modelId];
|
|
450
|
+
const visited = new Set<string>();
|
|
451
|
+
|
|
452
|
+
while (queue.length > 0) {
|
|
453
|
+
const candidate = queue.shift();
|
|
454
|
+
if (!candidate) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const normalized = candidate.trim();
|
|
458
|
+
if (!normalized || visited.has(normalized)) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
visited.add(normalized);
|
|
462
|
+
addCanonicalCandidate(candidates, normalized);
|
|
463
|
+
|
|
464
|
+
const lowercased = lowercaseCandidate(normalized);
|
|
465
|
+
if (lowercased) {
|
|
466
|
+
queue.push(lowercased);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const pathSegments = normalized.split("/");
|
|
470
|
+
for (let index = 1; index < pathSegments.length; index += 1) {
|
|
471
|
+
queue.push(pathSegments.slice(index).join("/"));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const suffix of getQualifiedNamespaceSuffixes(normalized)) {
|
|
475
|
+
queue.push(suffix);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const toggled of toggleShortVersionSeparators(normalized)) {
|
|
479
|
+
queue.push(toggled);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const attachedFamilyVersion = insertAttachedFamilyVersionSeparator(normalized);
|
|
483
|
+
if (attachedFamilyVersion) {
|
|
484
|
+
queue.push(attachedFamilyVersion);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
for (const toggledSeriesVersion of toggleSeriesMinorVersionSeparators(normalized)) {
|
|
488
|
+
queue.push(toggledSeriesVersion);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
for (const expandedVersion of expandCompactMinorVersions(normalized)) {
|
|
492
|
+
queue.push(expandedVersion);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const expandedSeriesVersion of expandCompactSeriesMinorVersions(normalized)) {
|
|
496
|
+
queue.push(expandedSeriesVersion);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for (const wrapperCandidate of getWrapperCanonicalCandidates(normalized)) {
|
|
500
|
+
queue.push(wrapperCandidate);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const strippedSyntheticPrefix = stripSyntheticPrefix(normalized);
|
|
504
|
+
if (strippedSyntheticPrefix) {
|
|
505
|
+
queue.push(strippedSyntheticPrefix);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const strippedLatest = stripLatestSuffix(normalized);
|
|
509
|
+
if (strippedLatest) {
|
|
510
|
+
queue.push(strippedLatest);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const strippedLegacyGlmTurbo = stripLegacyGlmTurboSuffix(normalized);
|
|
514
|
+
if (strippedLegacyGlmTurbo) {
|
|
515
|
+
queue.push(strippedLegacyGlmTurbo);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const extractedFamily = extractUpstreamFamilyCandidate(normalized);
|
|
519
|
+
if (extractedFamily) {
|
|
520
|
+
queue.push(extractedFamily);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const strippedProviderVersion = stripProviderVersionSuffix(normalized);
|
|
524
|
+
if (strippedProviderVersion) {
|
|
525
|
+
queue.push(strippedProviderVersion);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const strippedDate = stripDateSuffix(normalized);
|
|
529
|
+
if (strippedDate) {
|
|
530
|
+
queue.push(strippedDate);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const strippedMarker = stripTrailingMarker(normalized);
|
|
534
|
+
if (strippedMarker) {
|
|
535
|
+
queue.push(strippedMarker);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const reorderedAnthropic = reorderAnthropicFamily(normalized);
|
|
539
|
+
if (reorderedAnthropic) {
|
|
540
|
+
queue.push(reorderedAnthropic);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return [...candidates];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getPreferredFallbackCanonicalCandidate(modelId: string, candidates: readonly string[]): string | undefined {
|
|
548
|
+
if (!/[/:.]/.test(modelId)) {
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
const cleanCandidates = candidates.filter(candidate => {
|
|
552
|
+
if (!candidate || candidate === modelId) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
if (candidate.includes("/") || candidate.includes(":")) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (candidate.toLowerCase() !== candidate) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
const extractedFamily = extractUpstreamFamilyCandidate(candidate);
|
|
562
|
+
return extractedFamily?.toLowerCase() === candidate;
|
|
563
|
+
});
|
|
564
|
+
return selectBestOfficialCandidate(cleanCandidates);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveCanonicalIdForModel(
|
|
568
|
+
model: Model<Api>,
|
|
569
|
+
equivalence: CompiledEquivalenceConfig,
|
|
570
|
+
referenceData: CanonicalReferenceData,
|
|
571
|
+
): ResolvedCanonicalModel {
|
|
572
|
+
const selector = formatCanonicalVariantSelector(model);
|
|
573
|
+
const normalizedSelector = normalizeSelectorKey(selector);
|
|
574
|
+
|
|
575
|
+
if (equivalence.overrides.has(normalizedSelector)) {
|
|
576
|
+
return { id: equivalence.overrides.get(normalizedSelector)!, source: "override" };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (equivalence.exclude.has(normalizedSelector)) {
|
|
580
|
+
return { id: model.id, source: "fallback" };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const anthropicAlias = getAnthropicAliasOfficial(model.id, referenceData.officialIds);
|
|
584
|
+
if (anthropicAlias) {
|
|
585
|
+
return { id: anthropicAlias, source: anthropicAlias === model.id ? "bundled" : "heuristic" };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const claudeFamilyAlias = getClaudeFamilyAliasOfficial(model.id, referenceData.officialIds);
|
|
589
|
+
if (claudeFamilyAlias) {
|
|
590
|
+
return { id: claudeFamilyAlias, source: claudeFamilyAlias === model.id ? "bundled" : "heuristic" };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const heuristicCandidates = getHeuristicCanonicalCandidates(model.id);
|
|
594
|
+
const officialMatches = heuristicCandidates.filter(candidate => referenceData.officialIds.has(candidate));
|
|
595
|
+
const preferredFallback = getPreferredFallbackCanonicalCandidate(model.id, heuristicCandidates);
|
|
596
|
+
const match = selectBestOfficialCandidate(officialMatches);
|
|
597
|
+
if (match) {
|
|
598
|
+
if (
|
|
599
|
+
preferredFallback &&
|
|
600
|
+
(match.includes("/") || match.includes(":")) &&
|
|
601
|
+
compareCandidatePreference(preferredFallback, match) < 0
|
|
602
|
+
) {
|
|
603
|
+
return { id: preferredFallback, source: "heuristic" };
|
|
604
|
+
}
|
|
605
|
+
return { id: match, source: match === model.id ? "bundled" : "heuristic" };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (preferredFallback) {
|
|
609
|
+
return { id: preferredFallback, source: "heuristic" };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { id: model.id, source: "fallback" };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function getCanonicalRecordName(
|
|
616
|
+
record: CanonicalModelRecord | undefined,
|
|
617
|
+
canonicalId: string,
|
|
618
|
+
variant: CanonicalModelVariant,
|
|
619
|
+
referenceData: CanonicalReferenceData,
|
|
620
|
+
): string {
|
|
621
|
+
if (record) {
|
|
622
|
+
return record.name;
|
|
623
|
+
}
|
|
624
|
+
return referenceData.references.get(canonicalId)?.name ?? variant.model.name ?? canonicalId;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function compareCanonicalRecords(left: CanonicalModelRecord, right: CanonicalModelRecord): number {
|
|
628
|
+
return left.id.localeCompare(right.id);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function compareCanonicalVariants(left: CanonicalModelVariant, right: CanonicalModelVariant): number {
|
|
632
|
+
const leftSelector = left.selector;
|
|
633
|
+
const rightSelector = right.selector;
|
|
634
|
+
return leftSelector.localeCompare(rightSelector);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function buildCanonicalModelIndex(
|
|
638
|
+
models: readonly Model<Api>[],
|
|
639
|
+
equivalence?: ModelEquivalenceConfig,
|
|
640
|
+
): CanonicalModelIndex {
|
|
641
|
+
const referenceData = createCanonicalReferenceData();
|
|
642
|
+
const compiledEquivalence = compileEquivalenceConfig(equivalence);
|
|
643
|
+
const byId = new Map<string, CanonicalModelRecord>();
|
|
644
|
+
const bySelector = new Map<string, string>();
|
|
645
|
+
|
|
646
|
+
for (const model of models) {
|
|
647
|
+
const canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData);
|
|
648
|
+
const selector = formatCanonicalVariantSelector(model);
|
|
649
|
+
const variant: CanonicalModelVariant = {
|
|
650
|
+
canonicalId: canonical.id,
|
|
651
|
+
selector,
|
|
652
|
+
model,
|
|
653
|
+
source: canonical.source,
|
|
654
|
+
};
|
|
655
|
+
const canonicalKey = normalizeCanonicalIdKey(canonical.id);
|
|
656
|
+
const existing = byId.get(canonicalKey);
|
|
657
|
+
const nextRecord: CanonicalModelRecord = existing ?? {
|
|
658
|
+
id: canonical.id,
|
|
659
|
+
name: getCanonicalRecordName(existing, canonical.id, variant, referenceData),
|
|
660
|
+
variants: [],
|
|
661
|
+
};
|
|
662
|
+
nextRecord.name = getCanonicalRecordName(existing, canonical.id, variant, referenceData);
|
|
663
|
+
nextRecord.variants.push(variant);
|
|
664
|
+
byId.set(canonicalKey, nextRecord);
|
|
665
|
+
bySelector.set(normalizeSelectorKey(selector), canonical.id);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const records = [...byId.values()].sort(compareCanonicalRecords);
|
|
669
|
+
for (const record of records) {
|
|
670
|
+
record.variants.sort(compareCanonicalVariants);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { records, byId, bySelector };
|
|
674
|
+
}
|