Skip to content

Instantly share code, notes, and snippets.

@iRonin
Created April 15, 2026 13:06
Show Gist options
  • Select an option

  • Save iRonin/002167fcb514635b631e27233ab29ba0 to your computer and use it in GitHub Desktop.

Select an option

Save iRonin/002167fcb514635b631e27233ab29ba0 to your computer and use it in GitHub Desktop.
PR #2958: fix model-resolver :variant sort order
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