diff --git a/src/tools/delegate-task/subagent-resolver.test.ts b/src/tools/delegate-task/subagent-resolver.test.ts index 94f0a18f0..53cf0f0a4 100644 --- a/src/tools/delegate-task/subagent-resolver.test.ts +++ b/src/tools/delegate-task/subagent-resolver.test.ts @@ -88,7 +88,7 @@ describe("resolveSubagentExecution", () => { test("normalizes matched agent model string before returning categoryModel", async () => { //#given const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ - models: { openai: ["grok-3"] }, + models: { openai: ["grok-3", "gpt-5.3-codex"] }, connected: ["openai"], updatedAt: "2026-03-03T00:00:00.000Z", }) @@ -410,6 +410,56 @@ describe("resolveSubagentExecution", () => { connectedSpy.mockRestore() }) + test("does not use unavailable matchedAgent.model as fallback for custom subagent", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { minimaxi: ["MiniMax-M2.7"] }, + connected: ["minimaxi"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"]) + const args = createBaseArgs({ subagent_type: "my-custom-agent" }) + const executorCtx = createExecutorContext( + async () => ([ + { name: "my-custom-agent", mode: "subagent", model: "minimaxi/MiniMax-M2.7-highspeed" }, + ]), + ) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.categoryModel?.modelID).not.toBe("MiniMax-M2.7-highspeed") + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + + test("uses matchedAgent.model as fallback when model is available", async () => { + //#given + const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({ + models: { minimaxi: ["MiniMax-M2.7-highspeed"] }, + connected: ["minimaxi"], + updatedAt: "2026-03-03T00:00:00.000Z", + }) + const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"]) + const args = createBaseArgs({ subagent_type: "my-custom-agent" }) + const executorCtx = createExecutorContext( + async () => ([ + { name: "my-custom-agent", mode: "subagent", model: "minimaxi/MiniMax-M2.7-highspeed" }, + ]), + ) + + //#when + const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep") + + //#then + expect(result.error).toBeUndefined() + expect(result.categoryModel).toEqual({ providerID: "minimaxi", modelID: "MiniMax-M2.7-highspeed" }) + cacheSpy.mockRestore() + connectedSpy.mockRestore() + }) + test("prefers the most specific prefix match when fallback entries share a prefix", 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 1171eabf4..12e97f52a 100644 --- a/src/tools/delegate-task/subagent-resolver.ts +++ b/src/tools/delegate-task/subagent-resolver.ts @@ -13,6 +13,7 @@ import { log } from "../../shared/logger" import { getAvailableModelsForDelegateTask } from "./available-models" import type { FallbackEntry } from "../../shared/model-requirements" import { resolveModelForDelegateTask } from "./model-selection" +import { fuzzyMatchModel } from "../../shared/model-availability" export async function resolveSubagentExecution( args: DelegateTaskArgs, @@ -109,8 +110,9 @@ Create the work plan directly - that's your job as the planning agent.`, ?? (agentOverride?.category ? userCategories?.[agentOverride.category]?.fallback_models : undefined) ) + const availableModels = await getAvailableModelsForDelegateTask(client) + if (agentOverride?.model || agentCategoryModel || agentRequirement || matchedAgent.model) { - const availableModels = await getAvailableModelsForDelegateTask(client) const normalizedMatchedModel = matchedAgent.model ? normalizeModelFormat(matchedAgent.model) @@ -192,7 +194,15 @@ Create the work plan directly - that's your job as the planning agent.`, if (!categoryModel && matchedAgent.model) { const normalizedMatchedModel = normalizeModelFormat(matchedAgent.model) if (normalizedMatchedModel) { - categoryModel = normalizedMatchedModel + const fullModel = `${normalizedMatchedModel.providerID}/${normalizedMatchedModel.modelID}` + if (availableModels.size === 0 || fuzzyMatchModel(fullModel, availableModels, [normalizedMatchedModel.providerID])) { + categoryModel = normalizedMatchedModel + } else { + log("[delegate-task] Skipping unavailable agent default model", { + agent: agentToUse, + model: fullModel, + }) + } } } } catch (error) {