@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.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.
Files changed (155) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/types/async/job-manager.d.ts +3 -2
  3. package/dist/types/cli/auth-broker-cli.d.ts +25 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
  5. package/dist/types/cli/grievances-cli.d.ts +12 -0
  6. package/dist/types/commands/auth-broker.d.ts +54 -0
  7. package/dist/types/commands/auth-gateway.d.ts +32 -0
  8. package/dist/types/commands/grievances.d.ts +1 -1
  9. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
  10. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
  11. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
  12. package/dist/types/config/model-registry.d.ts +3 -0
  13. package/dist/types/config/models-config-schema.d.ts +1 -0
  14. package/dist/types/config/settings-schema.d.ts +46 -0
  15. package/dist/types/discovery/agents.d.ts +12 -1
  16. package/dist/types/edit/renderer.d.ts +3 -0
  17. package/dist/types/eval/index.d.ts +0 -2
  18. package/dist/types/goals/tools/goal-tool.d.ts +10 -2
  19. package/dist/types/index.d.ts +0 -1
  20. package/dist/types/internal-urls/index.d.ts +1 -1
  21. package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
  22. package/dist/types/internal-urls/types.d.ts +1 -1
  23. package/dist/types/main.d.ts +11 -2
  24. package/dist/types/modes/acp/acp-agent.d.ts +2 -1
  25. package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
  26. package/dist/types/modes/acp/acp-mode.d.ts +3 -1
  27. package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
  28. package/dist/types/modes/interactive-mode.d.ts +1 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  30. package/dist/types/plan-mode/approved-plan.d.ts +10 -4
  31. package/dist/types/sdk.d.ts +10 -3
  32. package/dist/types/session/agent-session.d.ts +7 -3
  33. package/dist/types/session/auth-broker-config.d.ts +13 -0
  34. package/dist/types/session/auth-storage.d.ts +1 -1
  35. package/dist/types/session/client-bridge.d.ts +3 -0
  36. package/dist/types/tools/eval.d.ts +41 -7
  37. package/dist/types/tools/irc.d.ts +8 -2
  38. package/dist/types/tools/report-tool-issue.d.ts +118 -1
  39. package/dist/types/tools/resolve.d.ts +8 -2
  40. package/examples/custom-tools/README.md +3 -12
  41. package/examples/extensions/README.md +2 -15
  42. package/examples/extensions/api-demo.ts +1 -7
  43. package/package.json +7 -7
  44. package/src/async/job-manager.ts +111 -13
  45. package/src/autoresearch/tools/init-experiment.ts +11 -33
  46. package/src/autoresearch/tools/log-experiment.ts +10 -24
  47. package/src/autoresearch/tools/run-experiment.ts +1 -1
  48. package/src/autoresearch/tools/update-notes.ts +2 -9
  49. package/src/cli/auth-broker-cli.ts +746 -0
  50. package/src/cli/auth-gateway-cli.ts +342 -0
  51. package/src/cli/grievances-cli.ts +109 -16
  52. package/src/cli/update-cli.ts +1 -5
  53. package/src/cli.ts +4 -2
  54. package/src/commands/auth-broker.ts +96 -0
  55. package/src/commands/auth-gateway.ts +61 -0
  56. package/src/commands/grievances.ts +13 -8
  57. package/src/commands/launch.ts +1 -1
  58. package/src/commit/agentic/agent.ts +2 -0
  59. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  60. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  61. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  62. package/src/commit/agentic/tools/git-overview.ts +2 -2
  63. package/src/commit/agentic/tools/propose-changelog.ts +1 -3
  64. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  65. package/src/commit/agentic/tools/schemas.ts +1 -9
  66. package/src/config/model-equivalence.ts +279 -174
  67. package/src/config/model-registry.ts +37 -6
  68. package/src/config/model-resolver.ts +13 -8
  69. package/src/config/models-config-schema.ts +8 -0
  70. package/src/config/settings-schema.ts +52 -0
  71. package/src/cursor.ts +1 -1
  72. package/src/debug/log-formatting.ts +1 -1
  73. package/src/debug/log-viewer.ts +1 -1
  74. package/src/debug/profiler.ts +4 -0
  75. package/src/debug/raw-sse-buffer.ts +100 -59
  76. package/src/debug/raw-sse.ts +1 -1
  77. package/src/discovery/agents.ts +15 -4
  78. package/src/edit/modes/apply-patch.ts +1 -5
  79. package/src/edit/modes/patch.ts +5 -5
  80. package/src/edit/modes/replace.ts +5 -5
  81. package/src/edit/renderer.ts +2 -1
  82. package/src/edit/streaming.ts +1 -1
  83. package/src/eval/index.ts +0 -2
  84. package/src/eval/js/shared/runtime.ts +107 -2
  85. package/src/eval/py/kernel.ts +1 -1
  86. package/src/exa/researcher.ts +4 -4
  87. package/src/exa/search.ts +10 -22
  88. package/src/exa/websets.ts +33 -33
  89. package/src/extensibility/typebox.ts +44 -17
  90. package/src/goals/tools/goal-tool.ts +3 -3
  91. package/src/index.ts +0 -3
  92. package/src/internal-urls/docs-index.generated.ts +21 -18
  93. package/src/internal-urls/index.ts +1 -1
  94. package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
  95. package/src/internal-urls/router.ts +3 -3
  96. package/src/internal-urls/types.ts +1 -1
  97. package/src/lsp/types.ts +8 -11
  98. package/src/main.ts +216 -146
  99. package/src/mcp/tool-bridge.ts +3 -3
  100. package/src/modes/acp/acp-agent.ts +203 -57
  101. package/src/modes/acp/acp-client-bridge.ts +2 -1
  102. package/src/modes/acp/acp-event-mapper.ts +208 -32
  103. package/src/modes/acp/acp-mode.ts +11 -3
  104. package/src/modes/components/bash-execution.ts +1 -1
  105. package/src/modes/components/diff.ts +1 -2
  106. package/src/modes/components/eval-execution.ts +1 -1
  107. package/src/modes/components/oauth-selector.ts +38 -2
  108. package/src/modes/components/tool-execution.ts +1 -2
  109. package/src/modes/components/tree-selector.ts +26 -7
  110. package/src/modes/controllers/command-controller.ts +95 -34
  111. package/src/modes/controllers/input-controller.ts +4 -3
  112. package/src/modes/data/emojis.json +1 -0
  113. package/src/modes/emoji-autocomplete.ts +285 -0
  114. package/src/modes/interactive-mode.ts +92 -19
  115. package/src/modes/print-mode.ts +3 -3
  116. package/src/modes/prompt-action-autocomplete.ts +14 -0
  117. package/src/plan-mode/approved-plan.ts +30 -9
  118. package/src/prompts/system/system-prompt.md +1 -1
  119. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  120. package/src/prompts/tools/ask.md +4 -3
  121. package/src/prompts/tools/eval.md +25 -26
  122. package/src/prompts/tools/read.md +1 -1
  123. package/src/prompts/tools/resolve.md +1 -1
  124. package/src/prompts/tools/search.md +1 -1
  125. package/src/prompts/tools/web-search.md +1 -1
  126. package/src/sdk.ts +81 -8
  127. package/src/session/agent-session.ts +362 -131
  128. package/src/session/agent-storage.ts +7 -2
  129. package/src/session/auth-broker-config.ts +102 -0
  130. package/src/session/auth-storage.ts +7 -1
  131. package/src/session/client-bridge.ts +3 -0
  132. package/src/session/streaming-output.ts +1 -1
  133. package/src/task/types.ts +10 -35
  134. package/src/tools/bash-interactive.ts +4 -1
  135. package/src/tools/bash-pty-selection.ts +2 -2
  136. package/src/tools/browser.ts +12 -20
  137. package/src/tools/eval.ts +77 -100
  138. package/src/tools/gh.ts +21 -45
  139. package/src/tools/hindsight-recall.ts +1 -1
  140. package/src/tools/hindsight-reflect.ts +2 -2
  141. package/src/tools/hindsight-retain.ts +3 -7
  142. package/src/tools/index.ts +8 -1
  143. package/src/tools/inspect-image.ts +4 -1
  144. package/src/tools/irc.ts +4 -12
  145. package/src/tools/job.ts +3 -11
  146. package/src/tools/report-tool-issue.ts +462 -17
  147. package/src/tools/resolve.ts +2 -7
  148. package/src/tools/todo-write.ts +8 -15
  149. package/src/utils/title-generator.ts +3 -0
  150. package/src/web/search/index.ts +6 -6
  151. package/dist/types/eval/parse.d.ts +0 -28
  152. package/dist/types/eval/sniff.d.ts +0 -11
  153. package/src/eval/eval.lark +0 -36
  154. package/src/eval/parse.ts +0 -407
  155. package/src/eval/sniff.ts +0 -28
