Skip to content

Commit 427051e

Browse files
authored
fix: normalize model name separators in CLI validation
Apply separator normalization (. and _ -> -) to the AWF CLI model validation path so both 'claude-haiku-4-5' and 'claude-haiku-4.5' are accepted and resolved to the canonical form 'claude-haiku-4.5'. This mirrors the existing tolerant behavior in the api-proxy's model-utils.js / model-resolver.js. The fix also extends to retired-alias lookups for consistency. Closes #4907
1 parent 631fd5c commit 427051e

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

src/copilot-model.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ describe('validateCopilotModel', () => {
2626
expect(result).toEqual({ valid: true, resolvedModel: 'gpt-4.1' });
2727
});
2828

29+
it('normalizes hyphenated version separator to canonical dot form (claude-haiku-4-5 → claude-haiku-4.5)', () => {
30+
const result = validateCopilotModel('claude-haiku-4-5');
31+
expect(result).toEqual({ valid: true, resolvedModel: 'claude-haiku-4.5' });
32+
});
33+
34+
it('normalizes underscore separator to canonical dot form (claude_haiku_4_5 → claude-haiku-4.5)', () => {
35+
const result = validateCopilotModel('claude_haiku_4_5');
36+
expect(result).toEqual({ valid: true, resolvedModel: 'claude-haiku-4.5' });
37+
});
38+
39+
it('normalizes uppercase + hyphen separators (CLAUDE-HAIKU-4-5 → claude-haiku-4.5)', () => {
40+
const result = validateCopilotModel('CLAUDE-HAIKU-4-5');
41+
expect(result).toEqual({ valid: true, resolvedModel: 'claude-haiku-4.5' });
42+
});
43+
44+
it('normalizes hyphenated version separator for other models (claude-sonnet-4-6 → claude-sonnet-4.6)', () => {
45+
const result = validateCopilotModel('claude-sonnet-4-6');
46+
expect(result).toEqual({ valid: true, resolvedModel: 'claude-sonnet-4.6' });
47+
});
48+
49+
it('accepts canonical dot form without normalization (claude-haiku-4.5 → claude-haiku-4.5)', () => {
50+
const result = validateCopilotModel('claude-haiku-4.5');
51+
expect(result).toEqual({ valid: true, resolvedModel: 'claude-haiku-4.5' });
52+
});
53+
54+
it('still rejects retired aliases regardless of separator (gpt-5-codex stays retired)', () => {
55+
const result = validateCopilotModel('gpt-5-codex');
56+
expect(result.valid).toBe(false);
57+
if (result.valid) return;
58+
expect(result.reason).toBe('retired');
59+
expect(result.message).toContain("Did you mean 'gpt-5.3-codex'?");
60+
});
61+
2962
it('rejects unsupported models with suggestion when close to known catalog', () => {
3063
const result = validateCopilotModel('gpt-5.3-codx');
3164
expect(result.valid).toBe(false);

src/copilot-model.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export type CopilotModelValidationResult =
1313
| CopilotModelValidationSuccess
1414
| CopilotModelValidationFailure;
1515

16+
/**
17+
* Normalize separators (`.` and `_`) to `-` for separator-agnostic model matching.
18+
* Mirrors the canonicalization used in `containers/api-proxy/model-utils.js`.
19+
*/
20+
function normalizeSeparators(s: string): string {
21+
return s.replace(/[._]/g, '-');
22+
}
23+
1624
const RETIRED_COPILOT_MODEL_ALIASES: Record<string, string> = {
1725
'gpt-5-codex': 'gpt-5.3-codex',
1826
};
@@ -39,6 +47,16 @@ const SUPPORTED_COPILOT_MODELS = new Set([
3947
'gemini-3.5-flash',
4048
]);
4149

50+
/** Maps separator-normalized names (all `-`) → canonical model name. */
51+
const NORMALIZED_TO_CANONICAL = new Map<string, string>(
52+
[...SUPPORTED_COPILOT_MODELS].map(m => [normalizeSeparators(m), m]),
53+
);
54+
55+
/** Maps separator-normalized retired alias keys → canonical replacement. */
56+
const NORMALIZED_RETIRED_ALIASES = new Map<string, string>(
57+
Object.entries(RETIRED_COPILOT_MODEL_ALIASES).map(([k, v]) => [normalizeSeparators(k), v]),
58+
);
59+
4260
function suggestionFor(model: string): string | undefined {
4361
let best: { candidate: string; distance: number } | undefined;
4462
for (const candidate of SUPPORTED_COPILOT_MODELS) {
@@ -73,8 +91,11 @@ export function validateCopilotModel(rawModel: string): CopilotModelValidationRe
7391
return { valid: true, resolvedModel: trimmed };
7492
}
7593
const normalized = trimmed.toLowerCase();
94+
const separatorNormalized = normalizeSeparators(normalized);
7695

77-
const retiredReplacement = RETIRED_COPILOT_MODEL_ALIASES[normalized];
96+
// Check retired aliases (exact, then separator-normalized)
97+
const retiredReplacement =
98+
RETIRED_COPILOT_MODEL_ALIASES[normalized] ?? NORMALIZED_RETIRED_ALIASES.get(separatorNormalized);
7899
if (retiredReplacement) {
79100
return {
80101
valid: false,
@@ -83,10 +104,17 @@ export function validateCopilotModel(rawModel: string): CopilotModelValidationRe
83104
};
84105
}
85106

107+
// Exact match
86108
if (SUPPORTED_COPILOT_MODELS.has(normalized)) {
87109
return { valid: true, resolvedModel: normalized };
88110
}
89111

112+
// Separator-normalized match: treat `.`, `_`, `-` as equivalent
113+
const canonicalModel = NORMALIZED_TO_CANONICAL.get(separatorNormalized);
114+
if (canonicalModel) {
115+
return { valid: true, resolvedModel: canonicalModel };
116+
}
117+
90118
const suggested = suggestionFor(normalized);
91119
return {
92120
valid: false,

0 commit comments

Comments
 (0)