diff --git a/src/backends/adapter.ts b/src/backends/adapter.ts index 0efaeda2..3f329003 100644 --- a/src/backends/adapter.ts +++ b/src/backends/adapter.ts @@ -133,6 +133,7 @@ async function buildExecutionPlan( gitHubToken: string | undefined, isGitHubAck: boolean, engineId: string, + engine: AgentEngine, ): Promise< Omit & { reviewSidecarPath?: string; @@ -173,7 +174,7 @@ async function buildExecutionPlan( const { systemPrompt, taskPrompt: taskPromptOverride, - model, + model: rawModel, maxIterations, contextFiles, } = await resolveModelConfig({ @@ -186,6 +187,9 @@ async function buildExecutionPlan( agentInput: input, }); + // Allow the engine to resolve/validate the model string (e.g. strip provider prefix) + const model = engine.resolveModel ? engine.resolveModel(rawModel) : rawModel; + const profile = await getAgentProfile(agentType); // Use profile to fetch agent-specific context injections @@ -412,6 +416,7 @@ async function resolvePartialExecutionPlan( gitHubToken, isGitHubAck, engine.definition.id, + engine, ); const partialInput = gitHubToken diff --git a/src/backends/claude-code/index.ts b/src/backends/claude-code/index.ts index 4e34c1b4..2ff9e22f 100644 --- a/src/backends/claude-code/index.ts +++ b/src/backends/claude-code/index.ts @@ -456,6 +456,10 @@ export class ClaudeCodeEngine implements AgentEngine { return true; } + resolveModel(cascadeModel: string): string { + return resolveClaudeModel(cascadeModel); + } + async execute(input: AgentExecutionPlan): Promise { const startTime = Date.now(); const systemPrompt = buildSystemPrompt(input.systemPrompt, input.availableTools); @@ -464,6 +468,11 @@ export class ClaudeCodeEngine implements AgentEngine { input.contextInjections, input.repoDir, ); + // Resolve model again here for backward compatibility: execute() may be called + // directly (e.g. in tests) without going through the adapter, so we cannot rely + // solely on the adapter's engine.resolveModel() pre-resolution. Since + // resolveClaudeModel() is idempotent, calling it twice via the normal adapter path + // is safe. const model = resolveClaudeModel(input.model); input.logWriter('INFO', 'Starting Claude Code SDK execution', { diff --git a/src/backends/codex/index.ts b/src/backends/codex/index.ts index 117db9b9..67123619 100644 --- a/src/backends/codex/index.ts +++ b/src/backends/codex/index.ts @@ -480,6 +480,10 @@ export class CodexEngine implements AgentEngine { return true; } + resolveModel(cascadeModel: string): string { + return resolveCodexModel(cascadeModel); + } + async execute(input: AgentExecutionPlan): Promise { const startTime = Date.now(); const systemPrompt = buildSystemPrompt(input.systemPrompt, input.availableTools); @@ -488,6 +492,11 @@ export class CodexEngine implements AgentEngine { input.contextInjections, input.repoDir, ); + // Resolve model again here for backward compatibility: execute() may be called + // directly (e.g. in tests) without going through the adapter, so we cannot rely + // solely on the adapter's engine.resolveModel() pre-resolution. Since + // resolveCodexModel() is idempotent, calling it twice via the normal adapter path + // is safe. const model = resolveCodexModel(input.model); const settings = resolveCodexSettings(input.project, input.nativeToolCapabilities); assertHeadlessCodexSettings(settings); diff --git a/src/backends/opencode/index.ts b/src/backends/opencode/index.ts index f07a742b..971c74b1 100644 --- a/src/backends/opencode/index.ts +++ b/src/backends/opencode/index.ts @@ -801,9 +801,18 @@ export class OpenCodeEngine implements AgentEngine { return true; } + resolveModel(cascadeModel: string): string { + return resolveOpenCodeModel(cascadeModel); + } + async execute(input: AgentExecutionPlan): Promise { const settings = resolveOpenCodeSettings(input.project); const agent = 'build' as const; + // Resolve model again here for backward compatibility: execute() may be called + // directly (e.g. in tests) without going through the adapter, so we cannot rely + // solely on the adapter's engine.resolveModel() pre-resolution. Since + // resolveOpenCodeModel() is idempotent, calling it twice via the normal adapter path + // is safe. const model = resolveOpenCodeModel(input.model); const config = buildConfig(input, model, settings); const { prompt: taskPrompt, hasOffloadedContext } = await buildTaskPrompt( diff --git a/src/backends/types.ts b/src/backends/types.ts index 9b510102..b8bef31e 100644 --- a/src/backends/types.ts +++ b/src/backends/types.ts @@ -147,4 +147,10 @@ export interface AgentEngine { execute(input: AgentExecutionPlan): Promise; supportsAgentType(agentType: string): boolean; + /** + * Optionally resolve a CASCADE model string to the engine-specific model identifier. + * Engines that need model validation (e.g., Claude Code, Codex) implement this method. + * Engines that pass the model through unchanged (e.g., LLMist) do not need to implement it. + */ + resolveModel?(cascadeModel: string): string; }