From e5c5438a4460e66b0db3914dfddd0b3410388bb3 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Wed, 25 Mar 2026 10:49:10 +0100 Subject: [PATCH] fix(delegate-task): gate fallback settings to real fallback matches --- assets/oh-my-opencode.schema.json | 960 +++++++++++++++++- .../delegate-task/category-resolver.test.ts | 46 + src/tools/delegate-task/category-resolver.ts | 25 +- .../delegate-task/model-selection.test.ts | 4 +- src/tools/delegate-task/model-selection.ts | 16 +- .../delegate-task/subagent-resolver.test.ts | 41 + src/tools/delegate-task/subagent-resolver.ts | 15 +- src/tools/delegate-task/tools.ts | 2 +- 8 files changed, 1070 insertions(+), 39 deletions(-) diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 118952a31..886c64e14 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -90,7 +90,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -309,7 +371,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -528,7 +652,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -747,7 +933,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -969,7 +1217,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -1188,7 +1498,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -1407,7 +1779,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -1626,7 +2060,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -1845,7 +2341,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -2064,7 +2622,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -2283,7 +2903,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -2502,7 +3184,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -2721,7 +3465,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -2940,7 +3746,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] @@ -3170,7 +4038,69 @@ { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "model" + ], + "additionalProperties": false + } + ] } } ] diff --git a/src/tools/delegate-task/category-resolver.test.ts b/src/tools/delegate-task/category-resolver.test.ts index 49ce1f733..0daf0e539 100644 --- a/src/tools/delegate-task/category-resolver.test.ts +++ b/src/tools/delegate-task/category-resolver.test.ts @@ -169,6 +169,52 @@ describe("resolveCategoryExecution", () => { agentsSpy.mockRestore() }) + test("does not apply object-style fallback settings when the configured primary model matches directly", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { openai: ["gpt-5.4-preview"] }, + connected: ["openai"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) + const args = { + category: "deep", + prompt: "test prompt", + description: "Test task", + run_in_background: false, + load_skills: [], + blockedBy: undefined, + enableSkillTools: false, + } + const executorCtx = createMockExecutorContext() + executorCtx.userCategories = { + deep: { + model: "openai/gpt-5.4-preview", + fallback_models: [ + { + model: "openai/gpt-5.4", + variant: "low", + reasoningEffort: "high", + }, + ], + }, + } + + //#when + const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6") + + //#then + expect(result.error).toBeUndefined() + expect(result.actualModel).toBe("openai/gpt-5.4-preview") + expect(result.categoryModel).toEqual({ + providerID: "openai", + modelID: "gpt-5.4-preview", + variant: "medium", + }) + cacheSpy.mockRestore() + agentsSpy.mockRestore() + }) + test("matches promoted fallback settings after fuzzy model resolution", async () => { //#given const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ diff --git a/src/tools/delegate-task/category-resolver.ts b/src/tools/delegate-task/category-resolver.ts index 492798155..648fa933c 100644 --- a/src/tools/delegate-task/category-resolver.ts +++ b/src/tools/delegate-task/category-resolver.ts @@ -89,6 +89,7 @@ Available categories: ${allCategoryNames}`, let categoryModel: DelegatedModelConfig | undefined let isModelResolutionSkipped = false let fallbackEntry: FallbackEntry | undefined + let matchedFallback = false const overrideModel = sisyphusJuniorModel const explicitCategoryModel = userCategories?.[args.category!]?.model @@ -122,8 +123,14 @@ Available categories: ${allCategoryNames}`, if (resolution && "skipped" in resolution) { isModelResolutionSkipped = true } else if (resolution) { - const { model: resolvedModel, variant: resolvedVariant, fallbackEntry: resolvedFallbackEntry } = resolution + const { + model: resolvedModel, + variant: resolvedVariant, + fallbackEntry: resolvedFallbackEntry, + matchedFallback: resolvedMatchedFallback, + } = resolution fallbackEntry = resolvedFallbackEntry + matchedFallback = resolvedMatchedFallback === true actualModel = resolvedModel if (!parseModelString(actualModel)) { @@ -202,13 +209,15 @@ Available categories: ${categoryNames.join(", ")}`, defaultProviderID, ) - // Apply per-model settings from the source that provided the match: - // 1. fallbackEntry from resolver (built-in chain match) — exact, no lookup needed - // 2. configuredFallbackChain (user's fallback_models) — prefix match against user config - const effectiveEntry = fallbackEntry - ?? (categoryModel && configuredFallbackChain - ? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain) - : undefined) + // Only promote fallback-only settings when resolution actually selected a fallback model. + const effectiveEntry = matchedFallback && categoryModel + ? ( + fallbackEntry + ?? (configuredFallbackChain + ? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain) + : undefined) + ) + : undefined if (categoryModel && effectiveEntry) { categoryModel = { diff --git a/src/tools/delegate-task/model-selection.test.ts b/src/tools/delegate-task/model-selection.test.ts index 4324db931..802990e0f 100644 --- a/src/tools/delegate-task/model-selection.test.ts +++ b/src/tools/delegate-task/model-selection.test.ts @@ -121,7 +121,7 @@ describe("resolveModelForDelegateTask", () => { availableModels: new Set(["openai/gpt-5.2"]), }) - expect(result).toEqual({ model: "openai/gpt-5.2", variant: "high" }) + expect(result).toEqual({ model: "openai/gpt-5.2", variant: "high", matchedFallback: true }) }) test("#then resolves a space-separated variant against the base available model", () => { @@ -130,7 +130,7 @@ describe("resolveModelForDelegateTask", () => { availableModels: new Set(["openai/gpt-5.2"]), }) - expect(result).toEqual({ model: "openai/gpt-5.2", variant: "medium" }) + expect(result).toEqual({ model: "openai/gpt-5.2", variant: "medium", matchedFallback: true }) }) }) }) diff --git a/src/tools/delegate-task/model-selection.ts b/src/tools/delegate-task/model-selection.ts index 4d52308be..4b140dfee 100644 --- a/src/tools/delegate-task/model-selection.ts +++ b/src/tools/delegate-task/model-selection.ts @@ -53,7 +53,7 @@ export function resolveModelForDelegateTask(input: { fallbackChain?: FallbackEntry[] availableModels: Set systemDefaultModel?: string -}): { model: string; variant?: string; fallbackEntry?: FallbackEntry } | { skipped: true } | undefined { +}): { model: string; variant?: string; fallbackEntry?: FallbackEntry; matchedFallback?: boolean } | { skipped: true } | undefined { const userModel = normalizeModel(input.userModel) if (userModel) { return { model: userModel } @@ -97,7 +97,7 @@ export function resolveModelForDelegateTask(input: { if (input.availableModels.size === 0) { const first = userFallbackModels[0] ? parseUserFallbackModel(userFallbackModels[0]) : undefined if (first) { - return { model: first.baseModel, variant: first.variant } + return { model: first.baseModel, variant: first.variant, matchedFallback: true } } } else { for (const fallbackModel of userFallbackModels) { @@ -106,7 +106,7 @@ export function resolveModelForDelegateTask(input: { const match = fuzzyMatchModel(parsedFallback.baseModel, input.availableModels, parsedFallback.providerHint) if (match) { - return { model: match, variant: parsedFallback.variant } + return { model: match, variant: parsedFallback.variant, matchedFallback: true } } } } @@ -119,7 +119,7 @@ export function resolveModelForDelegateTask(input: { const provider = first?.providers?.[0] if (provider) { const transformedModelId = transformModelForProvider(provider, first.model) - return { model: `${provider}/${transformedModelId}`, variant: first.variant, fallbackEntry: first } + return { model: `${provider}/${transformedModelId}`, variant: first.variant, fallbackEntry: first, matchedFallback: true } } } else { for (const entry of fallbackChain) { @@ -128,20 +128,20 @@ export function resolveModelForDelegateTask(input: { const match = fuzzyMatchModel(fullModel, input.availableModels, [provider]) if (match) { if (explicitHighModel && entry.variant === "high" && match === explicitHighBaseModel) { - return { model: explicitHighModel, fallbackEntry: entry } + return { model: explicitHighModel, fallbackEntry: entry, matchedFallback: true } } - return { model: match, variant: entry.variant, fallbackEntry: entry } + return { model: match, variant: entry.variant, fallbackEntry: entry, matchedFallback: true } } } const crossProviderMatch = fuzzyMatchModel(entry.model, input.availableModels) if (crossProviderMatch) { if (explicitHighModel && entry.variant === "high" && crossProviderMatch === explicitHighBaseModel) { - return { model: explicitHighModel, fallbackEntry: entry } + return { model: explicitHighModel, fallbackEntry: entry, matchedFallback: true } } - return { model: crossProviderMatch, variant: entry.variant, fallbackEntry: entry } + return { model: crossProviderMatch, variant: entry.variant, fallbackEntry: entry, matchedFallback: true } } } } diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 0d53d7157..94f0a18f0 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -227,6 +227,47 @@ describe("resolveSubagentExecution", () => { connectedSpy.mockRestore() }) + test("does not apply object-style fallback settings when the subagent primary model matches directly", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { openai: ["gpt-5.4-preview"] }, + connected: ["openai"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"]) + const args = createBaseArgs({ subagent_type: "explore" }) + const executorCtx = createExecutorContext( + async () => ([ + { name: "explore", mode: "subagent", model: "openai/gpt-5.4-preview" }, + ]), + { + agentOverrides: { + explore: { + fallback_models: [ + { + model: "openai/gpt-5.4", + variant: "low", + reasoningEffort: "high", + }, + ], + }, + } as ExecutorContext["agentOverrides"], + } + ) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.categoryModel).toEqual({ + providerID: "openai", + modelID: "gpt-5.4-preview", + }) + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + test("matches promoted fallback settings after fuzzy model resolution", async () => { //#given const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ diff --git a/src/tools/delegate-task/subagent-resolver.ts b/src/tools/delegate-task/subagent-resolver.ts index f1e03a6a5..fe80af663 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -142,12 +142,17 @@ Create the work plan directly - that's your job as the planning agent.`, ) fallbackChain = configuredFallbackChain ?? agentRequirement?.fallbackChain - // Apply per-model settings: prefer resolver's exact entry, fall back to prefix match on user config + // Only promote fallback-only settings when resolution actually selected a fallback model. const resolvedFallbackEntry = (resolution && !('skipped' in resolution)) ? resolution.fallbackEntry : undefined - const effectiveEntry = resolvedFallbackEntry - ?? (categoryModel && fallbackChain - ? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, fallbackChain) - : undefined) + const matchedFallback = (resolution && !('skipped' in resolution)) ? resolution.matchedFallback === true : false + const effectiveEntry = matchedFallback && categoryModel + ? ( + resolvedFallbackEntry + ?? (configuredFallbackChain + ? findMostSpecificFallbackEntry(categoryModel.providerID, categoryModel.modelID, configuredFallbackChain) + : undefined) + ) + : undefined if (categoryModel && effectiveEntry) { categoryModel = { diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index 280e7b79d..7542f9ac4 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -237,7 +237,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini return executeUnstableAgentTask(args, ctx, options, parentContext, agentToUse, categoryModel, systemContent, actualModel) } } else { - const resolution = await resolveSubagentExecution(args, options, parentContext.agent, categoryExamples, inheritedModel) + const resolution = await resolveSubagentExecution(args, options, parentContext.agent, categoryExamples) if (resolution.error) { return resolution.error }