From faae3d0f325f2502bdfdbd4c0dc3ceec52108a7c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 4 Feb 2026 11:25:59 +0900 Subject: [PATCH] fix(model-availability): prefer exact model ID match in fuzzyMatchModel (#1460) * fix(model-availability): prefer exact model ID match in fuzzyMatchModel * fix(model-availability): use filter+shortest for multi-provider tie-break - Change Priority 2 from find() to filter()+reduce() - Preserves shortest-match tie-break when multiple providers share model ID - Add test for multi-provider same model ID case - Addresses Oracle review feedback --- src/shared/model-availability.test.ts | 36 +++++++++++++++++++++++++++ src/shared/model-availability.ts | 19 ++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/shared/model-availability.test.ts b/src/shared/model-availability.test.ts index 784a8b57..61dcd886 100644 --- a/src/shared/model-availability.test.ts +++ b/src/shared/model-availability.test.ts @@ -277,6 +277,42 @@ describe("fuzzyMatchModel", () => { expect(result).toBe("anthropic/claude-opus-4-5") }) + // given available models with similar model IDs (e.g., glm-4.7 and glm-4.7-free) + // when searching for the longer variant (glm-4.7-free) + // then return exact model ID match, not the shorter one + it("should prefer exact model ID match over shorter substring match", () => { + const available = new Set([ + "zai-coding-plan/glm-4.7", + "zai-coding-plan/glm-4.7-free", + ]) + const result = fuzzyMatchModel("glm-4.7-free", available) + expect(result).toBe("zai-coding-plan/glm-4.7-free") + }) + + // given available models with similar model IDs + // when searching for the shorter variant + // then return the shorter match (existing behavior preserved) + it("should still prefer shorter match when searching for shorter variant", () => { + const available = new Set([ + "zai-coding-plan/glm-4.7", + "zai-coding-plan/glm-4.7-free", + ]) + const result = fuzzyMatchModel("glm-4.7", available) + expect(result).toBe("zai-coding-plan/glm-4.7") + }) + + // given same model ID from multiple providers + // when searching for exact model ID + // then return shortest full string (preserves tie-break behavior) + it("should use shortest tie-break when multiple providers have same model ID", () => { + const available = new Set([ + "opencode/gpt-5.2", + "openai/gpt-5.2", + ]) + const result = fuzzyMatchModel("gpt-5.2", available) + expect(result).toBe("openai/gpt-5.2") + }) + // given available models with multiple providers // when multiple providers are specified // then search all specified providers diff --git a/src/shared/model-availability.ts b/src/shared/model-availability.ts index 1b7ba0c5..62a72e8f 100644 --- a/src/shared/model-availability.ts +++ b/src/shared/model-availability.ts @@ -72,14 +72,29 @@ export function fuzzyMatchModel( return null } - // Priority 1: Exact match (normalized) + // Priority 1: Exact match (normalized full model string) const exactMatch = matches.find((model) => normalizeModelName(model) === targetNormalized) if (exactMatch) { log("[fuzzyMatchModel] exact match found", { exactMatch }) return exactMatch } - // Priority 2: Shorter model name (more specific) + // Priority 2: Exact model ID match (part after provider/) + // This ensures "glm-4.7-free" matches "zai-coding-plan/glm-4.7-free" over "zai-coding-plan/glm-4.7" + // Use filter + shortest to handle multi-provider cases (e.g., openai/gpt-5.2 + opencode/gpt-5.2) + const exactModelIdMatches = matches.filter((model) => { + const modelId = model.split("/").slice(1).join("/") + return normalizeModelName(modelId) === targetNormalized + }) + if (exactModelIdMatches.length > 0) { + const result = exactModelIdMatches.reduce((shortest, current) => + current.length < shortest.length ? current : shortest, + ) + log("[fuzzyMatchModel] exact model ID match found", { result, candidateCount: exactModelIdMatches.length }) + return result + } + + // Priority 3: Shorter model name (more specific, fallback for partial matches) const result = matches.reduce((shortest, current) => current.length < shortest.length ? current : shortest, )