diff --git a/src/tools/delegate-task/executor.ts b/src/tools/delegate-task/executor.ts index 2171b5bb5..5aabad130 100644 --- a/src/tools/delegate-task/executor.ts +++ b/src/tools/delegate-task/executor.ts @@ -14,7 +14,7 @@ import { getTaskToastManager } from "../../features/task-toast-manager" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "../../shared" import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability" -import { readConnectedProvidersCache } from "../../shared/connected-providers-cache" +import * as connectedProvidersCache from "../../shared/connected-providers-cache" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { storeToolMetadata } from "../../features/tool-metadata-store" @@ -40,6 +40,8 @@ export interface ExecutorContext { manager: BackgroundManager client: OpencodeClient directory: string + connectedProvidersOverride?: string[] | null + availableModelsOverride?: Set userCategories?: CategoriesConfig gitMasterConfig?: GitMasterConfig sisyphusJuniorModel?: string @@ -727,10 +729,15 @@ export async function resolveCategoryExecution( ): Promise { const { client, userCategories, sisyphusJuniorModel } = executorCtx - const connectedProviders = readConnectedProvidersCache() - const availableModels = await fetchAvailableModels(client, { - connectedProviders: connectedProviders ?? undefined, - }) + const connectedProviders = executorCtx.connectedProvidersOverride !== undefined + ? executorCtx.connectedProvidersOverride + : connectedProvidersCache.readConnectedProvidersCache() + + const availableModels = executorCtx.availableModelsOverride !== undefined + ? executorCtx.availableModelsOverride + : await fetchAvailableModels(client, { + connectedProviders: connectedProviders ?? undefined, + }) const resolved = resolveCategoryConfig(args.category!, { userCategories, @@ -775,7 +782,7 @@ export async function resolveCategoryExecution( userModel: explicitCategoryModel ?? overrideModel, categoryDefaultModel: resolved.model, }, - constraints: { availableModels }, + constraints: { availableModels, connectedProviders }, policy: { fallbackChain: requirement.fallbackChain, systemDefaultModel, @@ -941,26 +948,31 @@ Create the work plan directly - that's your job as the planning agent.`, const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentNameLower] if (agentOverride?.model || agentRequirement) { - const connectedProviders = readConnectedProvidersCache() - const availableModels = await fetchAvailableModels(client, { - connectedProviders: connectedProviders ?? undefined, - }) + const connectedProviders = executorCtx.connectedProvidersOverride !== undefined + ? executorCtx.connectedProvidersOverride + : connectedProvidersCache.readConnectedProvidersCache() + + const availableModels = executorCtx.availableModelsOverride !== undefined + ? executorCtx.availableModelsOverride + : await fetchAvailableModels(client, { + connectedProviders: connectedProviders ?? undefined, + }) const matchedAgentModelStr = matchedAgent.model ? `${matchedAgent.model.providerID}/${matchedAgent.model.modelID}` : undefined - const resolution = resolveModelPipeline({ - intent: { - userModel: agentOverride?.model, - categoryDefaultModel: matchedAgentModelStr, - }, - constraints: { availableModels }, - policy: { - fallbackChain: agentRequirement?.fallbackChain, - systemDefaultModel: undefined, - }, - }) + const resolution = resolveModelPipeline({ + intent: { + userModel: agentOverride?.model, + categoryDefaultModel: matchedAgentModelStr, + }, + constraints: { availableModels, connectedProviders }, + policy: { + fallbackChain: agentRequirement?.fallbackChain, + systemDefaultModel: undefined, + }, + }) if (resolution) { const parsed = parseModelString(resolution.model) diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 4f32addf8..e54420706 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -10,6 +10,21 @@ import * as connectedProvidersCache from "../../shared/connected-providers-cache const SYSTEM_DEFAULT_MODEL = "anthropic/claude-sonnet-4-5" +const TEST_CONNECTED_PROVIDERS = ["anthropic", "google", "openai"] +const TEST_AVAILABLE_MODELS = new Set([ + "anthropic/claude-opus-4-6", + "anthropic/claude-sonnet-4-5", + "anthropic/claude-haiku-4-5", + "google/gemini-3-pro", + "google/gemini-3-flash", + "openai/gpt-5.2", + "openai/gpt-5.3-codex", +]) + +function createTestAvailableModels(): Set { + return new Set(TEST_AVAILABLE_MODELS) +} + describe("sisyphus-task", () => { let cacheSpy: ReturnType let providerModelsSpy: ReturnType @@ -271,6 +286,8 @@ describe("sisyphus-task", () => { const tool = createDelegateTask({ manager: mockManager, client: mockClient, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -324,6 +341,8 @@ describe("sisyphus-task", () => { const tool = createDelegateTask({ manager: mockManager, client: mockClient, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -436,6 +455,8 @@ describe("sisyphus-task", () => { const tool = createDelegateTask({ manager: mockManager, client: mockClient, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const metadataCalls: Array<{ title?: string; metadata?: Record }> = [] @@ -727,6 +748,8 @@ describe("sisyphus-task", () => { userCategories: { ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" }, }, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -790,6 +813,8 @@ describe("sisyphus-task", () => { const tool = createDelegateTask({ manager: mockManager, client: mockClient, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -1950,6 +1975,8 @@ describe("sisyphus-task", () => { client: mockClient, // userCategories: undefined - use DEFAULT_CATEGORIES only // sisyphusJuniorModel: undefined + connectedProvidersOverride: null, + availableModelsOverride: new Set(), }) const toolContext = { @@ -2013,6 +2040,8 @@ describe("sisyphus-task", () => { userCategories: { "fallback-test": { model: "anthropic/claude-opus-4-6" }, }, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -2072,6 +2101,8 @@ describe("sisyphus-task", () => { manager: mockManager, client: mockClient, sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -2135,6 +2166,8 @@ describe("sisyphus-task", () => { userCategories: { ultrabrain: { model: "openai/gpt-5.3-codex" }, }, + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -2194,6 +2227,8 @@ describe("sisyphus-task", () => { manager: mockManager, client: mockClient, sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { @@ -3207,6 +3242,8 @@ describe("sisyphus-task", () => { manager: mockManager, client: mockClient, // no agentOverrides + connectedProvidersOverride: TEST_CONNECTED_PROVIDERS, + availableModelsOverride: createTestAvailableModels(), }) const toolContext = { diff --git a/src/tools/delegate-task/types.ts b/src/tools/delegate-task/types.ts index 4327bdced..13d1973a4 100644 --- a/src/tools/delegate-task/types.ts +++ b/src/tools/delegate-task/types.ts @@ -50,6 +50,15 @@ export interface DelegateTaskToolOptions { manager: BackgroundManager client: OpencodeClient directory: string + /** + * Test hook: bypass global cache reads (Bun runs tests in parallel). + * If provided, resolveCategoryExecution/resolveSubagentExecution uses this instead of reading from disk cache. + */ + connectedProvidersOverride?: string[] | null + /** + * Test hook: bypass fetchAvailableModels() by providing an explicit available model set. + */ + availableModelsOverride?: Set userCategories?: CategoriesConfig gitMasterConfig?: GitMasterConfig sisyphusJuniorModel?: string