@@ -41,35 +41,8 @@ interface ResolvedCanonicalModel {
41
41
  source: CanonicalModelSource;
42
42
  }
43
43
 
44
- const TRAILING_CANONICAL_MARKERS = [
45
- "thinking",
46
- "customtools",
47
- "high",
48
- "low",
49
- "medium",
50
- "minimal",
51
- "xhigh",
52
- "free",
53
- "cloud",
54
- "exacto",
55
- "nitro",
56
- "original",
57
- "optimized",
58
- "nvfp4",
59
- "fp8",
60
- "fp4",
61
- "bf16",
62
- "int8",
63
- "int4",
64
- ] as const;
65
- const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => {
66
- const suffixes: string[] = [];
67
- for (const marker of TRAILING_CANONICAL_MARKERS) {
68
- const lower = marker.toLowerCase();
69
- suffixes.push(`-${lower}`, `:${lower}`);
70
- }
71
- return suffixes;
72
- })();
44
+ const TRAILING_MARKER_PATTERN =
45
+ /[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4)$/i;
73
46
  const WRAPPER_PREFIXES = ["duo-chat-"] as const;
74
47
 
75
48
  let referenceDataCache: CanonicalReferenceData | undefined;
@@ -77,7 +50,10 @@ const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = {
77
50
  overrides: new Map<string, string>(),
78
51
  exclude: new Set<string>(),
79
52
  };
