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:
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user