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 () => {
|
test("normalizes matched agent model string before returning categoryModel", async () => {
|
||||||
//#given
|
//#given
|
||||||
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
||||||
models: { openai: ["grok-3"] },
|
models: { openai: ["grok-3", "gpt-5.3-codex"] },
|
||||||
connected: ["openai"],
|
connected: ["openai"],
|
||||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
})
|
})
|
||||||
@@ -410,6 +410,56 @@ describe("resolveSubagentExecution", () => {
|
|||||||
connectedSpy.mockRestore()
|
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 () => {
|
test("prefers the most specific prefix match when fallback entries share a prefix", async () => {
|
||||||
//#given
|
//#given
|
||||||
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { log } from "../../shared/logger"
|
|||||||
import { getAvailableModelsForDelegateTask } from "./available-models"
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||||
import { resolveModelForDelegateTask } from "./model-selection"
|
import { resolveModelForDelegateTask } from "./model-selection"
|
||||||
|
import { fuzzyMatchModel } from "../../shared/model-availability"
|
||||||
|
|
||||||
export async function resolveSubagentExecution(
|
export async function resolveSubagentExecution(
|
||||||
args: DelegateTaskArgs,
|
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)
|
?? (agentOverride?.category ? userCategories?.[agentOverride.category]?.fallback_models : undefined)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const availableModels = await getAvailableModelsForDelegateTask(client)
|
||||||
|
|
||||||
if (agentOverride?.model || agentCategoryModel || agentRequirement || matchedAgent.model) {
|
if (agentOverride?.model || agentCategoryModel || agentRequirement || matchedAgent.model) {
|
||||||
const availableModels = await getAvailableModelsForDelegateTask(client)
|
|
||||||
|
|
||||||
const normalizedMatchedModel = matchedAgent.model
|
const normalizedMatchedModel = matchedAgent.model
|
||||||
? normalizeModelFormat(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) {
|
if (!categoryModel && matchedAgent.model) {
|
||||||
const normalizedMatchedModel = normalizeModelFormat(matchedAgent.model)
|
const normalizedMatchedModel = normalizeModelFormat(matchedAgent.model)
|
||||||
if (normalizedMatchedModel) {
|
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) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user