diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index eb2fa5a9..0bf10123 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). - Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 292ccf37..0017df00 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -422,6 +422,27 @@ export function resolveCliModel(options: { }); if (model) { + // If provider inference matched an unauthenticated provider/model pair, prefer + // one exact raw model-id match that is authenticated. This keeps + // "provider/model" syntax preferred when usable, but handles models whose + // literal id starts with a known provider name (for example + // commandcode model id "xiaomi/mimo-v2.5-pro"). + if (inferredProvider) { + const rawExactMatches = availableModels.filter( + (m) => m.id.toLowerCase() === cliModel.toLowerCase() && !modelsAreEqual(m, model), + ); + if (rawExactMatches.length > 0 && !modelRegistry.hasConfiguredAuth(model)) { + const authenticatedRawMatches = rawExactMatches.filter((m) => modelRegistry.hasConfiguredAuth(m)); + if (authenticatedRawMatches.length === 1) { + return { + model: authenticatedRawMatches[0], + thinkingLevel: undefined, + warning: undefined, + error: undefined, + }; + } + } + } return { model, thinkingLevel, warning, error: undefined }; } diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 32861a9e..163c3ab5 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -344,6 +344,7 @@ describe("resolveCliModel", () => { }; const registry = { getAll: () => [...allModels, zaiModel, gatewayModel], + hasConfiguredAuth: () => true, } as unknown as Parameters[0]["modelRegistry"]; const result = resolveCliModel({ @@ -356,6 +357,46 @@ describe("resolveCliModel", () => { expect(result.model?.id).toBe("glm-5"); }); + test("prefers an authenticated exact raw model id over an unauthenticated inferred provider", () => { + const commandcodeModel: Model<"anthropic-messages"> = { + id: "xiaomi/mimo-v2.5-pro", + name: "Xiaomi MiMo via Commandcode", + api: "anthropic-messages", + provider: "commandcode", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 8192, + }; + const xiaomiModel: Model<"anthropic-messages"> = { + id: "mimo-v2.5-pro", + name: "Xiaomi MiMo", + api: "anthropic-messages", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 8192, + }; + const registry = { + getAll: () => [...allModels, commandcodeModel, xiaomiModel], + hasConfiguredAuth: (model: Model<"anthropic-messages">) => model.provider === "commandcode", + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "xiaomi/mimo-v2.5-pro", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("commandcode"); + expect(result.model?.id).toBe("xiaomi/mimo-v2.5-pro"); + }); + test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => { const registry = { getAll: () => allModels,