Created
April 15, 2026 13:06
-
-
Save iRonin/002167fcb514635b631e27233ab29ba0 to your computer and use it in GitHub Desktop.
PR #2958: fix model-resolver :variant sort order
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts | |
| index 1825274a..8b9a5cbf 100644 | |
| --- a/packages/coding-agent/src/core/model-resolver.ts | |
| +++ b/packages/coding-agent/src/core/model-resolver.ts | |
| @@ -131,12 +131,24 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod | |
| const datedVersions = matches.filter((m) => !isAlias(m.id)); | |
| if (aliases.length > 0) { | |
| - // Prefer alias - if multiple aliases, pick the one that sorts highest | |
| - aliases.sort((a, b) => b.id.localeCompare(a.id)); | |
| + // Prefer aliases without a colon-variant suffix (e.g. "model" over "model:free"). | |
| + // A longer string beats its own prefix in localeCompare, so without this | |
| + // "qwen/qwen3.6-plus:free" would silently win over "qwen/qwen3.6-plus". | |
| + aliases.sort((a, b) => { | |
| + const aHasSuffix = a.id.includes(":"); | |
| + const bHasSuffix = b.id.includes(":"); | |
| + if (aHasSuffix !== bHasSuffix) return aHasSuffix ? 1 : -1; | |
| + return b.id.localeCompare(a.id); | |
| + }); | |
| return aliases[0]; | |
| } else { | |
| - // No alias found, pick latest dated version | |
| - datedVersions.sort((a, b) => b.id.localeCompare(a.id)); | |
| + // No alias found, pick latest dated version without a colon-variant suffix first. | |
| + datedVersions.sort((a, b) => { | |
| + const aHasSuffix = a.id.includes(":"); | |
| + const bHasSuffix = b.id.includes(":"); | |
| + if (aHasSuffix !== bHasSuffix) return aHasSuffix ? 1 : -1; | |
| + return b.id.localeCompare(a.id); | |
| + }); | |
| return datedVersions[0]; | |
| } | |
| } | |
| diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts | |
| index 511e5408..256ed618 100644 | |
| --- a/packages/coding-agent/test/model-resolver.test.ts | |
| +++ b/packages/coding-agent/test/model-resolver.test.ts | |
| @@ -37,6 +37,30 @@ const mockModels: Model<"anthropic-messages">[] = [ | |
| // Mock OpenRouter models with colons in IDs | |
| const mockOpenRouterModels: Model<"anthropic-messages">[] = [ | |
| + { | |
| + id: "qwen/qwen3.6-plus", | |
| + name: "Qwen3.6 Plus", | |
| + api: "anthropic-messages", | |
| + provider: "openrouter", | |
| + baseUrl: "https://openrouter.ai/api/v1", | |
| + reasoning: true, | |
| + input: ["text", "image"], | |
| + cost: { input: 0.325, output: 1.95, cacheRead: 0, cacheWrite: 0 }, | |
| + contextWindow: 1000000, | |
| + maxTokens: 65536, | |
| + }, | |
| + { | |
| + id: "qwen/qwen3.6-plus:free", | |
| + name: "Qwen3.6 Plus (free)", | |
| + api: "anthropic-messages", | |
| + provider: "openrouter", | |
| + baseUrl: "https://openrouter.ai/api/v1", | |
| + reasoning: true, | |
| + input: ["text", "image"], | |
| + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, | |
| + contextWindow: 1000000, | |
| + maxTokens: 65536, | |
| + }, | |
| { | |
| id: "qwen/qwen3-coder:exacto", | |
| name: "Qwen3 Coder Exacto", | |
| @@ -131,6 +155,34 @@ describe("parseModelPattern", () => { | |
| }); | |
| }); | |
| + describe("colon-variant sort order", () => { | |
| + test("base model wins over :free when both exist", () => { | |
| + // "qwen3.6-plus" partial-matches both "qwen/qwen3.6-plus" and "qwen/qwen3.6-plus:free". | |
| + // The base model must win — not the :free variant. | |
| + const result = parseModelPattern("qwen3.6-plus", allModels); | |
| + expect(result.model?.id).toBe("qwen/qwen3.6-plus"); | |
| + expect(result.thinkingLevel).toBeUndefined(); | |
| + }); | |
| + | |
| + test("base model wins over :free with thinking level", () => { | |
| + const result = parseModelPattern("qwen3.6-plus:high", allModels); | |
| + expect(result.model?.id).toBe("qwen/qwen3.6-plus"); | |
| + expect(result.thinkingLevel).toBe("high"); | |
| + }); | |
| + | |
| + test("explicit :free suffix selects the free variant", () => { | |
| + const result = parseModelPattern("qwen/qwen3.6-plus:free", allModels); | |
| + expect(result.model?.id).toBe("qwen/qwen3.6-plus:free"); | |
| + expect(result.thinkingLevel).toBeUndefined(); | |
| + }); | |
| + | |
| + test("only :free exists — still matches it when no base model is available", () => { | |
| + const freeOnly = allModels.filter((m) => m.id !== "qwen/qwen3.6-plus"); | |
| + const result = parseModelPattern("qwen3.6-plus", freeOnly); | |
| + expect(result.model?.id).toBe("qwen/qwen3.6-plus:free"); | |
| + }); | |
| + }); | |
| + | |
| describe("OpenRouter models with colons in IDs", () => { | |
| test("qwen3-coder:exacto matches the model with undefined thinking level", () => { | |
| const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels); | |
| @@ -362,7 +414,7 @@ describe("resolveCliModel", () => { | |
| } as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"]; | |
| const result = resolveCliModel({ | |
| - cliModel: "openrouter/qwen", | |
| + cliModel: "openrouter/qwen3-coder", | |
| modelRegistry: registry, | |
| }); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment