diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 56db5265c..089cd3e81 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `--model provider/id` resolving to wrong provider when model ID exists in multiple catalogs ([#560](https://github.com/can1357/oh-my-pi/issues/560)) + ## [13.16.4] - 2026-03-28 ### Changed diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index 82d20b3b5..f4c1c3f7c 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -730,9 +730,24 @@ export function resolveCliModel(options: { if (!provider) { const lower = cliModel.toLowerCase(); - const exact = availableModels.find( - model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower, - ); + // When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed + // provider+id match over flat id match. Without this, a model with id + // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai" + // with id "glm-5", because Array.find returns the first catalog hit. + const slashIdx = lower.indexOf("/"); + let exact: (typeof availableModels)[number] | undefined; + if (slashIdx !== -1) { + const prefix = lower.substring(0, slashIdx); + const suffix = lower.substring(slashIdx + 1); + exact = availableModels.find( + model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix, + ); + } + if (!exact) { + exact = availableModels.find( + model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower, + ); + } if (exact) { return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined }; } diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 10b0996e3..41ed6b689 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -551,6 +551,49 @@ describe("resolveCliModel", () => { expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); }); + + test("prefers decomposed provider+id over flat id match when ambiguous", () => { + // Simulates the zai/glm-5 bug: vercel-ai-gateway has id="zai/glm-5", + // zai has id="glm-5". Input "zai/glm-5" should resolve to provider=zai. + const ambiguousModels: Model<"anthropic-messages">[] = [ + { + id: "zai/glm-5", + name: "GLM-5 (Vercel)", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://vercel.ai", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 4096, + }, + { + id: "glm-5", + name: "GLM-5", + api: "anthropic-messages", + provider: "zai", + baseUrl: "https://api.z.ai", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 4096, + }, + ]; + const registry = { + getAll: () => ambiguousModels, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "zai/glm-5", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("zai"); + expect(result.model?.id).toBe("glm-5"); + }); }); describe("parseModelString", () => {