fix: resolve --model provider/id to correct provider when ambiguous (#563)

fixes #560

When the CLI input has provider/id format (e.g. zai/glm-5), the exact
match now checks decomposed provider+id first (provider=zai, id=glm-5)
before falling back to flat model.id string match. This prevents
vercel-ai-gateway's 'zai/glm-5' model from winning over the zai
provider's 'glm-5' model due to Array.find catalog ordering.

Keeps getAll() rather than switching to getAvailable() to preserve
the --api-key ephemeral flow, where auth is injected after resolution.
This commit is contained in:
Sam Biggins
2026-03-29 17:48:31 +02:00
committed by GitHub
parent 77d459cbd7
commit 911db0000f
3 changed files with 65 additions and 3 deletions
+4
View File
@@ -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
@@ -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 };
}
@@ -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<typeof resolveCliModel>[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", () => {