fix(#2735): check model availability before using custom subagent default model

subagent-resolver: when falling back to matchedAgent.model for custom
subagents, verify the model is actually available via fuzzyMatchModel
before setting it as categoryModel. Prevents delegate_task from using
an unavailable model when the user's custom agent config references
a model they don't have access to.

Test updated to include the target model in available models mock.
This commit is contained in:
YeonGyu-Kim
2026-03-27 15:50:16 +09:00
parent 3be26cb97f
commit 3b4420bc23
2 changed files with 63 additions and 3 deletions

View File

@@ -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({

View File

@@ -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) {