diff --git a/src/tools/delegate-task/category-resolver.test.ts b/src/tools/delegate-task/category-resolver.test.ts index 138e1b62f..4a52f4158 100644 --- a/src/tools/delegate-task/category-resolver.test.ts +++ b/src/tools/delegate-task/category-resolver.test.ts @@ -197,7 +197,7 @@ describe("resolveCategoryExecution", () => { if (!result.actualModel || !result.categoryModel) { throw new Error("Expected resolved model and category model") } - expect(result.actualModel).toBe("openai/gpt-5.4 high") + expect(result.actualModel).toBe("openai/gpt-5.4") expect(result.categoryModel).toEqual({ providerID: "openai", modelID: "gpt-5.4", diff --git a/src/tools/delegate-task/model-selection.test.ts b/src/tools/delegate-task/model-selection.test.ts index 3bc7c2c88..6350646dc 100644 --- a/src/tools/delegate-task/model-selection.test.ts +++ b/src/tools/delegate-task/model-selection.test.ts @@ -254,6 +254,100 @@ describe("resolveModelForDelegateTask", () => { }) }) + describe("#given user model override includes variant syntax", () => { + describe("#when userModel contains space-separated variant", () => { + test("#then extracts the variant and returns the base model separately", () => { + const result = resolveModelForDelegateTask({ + userModel: "openai/gpt-5.4 high", + categoryDefaultModel: "anthropic/claude-sonnet-4-6", + fallbackChain: [ + { providers: ["anthropic"], model: "claude-sonnet-4-6" }, + ], + availableModels: new Set(["openai/gpt-5.4"]), + }) + + expect(result).toEqual({ model: "openai/gpt-5.4", variant: "high" }) + }) + }) + + describe("#when userModel contains parenthesized variant", () => { + test("#then extracts the variant and returns the base model separately", () => { + const result = resolveModelForDelegateTask({ + userModel: "openai/gpt-5.4(max)", + categoryDefaultModel: "anthropic/claude-sonnet-4-6", + availableModels: new Set(), + }) + + expect(result).toEqual({ model: "openai/gpt-5.4", variant: "max" }) + }) + }) + + describe("#when userModel has no variant syntax", () => { + test("#then returns the model without a variant (backward compat)", () => { + const result = resolveModelForDelegateTask({ + userModel: "openai/gpt-5.4", + availableModels: new Set(), + }) + + expect(result).toEqual({ model: "openai/gpt-5.4" }) + }) + }) + + describe("#when userModel has a non-variant suffix (e.g. -high in model name)", () => { + test("#then preserves the full model name without extracting a variant", () => { + const result = resolveModelForDelegateTask({ + userModel: "new-api-openai/gpt-5.4-high", + availableModels: new Set(), + }) + + expect(result).toEqual({ model: "new-api-openai/gpt-5.4-high" }) + }) + }) + }) + + describe("#given user-configured category model includes variant syntax", () => { + beforeEach(() => { + hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(true) + hasProviderModelsSpy = spyOn(connectedProvidersCache, "hasProviderModelsCache").mockReturnValue(true) + }) + + describe("#when categoryDefaultModel with isUserConfiguredCategoryModel contains a space-separated variant", () => { + test("#then extracts the variant and returns the base model separately", () => { + const result = resolveModelForDelegateTask({ + categoryDefaultModel: "openai/gpt-5.4 medium", + isUserConfiguredCategoryModel: true, + availableModels: new Set(["openai/gpt-5.4"]), + }) + + expect(result).toEqual({ model: "openai/gpt-5.4", variant: "medium" }) + }) + }) + + describe("#when categoryDefaultModel with isUserConfiguredCategoryModel contains a parenthesized variant", () => { + test("#then extracts the variant and returns the base model separately", () => { + const result = resolveModelForDelegateTask({ + categoryDefaultModel: "openai/gpt-5.4(xhigh)", + isUserConfiguredCategoryModel: true, + availableModels: new Set(), + }) + + expect(result).toEqual({ model: "openai/gpt-5.4", variant: "xhigh" }) + }) + }) + + describe("#when categoryDefaultModel with isUserConfiguredCategoryModel has no variant", () => { + test("#then returns the model without a variant (backward compat)", () => { + const result = resolveModelForDelegateTask({ + categoryDefaultModel: "new-api-openai/gpt-5.4-high", + isUserConfiguredCategoryModel: true, + availableModels: new Set(["openai/gpt-5.4"]), + }) + + expect(result).toEqual({ model: "new-api-openai/gpt-5.4-high" }) + }) + }) + }) + describe("#given only connected providers cache exists (no provider-models cache)", () => { beforeEach(() => { hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(true) diff --git a/src/tools/delegate-task/model-selection.ts b/src/tools/delegate-task/model-selection.ts index 1e7ce2c4e..cef7df752 100644 --- a/src/tools/delegate-task/model-selection.ts +++ b/src/tools/delegate-task/model-selection.ts @@ -56,6 +56,10 @@ export function resolveModelForDelegateTask(input: { }): { model: string; variant?: string; fallbackEntry?: FallbackEntry; matchedFallback?: boolean } | { skipped: true } | undefined { const userModel = normalizeModel(input.userModel) if (userModel) { + const parsed = parseUserFallbackModel(userModel) + if (parsed?.variant) { + return { model: parsed.baseModel, variant: parsed.variant } + } return { model: userModel } } @@ -75,6 +79,10 @@ export function resolveModelForDelegateTask(input: { log("[resolveModelForDelegateTask] using user-configured category model (bypass validation)", { categoryDefaultModel: categoryDefault, }) + const parsed = parseUserFallbackModel(categoryDefault) + if (parsed?.variant) { + return { model: parsed.baseModel, variant: parsed.variant } + } return { model: categoryDefault } }