80
- const resolutionCache: WeakMap<CompiledEquivalenceConfig, WeakMap<Model<Api>, ResolvedCanonicalModel>> = new WeakMap();
53
+ const kModelResolutionCache = Symbol("model-equivalence.resolutionCache");
54
+ interface CompiledEquivalenceConfigWithCache extends CompiledEquivalenceConfig {
55
+ [kModelResolutionCache]?: WeakMap<Model<Api>, ResolvedCanonicalModel>;
56
+ }
81
57
  const FAMILY_EXTRACTION_PATTERNS = [
82
58
  /(?:^|[/:._-])((?: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,
83
59
  /(?:^|[/:._-])((?: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,
@@ -172,13 +148,12 @@ function addCanonicalCandidate(candidates: Set<string>, candidate: string): void
172
148
  }
173
149
 
174
150
  function stripTrailingMarker(candidate: string): string | undefined {
175
- const lower = candidate.toLowerCase();
176
- for (const suffix of TRAILING_MARKER_SUFFIXES) {
177
- if (lower.endsWith(suffix)) {
178
- return candidate.slice(0, -suffix.length);
179
- }
180
- }
181
- return undefined;
151
+ const match = TRAILING_MARKER_PATTERN.exec(candidate);
152
+ return match ? candidate.slice(0, match.index) : undefined;
153
+ }
154
+
155
+ function hasTrailingMarker(candidate: string): boolean {
156
+ return TRAILING_MARKER_PATTERN.test(candidate);
182
157
  }
183
158
 
184
159
  function lowercaseCandidate(candidate: string): string | undefined {
@@ -186,23 +161,41 @@ function lowercaseCandidate(candidate: string): string | undefined {
186
161
  return lowercased !== candidate ? lowercased : undefined;
187
162
  }
188
163
 
164
+ const STRIP_SYNTHETIC_PREFIX_PATTERN = /^hf:/i;
165
+ const STRIP_LATEST_SUFFIX_PATTERN = /-latest$/i;
166
+ const STRIP_LEGACY_GLM_TURBO_PATTERN = /^(glm-4(?:\.\d+)?v?)-turbo$/i;
167
+ const REORDER_ANTHROPIC_FAMILY_PATTERN = /^claude-(\d+(?:[.-]\d+)+)-(opus|sonnet|haiku)$/i;
168
+ const STRIP_PROVIDER_VERSION_SUFFIX_PATTERN = /-v\d+(?::\d+)?$/i;
169
+ const STRIP_DATE_SUFFIX_PATTERN = /-\d{8}$/i;
170
+ const INSERT_ATTACHED_FAMILY_VERSION_SEPARATOR_PATTERN =
171
+ /(^|[/:._-])((?: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;
172
+ const SERIES_MINOR_DOT_TO_DASH_PATTERN = /(^|[/:._-])([a-z])(\d)\.(\d)(?=$|[-_/.:a-z])/gi;
173
+ const SERIES_MINOR_DASH_TO_DOT_PATTERN = /(^|[/:._-])([a-z])(\d)-(\d)(?=$|[-_/.:a-z])/gi;
174
+ const EXPAND_COMPACT_SERIES_MINOR_PATTERN = /(^|[/:._-])([a-z])(\d)(\d)(?=$|[-_/.:a-z])/gi;
175
+ const NAMESPACE_SUFFIX_BOUNDARY_PATTERN = /[/:.]/;
176
+ const NAMESPACE_SUFFIX_ALPHA_PATTERN = /[a-z]/i;
177
+ const NAMESPACE_SUFFIX_DIGIT_PATTERN = /\d/;
178
+ const SHORT_VERSION_DOT_TO_DASH_PATTERN = /(^|[-_/])(\d{1,2})\.(\d{1,2})(?=$|[-_a-z])/gi;
179
+ const SHORT_VERSION_DASH_TO_DOT_PATTERN = /(^|[-_/])(\d{1,2})-(\d{1,2})(?=$|[-_a-z])/gi;
180
+ const EXPAND_COMPACT_MINOR_PATTERN = /(^|[-_/])(\d)(\d)(?=$|[-_a-z])/g;
181
+
189
182
  function stripSyntheticPrefix(candidate: string): string | undefined {
190
- const stripped = candidate.replace(/^hf:/i, "");
183
+ const stripped = candidate.replace(STRIP_SYNTHETIC_PREFIX_PATTERN, "");
191
184
  return stripped !== candidate ? stripped : undefined;
192
185
  }
193
186
 
194
187
  function stripLatestSuffix(candidate: string): string | undefined {
195
- const stripped = candidate.replace(/-latest$/i, "");
188
+ const stripped = candidate.replace(STRIP_LATEST_SUFFIX_PATTERN, "");
196
189
  return stripped !== candidate ? stripped : undefined;
197
190
  }
198
191
 
199
192
  function stripLegacyGlmTurboSuffix(candidate: string): string | undefined {
200
- const stripped = candidate.replace(/^(glm-4(?:\.\d+)?v?)-turbo$/i, "$1");
193
+ const stripped = candidate.replace(STRIP_LEGACY_GLM_TURBO_PATTERN, "$1");
201
194
  return stripped !== candidate ? stripped : undefined;
202
195
  }
203
196
 
204
197
  function reorderAnthropicFamily(candidate: string): string | undefined {
205
- const match = /^claude-(\d+(?:[.-]\d+)+)-(opus|sonnet|haiku)$/i.exec(candidate);
198
+ const match = REORDER_ANTHROPIC_FAMILY_PATTERN.exec(candidate);
206
199
  if (!match) {
207
200
  return undefined;
208
201
  }
@@ -211,30 +204,27 @@ function reorderAnthropicFamily(candidate: string): string | undefined {
211
204
  }
212
205
 
213
206
  function stripProviderVersionSuffix(candidate: string): string | undefined {
214
- const stripped = candidate.replace(/-v\d+(?::\d+)?$/i, "");
207
+ const stripped = candidate.replace(STRIP_PROVIDER_VERSION_SUFFIX_PATTERN, "");
215
208
  return stripped !== candidate ? stripped : undefined;
216
209
  }
217
210
 
218
211
  function stripDateSuffix(candidate: string): string | undefined {
219
- const stripped = candidate.replace(/-\d{8}$/i, "");
212
+ const stripped = candidate.replace(STRIP_DATE_SUFFIX_PATTERN, "");
220
213
  return stripped !== candidate ? stripped : undefined;
221
214
  }
222
215
 
223
216
  function insertAttachedFamilyVersionSeparator(candidate: string): string | undefined {
224
- const inserted = candidate.replace(
225
- /(^|[/:._-])((?: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,
226
- "$1$2-$3",
227
- );
217
+ const inserted = candidate.replace(INSERT_ATTACHED_FAMILY_VERSION_SEPARATOR_PATTERN, "$1$2-$3");
228
218
  return inserted !== candidate ? inserted : undefined;
229
219
  }
230
220
 
231
221
  function toggleSeriesMinorVersionSeparators(candidate: string): string[] {
232
222
  const toggled = new Set<string>();
233
- const dotToDash = candidate.replace(/(^|[/:._-])([a-z])(\d)\.(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3-$4");
223
+ const dotToDash = candidate.replace(SERIES_MINOR_DOT_TO_DASH_PATTERN, "$1$2$3-$4");
234
224
  if (dotToDash !== candidate) {
235
225
  toggled.add(dotToDash);
236
226
  }
237
- const dashToDot = candidate.replace(/(^|[/:._-])([a-z])(\d)-(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3.$4");
227
+ const dashToDot = candidate.replace(SERIES_MINOR_DASH_TO_DOT_PATTERN, "$1$2$3.$4");
238
228
  if (dashToDot !== candidate) {
239
229
  toggled.add(dashToDot);
240
230
  }
@@ -243,33 +233,51 @@ function toggleSeriesMinorVersionSeparators(candidate: string): string[] {
243
233
 
244
234
  function expandCompactSeriesMinorVersions(candidate: string): string[] {
245
235
  const expanded = new Set<string>();
246
- const compactToDash = candidate.replace(/(^|[/:._-])([a-z])(\d)(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3-$4");
236
+ const compactToDash = candidate.replace(EXPAND_COMPACT_SERIES_MINOR_PATTERN, "$1$2$3-$4");
247
237
  if (compactToDash !== candidate) {
248
238
  expanded.add(compactToDash);
249
239
  }
250
- const compactToDot = candidate.replace(/(^|[/:._-])([a-z])(\d)(\d)(?=$|[-_/.:a-z])/gi, "$1$2$3.$4");
240
+ const compactToDot = candidate.replace(EXPAND_COMPACT_SERIES_MINOR_PATTERN, "$1$2$3.$4");
251
241
  if (compactToDot !== candidate) {
252
242
  expanded.add(compactToDot);
253
243
  }
254
244
  return [...expanded];
255
245
  }
256
246
 
247
+ // Bounded FIFO memo: pure function of `candidate`. Cached arrays are read-only at
248
+ // every callsite (they are iterated to push into a queue — never mutated), so we
249
+ // safely return the same instance. Cap keeps memory bounded under adversarial
250
+ // model-id churn.
251
+ const QUALIFIED_NAMESPACE_SUFFIX_CACHE = new Map<string, string[]>();
252
+ const QUALIFIED_NAMESPACE_SUFFIX_CACHE_CAP = 256;
257
253
  function getQualifiedNamespaceSuffixes(candidate: string): string[] {
254
+ const cached = QUALIFIED_NAMESPACE_SUFFIX_CACHE.get(candidate);
255
+ if (cached !== undefined) {
256
+ return cached;
257
+ }
258
258
  const results = new Set<string>();
259
259
  for (let index = 1; index < candidate.length; index += 1) {
260
- if (!/[/:.]/.test(candidate[index - 1]!)) {
260
+ if (!NAMESPACE_SUFFIX_BOUNDARY_PATTERN.test(candidate[index - 1]!)) {
261
261
  continue;
262
262
  }
263
263
  const suffix = candidate.slice(index);
264
264
  if (suffix.length < 4) {
265
265
  continue;
266
266
  }
267
- if (!/[a-z]/i.test(suffix) || !/\d/.test(suffix)) {
267
+ if (!NAMESPACE_SUFFIX_ALPHA_PATTERN.test(suffix) || !NAMESPACE_SUFFIX_DIGIT_PATTERN.test(suffix)) {
268
268
  continue;
269
269
  }
270
270
  addCanonicalCandidate(results, suffix);
271
271
  }
272
- return [...results];
272
+ const output = [...results];
273
+ if (QUALIFIED_NAMESPACE_SUFFIX_CACHE.size >= QUALIFIED_NAMESPACE_SUFFIX_CACHE_CAP) {
274
+ const oldest = QUALIFIED_NAMESPACE_SUFFIX_CACHE.keys().next().value;
275
+ if (oldest !== undefined) {
276
+ QUALIFIED_NAMESPACE_SUFFIX_CACHE.delete(oldest);
277
+ }
278
+ }
279
+ QUALIFIED_NAMESPACE_SUFFIX_CACHE.set(candidate, output);
280
+ return output;
273
281
  }
274
282
 
275
283
  function extractUpstreamFamilyCandidate(candidate: string): string | undefined {
@@ -282,6 +290,15 @@ function extractUpstreamFamilyCandidate(candidate: string): string | undefined {
282
290
  return undefined;
283
291
  }
284
292
 
293
+ const PENALTY_DATE_SUFFIX = /-\d{8}$/i;
294
+ const PENALTY_PROVIDER_VERSION_SUFFIX = /-v\d+(?::\d+)?$/i;
295
+ const PENALTY_HAS_UPPERCASE = /[A-Z]/;
296
+ const PENALTY_CLAUDE_LEADING_VERSION = /^claude-\d/i;
297
+ const PENALTY_CLAUDE_LEGACY_DATE = /^claude-(?:opus|sonnet|haiku)-\d{2}(?=$|[-_a-z])/i;
298
+ const PENALTY_LETTER_DIGIT_DIGIT = /(?:^|[/:._-])[a-z]\d-\d(?=$|[-_/.:a-z])/i;
299
+ const PENALTY_DIGIT_DIGIT = /(?:^|[-_/])\d-\d(?=$|[-_a-z])/;
300
+ const PENALTY_CLAUDE_FAMILY_DIGIT_DIGIT = /^claude-(?:opus|sonnet|haiku)-\d-\d/i;
301
+
285
302
  function getCandidatePenalty(candidate: string): number {
286
303
  let penalty = 0;
287
304
  if (candidate.includes("/")) {
@@ -290,28 +307,28 @@ function getCandidatePenalty(candidate: string): number {
290
307
  if (candidate.includes(":")) {
291
308
  penalty += 40;
292
309
  }
293
- if (/-\d{8}$/i.test(candidate)) {
310
+ if (PENALTY_DATE_SUFFIX.test(candidate)) {
294
311
  penalty += 25;
295
312
  }
296
- if (/-v\d+(?::\d+)?$/i.test(candidate)) {
313
+ if (PENALTY_PROVIDER_VERSION_SUFFIX.test(candidate)) {
297
314
  penalty += 25;
298
315
  }
299
- if (stripTrailingMarker(candidate)) {
316
+ if (hasTrailingMarker(candidate)) {
300
317
  penalty += 20;
301
318
  }
302
- if (/[A-Z]/.test(candidate)) {
319
+ if (PENALTY_HAS_UPPERCASE.test(candidate)) {
303
320
  penalty += 10;
304
321
  }
305
- if (/^claude-\d/i.test(candidate)) {
322
+ if (PENALTY_CLAUDE_LEADING_VERSION.test(candidate)) {
306
323
  penalty += 20;
307
324
  }
308
- if (/^claude-(?:opus|sonnet|haiku)-\d{2}(?=$|[-_a-z])/i.test(candidate)) {
325
+ if (PENALTY_CLAUDE_LEGACY_DATE.test(candidate)) {
309
326
  penalty += 10;
310
327
  }
311
- if (/(?:^|[/:._-])[a-z]\d-\d(?=$|[-_/.:a-z])/i.test(candidate)) {
328
+ if (PENALTY_LETTER_DIGIT_DIGIT.test(candidate)) {
312
329
  penalty += 6;
313
330
  }
314
- if (/(?:^|[-_/])\d-\d(?=$|[-_a-z])/.test(candidate) && !/^claude-(?:opus|sonnet|haiku)-\d-\d/i.test(candidate)) {
331
+ if (PENALTY_DIGIT_DIGIT.test(candidate) && !PENALTY_CLAUDE_FAMILY_DIGIT_DIGIT.test(candidate)) {
315
332
  penalty += 4;
316
333
  }
317
334
  penalty += candidate.length * 0.01;
@@ -333,8 +350,40 @@ function selectBestOfficialCandidate(candidates: readonly string[]): string | un
333
350
  if (candidates.length === 0) {
334
351
  return undefined;
335
352
  }
336
- const ranked = [...new Set(candidates)].sort(compareCandidatePreference);
337
- return ranked[0];
353
+ let bestCandidate: string | undefined;
354
+ let bestPenalty = 0;
355
+ let bestLength = 0;
356
+ for (const candidate of candidates) {
357
+ const penalty = getCandidatePenalty(candidate);
358
+ const length = candidate.length;
359
+ if (bestCandidate === undefined) {
360
+ bestCandidate = candidate;
361
+ bestPenalty = penalty;
362
+ bestLength = length;
363
+ continue;
364
+ }
365
+ if (penalty < bestPenalty) {
366
+ bestCandidate = candidate;
367
+ bestPenalty = penalty;
368
+ bestLength = length;
369
+ continue;
370
+ }
371
+ if (penalty > bestPenalty) {
372
+ continue;
373
+ }
374
+ if (length < bestLength) {
375
+ bestCandidate = candidate;
376
+ bestLength = length;
377
+ continue;
378
+ }
379
+ if (length > bestLength) {
380
+ continue;
381
+ }
382
+ if (candidate.localeCompare(bestCandidate) < 0) {
383
+ bestCandidate = candidate;
384
+ }
385
+ }
386
+ return bestCandidate;
338
387
  }
339
388
 
340
389
  function getWrapperCanonicalCandidates(candidate: string): string[] {
@@ -405,48 +454,81 @@ function parseClaudeFamilyVersionSegments(candidate: string, prefix: string): nu
405
454
  return versionSegments;
406
455
  }
407
456
 
457
+ const CLAUDE_FAMILY_ALIAS_PATTERN = /^(?:anthropic\/)?(claude(?:-\d(?:[.-]\d+)?)?-(?:haiku|opus|sonnet))(?:-latest)?$/i;
458
+ const CLAUDE_DATE_SUFFIX_PATTERN = /-\d{8}(?:$|-)/i;
459
+
408
460
  function getClaudeFamilyAliasOfficial(candidate: string, officialIds: Set<string>): string | undefined {
409
- const match = /^(?:anthropic\/)?(claude(?:-\d(?:[.-]\d+)?)?-(?:haiku|opus|sonnet))(?:-latest)?$/i.exec(candidate);
461
+ const match = CLAUDE_FAMILY_ALIAS_PATTERN.exec(candidate);
410
462
  if (!match?.[1]) {
411
463
  return undefined;
412
464
  }
413
465
  const familyPrefix = match[1].toLowerCase();
414
- const familyMatches = [...officialIds].filter(officialId => {
415
- const normalizedOfficialId = officialId.toLowerCase();
416
- return normalizedOfficialId.startsWith(`${familyPrefix}-`) || normalizedOfficialId === familyPrefix;
417
- });
418
- if (familyMatches.length === 0) {
419
- return undefined;
420
- }
421
- return [...familyMatches].sort((left, right) => {
422
- const versionDiff = compareVersionSegments(
423
- parseClaudeFamilyVersionSegments(right, familyPrefix),
424
- parseClaudeFamilyVersionSegments(left, familyPrefix),
425
- );
466
+ const familyPrefixWithDash = `${familyPrefix}-`;
467
+
468
+ let best: string | undefined;
469
+ let bestVersion: number[] = [];
470
+ let bestHasDate = false;
471
+ let bestHasMarker = false;
472
+
473
+ for (const officialId of officialIds) {
474
+ const normalized = officialId.toLowerCase();
475
+ if (normalized !== familyPrefix && !normalized.startsWith(familyPrefixWithDash)) {
476
+ continue;
477
+ }
478
+ const version = parseClaudeFamilyVersionSegments(officialId, familyPrefix);
479
+ const hasDate = CLAUDE_DATE_SUFFIX_PATTERN.test(officialId);
480
+ const hasMarker = hasTrailingMarker(officialId);
481
+
482
+ if (best === undefined) {
483
+ best = officialId;
484
+ bestVersion = version;
485
+ bestHasDate = hasDate;
486
+ bestHasMarker = hasMarker;
487
+ continue;
488
+ }
489
+
490
+ const versionDiff = compareVersionSegments(version, bestVersion);
426
491
  if (versionDiff !== 0) {
427
- return versionDiff;
492
+ if (versionDiff > 0) {
493
+ best = officialId;
494
+ bestVersion = version;
495
+ bestHasDate = hasDate;
496
+ bestHasMarker = hasMarker;
497
+ }
498
+ continue;
428
499
  }
429
- const leftHasDate = /-\d{8}(?:$|-)/i.test(left);
430
- const rightHasDate = /-\d{8}(?:$|-)/i.test(right);
431
- if (leftHasDate !== rightHasDate) {
432
- return leftHasDate ? 1 : -1;
500
+ if (hasDate !== bestHasDate) {
501
+ if (!hasDate) {
502
+ best = officialId;
503
+ bestVersion = version;
504
+ bestHasDate = hasDate;
505
+ bestHasMarker = hasMarker;
506
+ }
507
+ continue;
433
508
  }
434
- const leftHasMarker = stripTrailingMarker(left) !== undefined;
435
- const rightHasMarker = stripTrailingMarker(right) !== undefined;
436
- if (leftHasMarker !== rightHasMarker) {
437
- return leftHasMarker ? 1 : -1;
509
+ if (hasMarker !== bestHasMarker) {
510
+ if (!hasMarker) {
511
+ best = officialId;
512
+ bestVersion = version;
513
+ bestHasMarker = hasMarker;
514
+ }
515
+ continue;
438
516
  }
439
- return compareCandidatePreference(left, right);
440
- })[0];
517
+ if (compareCandidatePreference(officialId, best) < 0) {
518
+ best = officialId;
519
+ bestVersion = version;
520
+ }
521
+ }
522
+ return best;
441
523
  }
442
524
 
443
525
  function toggleShortVersionSeparators(candidate: string): string[] {
444
526
  const toggled = new Set<string>();
445
- const dotToDash = candidate.replace(/(^|[-_/])(\d{1,2})\.(\d{1,2})(?=$|[-_a-z])/gi, "$1$2-$3");
527
+ const dotToDash = candidate.replace(SHORT_VERSION_DOT_TO_DASH_PATTERN, "$1$2-$3");
446
528
  if (dotToDash !== candidate) {
447
529
  toggled.add(dotToDash);
448
530
  }
449
- const dashToDot = candidate.replace(/(^|[-_/])(\d{1,2})-(\d{1,2})(?=$|[-_a-z])/gi, "$1$2.$3");
531
+ const dashToDot = candidate.replace(SHORT_VERSION_DASH_TO_DOT_PATTERN, "$1$2.$3");
450
532
  if (dashToDot !== candidate) {
451
533
  toggled.add(dashToDot);
452
534
  }
@@ -455,115 +537,138 @@ function toggleShortVersionSeparators(candidate: string): string[] {
455
537
 
456
538
  function expandCompactMinorVersions(candidate: string): string[] {
457
539
  const expanded = new Set<string>();
458
- const compactToDash = candidate.replace(/(^|[-_/])(\d)(\d)(?=$|[-_a-z])/g, "$1$2-$3");
540
+ const compactToDash = candidate.replace(EXPAND_COMPACT_MINOR_PATTERN, "$1$2-$3");
459
541
  if (compactToDash !== candidate) {
460
542
  expanded.add(compactToDash);
461
543
  }
462
- const compactToDot = candidate.replace(/(^|[-_/])(\d)(\d)(?=$|[-_a-z])/g, "$1$2.$3");
544
+ const compactToDot = candidate.replace(EXPAND_COMPACT_MINOR_PATTERN, "$1$2.$3");
463
545
  if (compactToDot !== candidate) {
464
546
  expanded.add(compactToDot);
465
547
  }
466
548
  return [...expanded];
467
549
  }
468
550
 
469
- function getHeuristicCanonicalCandidates(modelId: string): string[] {
470
- const candidates = new Set<string>();
471
- const queue = [modelId];
472
- const visited = new Set<string>();
551
+ function expandCheapCanonicalCandidates(normalized: string, queue: string[]): void {
552
+ const lowercased = lowercaseCandidate(normalized);
553
+ if (lowercased) {
554
+ queue.push(lowercased);
555
+ }
473
556
 
474
- for (let qi = 0; qi < queue.length; qi += 1) {
475
- const candidate = queue[qi];
476
- if (!candidate) {
477
- continue;
478
- }
479
- const normalized = candidate.trim();
480
- if (!normalized || visited.has(normalized)) {
481
- continue;
482
- }
483
- visited.add(normalized);
484
- addCanonicalCandidate(candidates, normalized);
557
+ const pathSegments = normalized.split("/");
558
+ for (let index = 1; index < pathSegments.length; index += 1) {
559
+ queue.push(pathSegments.slice(index).join("/"));
560
+ }
485
561
 
486
- const lowercased = lowercaseCandidate(normalized);
487
- if (lowercased) {
488
- queue.push(lowercased);
489
- }
562
+ for (const suffix of getQualifiedNamespaceSuffixes(normalized)) {
563
+ queue.push(suffix);
564
+ }
565
+ }
490
566
 
491
- const pathSegments = normalized.split("/");
492
- for (let index = 1; index < pathSegments.length; index += 1) {
493
- queue.push(pathSegments.slice(index).join("/"));
494
- }
567
+ function expandHeavyCanonicalCandidates(normalized: string, queue: string[]): void {
568
+ for (const toggled of toggleShortVersionSeparators(normalized)) {
569
+ queue.push(toggled);
570
+ }
495
571
 
496
- for (const suffix of getQualifiedNamespaceSuffixes(normalized)) {
497
- queue.push(suffix);
498
- }
572
+ const attachedFamilyVersion = insertAttachedFamilyVersionSeparator(normalized);
573
+ if (attachedFamilyVersion) {
574
+ queue.push(attachedFamilyVersion);
575
+ }
499
576
 
500
- for (const toggled of toggleShortVersionSeparators(normalized)) {
501
- queue.push(toggled);
502
- }
577
+ for (const toggledSeriesVersion of toggleSeriesMinorVersionSeparators(normalized)) {
578
+ queue.push(toggledSeriesVersion);
579
+ }
503
580
 
504
- const attachedFamilyVersion = insertAttachedFamilyVersionSeparator(normalized);
505
- if (attachedFamilyVersion) {
506
- queue.push(attachedFamilyVersion);
507
- }
581
+ for (const expandedVersion of expandCompactMinorVersions(normalized)) {
582
+ queue.push(expandedVersion);
583
+ }
508
584
 
509
- for (const toggledSeriesVersion of toggleSeriesMinorVersionSeparators(normalized)) {
510
- queue.push(toggledSeriesVersion);
511
- }
585
+ for (const expandedSeriesVersion of expandCompactSeriesMinorVersions(normalized)) {
586
+ queue.push(expandedSeriesVersion);
587
+ }
512
588
 
513
- for (const expandedVersion of expandCompactMinorVersions(normalized)) {
514
- queue.push(expandedVersion);
515
- }
589
+ for (const wrapperCandidate of getWrapperCanonicalCandidates(normalized)) {
590
+ queue.push(wrapperCandidate);
591
+ }
516
592
 
517
- for (const expandedSeriesVersion of expandCompactSeriesMinorVersions(normalized)) {
518
- queue.push(expandedSeriesVersion);
519
- }
593
+ const strippedSyntheticPrefix = stripSyntheticPrefix(normalized);
594
+ if (strippedSyntheticPrefix) {
595
+ queue.push(strippedSyntheticPrefix);
596
+ }
520
597
 
521
- for (const wrapperCandidate of getWrapperCanonicalCandidates(normalized)) {
522
- queue.push(wrapperCandidate);
523
- }
598
+ const strippedLatest = stripLatestSuffix(normalized);
599
+ if (strippedLatest) {
600
+ queue.push(strippedLatest);
601
+ }
524
602
 
525
- const strippedSyntheticPrefix = stripSyntheticPrefix(normalized);
526
- if (strippedSyntheticPrefix) {
527
- queue.push(strippedSyntheticPrefix);
528
- }
603
+ const strippedLegacyGlmTurbo = stripLegacyGlmTurboSuffix(normalized);
604
+ if (strippedLegacyGlmTurbo) {
605
+ queue.push(strippedLegacyGlmTurbo);
606
+ }
529
607
 
530
- const strippedLatest = stripLatestSuffix(normalized);
531
- if (strippedLatest) {
532
- queue.push(strippedLatest);
533
- }
608
+ const extractedFamily = extractUpstreamFamilyCandidate(normalized);
609
+ if (extractedFamily) {
610
+ queue.push(extractedFamily);
611
+ }
534
612
 
535
- const strippedLegacyGlmTurbo = stripLegacyGlmTurboSuffix(normalized);
536
- if (strippedLegacyGlmTurbo) {
537
- queue.push(strippedLegacyGlmTurbo);
538
- }
613
+ const strippedProviderVersion = stripProviderVersionSuffix(normalized);
614
+ if (strippedProviderVersion) {
615
+ queue.push(strippedProviderVersion);
616
+ }
539
617
 
540
- const extractedFamily = extractUpstreamFamilyCandidate(normalized);
541
- if (extractedFamily) {
542
- queue.push(extractedFamily);
543
- }
618
+ const strippedDate = stripDateSuffix(normalized);
619
+ if (strippedDate) {
620
+ queue.push(strippedDate);
621
+ }
544
622
 
545
- const strippedProviderVersion = stripProviderVersionSuffix(normalized);
546
- if (strippedProviderVersion) {
547
- queue.push(strippedProviderVersion);
548
- }
623
+ const strippedMarker = stripTrailingMarker(normalized);
624
+ if (strippedMarker) {
625
+ queue.push(strippedMarker);
626
+ }
549
627
 
550
- const strippedDate = stripDateSuffix(normalized);
551
- if (strippedDate) {
552
- queue.push(strippedDate);
553
- }
628
+ const reorderedAnthropic = reorderAnthropicFamily(normalized);
629
+ if (reorderedAnthropic) {
630
+ queue.push(reorderedAnthropic);
631
+ }
632
+ }
554
633
 
555
- const strippedMarker = stripTrailingMarker(normalized);
556
- if (strippedMarker) {
557
- queue.push(strippedMarker);
558
- }
634
+ // Bounded FIFO memo: result depends only on `modelId` (the `_officialIds` param
635
+ // is unused — kept for signature stability). The returned array is consumed via
636
+ // `.filter` at every callsite, so sharing the cached instance is safe.
637
+ const HEURISTIC_CANDIDATES_CACHE = new Map<string, string[]>();
638
+ const HEURISTIC_CANDIDATES_CACHE_CAP = 256;
639
+ function getHeuristicCanonicalCandidates(modelId: string, _officialIds?: ReadonlySet<string>): string[] {
640
+ const cached = HEURISTIC_CANDIDATES_CACHE.get(modelId);
641
+ if (cached !== undefined) {
642
+ return cached;
643
+ }
644
+ const candidates = new Set<string>();
645
+ const queue: string[] = [modelId];
646
+ const visited = new Set<string>();
559
647
 
560
- const reorderedAnthropic = reorderAnthropicFamily(normalized);
561
- if (reorderedAnthropic) {
562
- queue.push(reorderedAnthropic);
648
+ for (let qi = 0; qi < queue.length; qi += 1) {
649
+ const candidate = queue[qi];
650
+ if (!candidate) {
651
+ continue;
563
652
  }
653
+ const normalized = candidate.trim();
654
+ if (!normalized || visited.has(normalized)) {
655
+ continue;
656
+ }
657
+ visited.add(normalized);
658
+ addCanonicalCandidate(candidates, normalized);
659
+ expandCheapCanonicalCandidates(normalized, queue);
660
+ expandHeavyCanonicalCandidates(normalized, queue);
564
661
  }
565
662
 
566
- return [...candidates];
663
+ const output = [...candidates];
664
+ if (HEURISTIC_CANDIDATES_CACHE.size >= HEURISTIC_CANDIDATES_CACHE_CAP) {
665
+ const oldest = HEURISTIC_CANDIDATES_CACHE.keys().next().value;
666
+ if (oldest !== undefined) {
667
+ HEURISTIC_CANDIDATES_CACHE.delete(oldest);
668
+ }
669
+ }
670
+ HEURISTIC_CANDIDATES_CACHE.set(modelId, output);
671
+ return output;
567
672
  }
568
673
 
569
674
  function getPreferredFallbackCanonicalCandidate(modelId: string, candidates: readonly string[]): string | undefined {
@@ -612,7 +717,7 @@ function resolveCanonicalIdForModel(
612
717
  return { id: claudeFamilyAlias, source: claudeFamilyAlias === model.id ? "bundled" : "heuristic" };
613
718
  }
614
719
 
615
- const heuristicCandidates = getHeuristicCanonicalCandidates(model.id);
720
+ const heuristicCandidates = getHeuristicCanonicalCandidates(model.id, referenceData.officialIds);
616
721
  const officialMatches = heuristicCandidates.filter(candidate => referenceData.officialIds.has(candidate));
617
722
  const preferredFallback = getPreferredFallbackCanonicalCandidate(model.id, heuristicCandidates);
618
723
  const match = selectBestOfficialCandidate(officialMatches);
@@ -665,10 +770,11 @@ export function buildCanonicalModelIndex(
665
770
  const byId = new Map<string, CanonicalModelRecord>();
666
771
  const bySelector = new Map<string, string>();
667
772
 
668
- let modelCache = resolutionCache.get(compiledEquivalence);
773
+ const compiledWithCache = compiledEquivalence as CompiledEquivalenceConfigWithCache;
774
+ let modelCache = compiledWithCache[kModelResolutionCache];
669
775
  if (!modelCache) {
670
776
  modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
671
- resolutionCache.set(compiledEquivalence, modelCache);
777
+ compiledWithCache[kModelResolutionCache] = modelCache;
672
778
  }
673
779
 
674
780
  for (const model of models) {
@@ -688,10 +794,9 @@ export function buildCanonicalModelIndex(
688
794
  const existing = byId.get(canonicalKey);
689
795
  const nextRecord: CanonicalModelRecord = existing ?? {
690
796
  id: canonical.id,
691
- name: getCanonicalRecordName(existing, canonical.id, variant, referenceData),
797
+ name: getCanonicalRecordName(undefined, canonical.id, variant, referenceData),
692
798
  variants: [],
693
799
  };
694
- nextRecord.name = getCanonicalRecordName(existing, canonical.id, variant, referenceData);
695
800
  nextRecord.variants.push(variant);
696
801
  byId.set(canonicalKey, nextRecord);
697
802
  bySelector.set(normalizeSelectorKey(selector), canonical.id);