fix(coding-agent): parse :thinking suffix from custom model IDs in fallback path
Fixes #5552
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ function buildSessionOptions(
|
||||
const resolved = resolveCliModel({
|
||||
cliProvider: parsed.provider,
|
||||
cliModel: parsed.model,
|
||||
cliThinking: parsed.thinking,
|
||||
modelRegistry,
|
||||
});
|
||||
if (resolved.warning) {
|
||||
|
||||
@@ -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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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<typeof resolveCliModel>[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", () => {
|
||||
|
||||
Reference in New Issue
Block a user