diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 53b8ad11..c9afb7b2 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -340,9 +340,10 @@ export interface ResolveCliModelResult { export function resolveCliModel(options: { cliProvider?: string; cliModel?: string; + cliThinking?: string; modelRegistry: ModelRegistry; }): ResolveCliModelResult { - const { cliProvider, cliModel, modelRegistry } = options; + const { cliProvider, cliModel, cliThinking, modelRegistry } = options; if (!cliModel) { return { model: undefined, warning: undefined, error: undefined }; @@ -451,12 +452,28 @@ export function resolveCliModel(options: { } if (provider) { - const fallbackModel = buildFallbackModel(provider, pattern, availableModels); + // Parse thinking level suffix from the pattern before building the fallback model, + // but only when --thinking is not explicitly provided. + // e.g. "zai-org/GLM-5.1-FP8:high" → modelId="zai-org/GLM-5.1-FP8", fallbackThinking="high" + let fallbackPattern = pattern; + let fallbackThinking: ThinkingLevel | undefined; + if (!cliThinking) { + const lastColon = pattern.lastIndexOf(":"); + if (lastColon !== -1) { + const suffix = pattern.substring(lastColon + 1); + if (isValidThinkingLevel(suffix)) { + fallbackPattern = pattern.substring(0, lastColon); + fallbackThinking = suffix; + } + } + } + + const fallbackModel = buildFallbackModel(provider, fallbackPattern, availableModels); if (fallbackModel) { const fallbackWarning = warning - ? `${warning} Model "${pattern}" not found for provider "${provider}". Using custom model id.` - : `Model "${pattern}" not found for provider "${provider}". Using custom model id.`; - return { model: fallbackModel, thinkingLevel: undefined, warning: fallbackWarning, error: undefined }; + ? `${warning} Model "${fallbackPattern}" not found for provider "${provider}". Using custom model id.` + : `Model "${fallbackPattern}" not found for provider "${provider}". Using custom model id.`; + return { model: fallbackModel, thinkingLevel: fallbackThinking, warning: fallbackWarning, error: undefined }; } } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 38f95246..d6b95b73 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -360,6 +360,7 @@ function buildSessionOptions( const resolved = resolveCliModel({ cliProvider: parsed.provider, cliModel: parsed.model, + cliThinking: parsed.thinking, modelRegistry, }); if (resolved.warning) { diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 0ac54939..4b4caf0f 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -370,6 +370,127 @@ describe("resolveCliModel", () => { expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); }); + + describe("custom model fallback with :thinking suffix (#5552)", () => { + // Models for a provider that has registered models but the specific model ID + // is not in the registry (triggers buildFallbackModel path). + const neuralwattModel: Model<"anthropic-messages"> = { + id: "some-base-model", + name: "Some Base Model", + api: "anthropic-messages", + provider: "neuralwatt", + baseUrl: "https://api.neuralwatt.com", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 8192, + }; + + const modelsWithNeuralwatt = [...allModels, neuralwattModel]; + + test("strips :thinking suffix from custom model id in fallback path", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:high", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("neuralwatt"); + // The :high suffix must NOT leak into the model id sent to the API + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8"); + expect(result.thinkingLevel).toBe("high"); + }); + + test("custom model without thinking suffix works normally in fallback path", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "neuralwatt/zai-org/GLM-5.1-FP8", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("neuralwatt"); + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8"); + expect(result.thinkingLevel).toBeUndefined(); + }); + + test("all valid thinking levels work in fallback path", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + for (const level of ["off", "minimal", "low", "medium", "high", "xhigh"]) { + const result = resolveCliModel({ + cliModel: `neuralwatt/zai-org/GLM-5.1-FP8:${level}`, + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8"); + expect(result.thinkingLevel).toBe(level); + } + }); + + test("invalid thinking suffix on custom model is treated as part of model id", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:banana", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("neuralwatt"); + // Invalid suffix stays in the id (it's not a thinking level) + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8:banana"); + expect(result.thinkingLevel).toBeUndefined(); + }); + + test("explicit --provider with custom model:thinking strips suffix correctly", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliProvider: "neuralwatt", + cliModel: "zai-org/GLM-5.1-FP8:high", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("neuralwatt"); + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8"); + expect(result.thinkingLevel).toBe("high"); + }); + + test("with explicit --thinking, :suffix is kept as part of model id", () => { + const registry = { + getAll: () => modelsWithNeuralwatt, + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:high", + cliThinking: "medium", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("neuralwatt"); + // :high is kept as part of the model id since --thinking was explicit + expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8:high"); + expect(result.thinkingLevel).toBeUndefined(); + }); + }); }); describe("default model selection", () => {