From 699f709c3fceba1863eb74ddcd691a8c249f5e53 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:14:17 -0700 Subject: [PATCH] fix(recipes): let Codex model args override defaults (#8773) * fix(recipes): let Codex model args override defaults * fix(recipes): harden Codex model override forms --- src/shared/commit-message-plan.test.ts | 79 +++++++++++++++++++++++--- src/shared/commit-message-plan.ts | 77 ++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/shared/commit-message-plan.test.ts b/src/shared/commit-message-plan.test.ts index 2aa20a4cd63..29abb0fe03e 100644 --- a/src/shared/commit-message-plan.test.ts +++ b/src/shared/commit-message-plan.test.ts @@ -215,12 +215,51 @@ describe('planCommitMessageGeneration', () => { }) }) - it('appends per-action CLI arguments after the built-in model args for stdin agents', () => { + it.each([ + ['long option', '--model gpt-5.6-luna', ['--model', 'gpt-5.6-luna'], []], + ['short option', '-m gpt-5.6-luna', ['-m', 'gpt-5.6-luna'], []], + ['equals form', '--model=gpt-5.6-luna', ['--model=gpt-5.6-luna'], []], + ['attached short form', '-mgpt-5.6-luna', ['-mgpt-5.6-luna'], []], + [ + 'sibling arguments', + '--model gpt-5.6-luna --sandbox read-only', + ['--model', 'gpt-5.6-luna'], + ['--sandbox', 'read-only'] + ] + ])( + 'lets Codex recipe args override the generated model via %s', + (_, agentArgs, overrideArgs, trailingArgs) => { + const result = planCommitMessageGeneration( + { agentId: 'codex', model: 'gpt-5.4-mini', thinkingLevel: 'medium', agentArgs }, + 'PROMPT' + ) + + expect(result).toMatchObject({ + ok: true, + plan: { + args: [ + 'exec', + '--ephemeral', + '--skip-git-repo-check', + '-s', + 'read-only', + ...overrideArgs, + '-c', + 'model_reasoning_effort=medium', + ...trailingArgs + ], + stdinPayload: 'PROMPT' + } + }) + } + ) + + it('keeps Codex recipe arguments unchanged when they do not override the model', () => { const result = planCommitMessageGeneration( { agentId: 'codex', model: 'gpt-5.4-mini', - agentArgs: '--model gpt-5.5 --sandbox read-only' + agentArgs: '--sandbox workspace-write' }, 'PROMPT' ) @@ -236,12 +275,38 @@ describe('planCommitMessageGeneration', () => { 'read-only', '--model', 'gpt-5.4-mini', - '--model', - 'gpt-5.5', '--sandbox', - 'read-only' - ], - stdinPayload: 'PROMPT' + 'workspace-write' + ] + } + }) + }) + + it('keeps the generated Codex model when model-like text follows an option terminator', () => { + const result = planCommitMessageGeneration( + { + agentId: 'codex', + model: 'gpt-5.4-mini', + agentArgs: '-- --model literal' + }, + 'PROMPT' + ) + + expect(result).toMatchObject({ + ok: true, + plan: { + args: [ + 'exec', + '--ephemeral', + '--skip-git-repo-check', + '-s', + 'read-only', + '--model', + 'gpt-5.4-mini', + '--', + '--model', + 'literal' + ] } }) }) diff --git a/src/shared/commit-message-plan.ts b/src/shared/commit-message-plan.ts index bef816dd720..1141ecec0e7 100644 --- a/src/shared/commit-message-plan.ts +++ b/src/shared/commit-message-plan.ts @@ -68,6 +68,69 @@ function planAdditionalAgentArgs( return { ok: true, args: tokenized.tokens } } +const CODEX_MODEL_OPTION_ALIASES = ['--model', '-m'] as const + +function matchesOption(token: string, aliases: readonly string[]): boolean { + return aliases.some( + (alias) => + token === alias || + token.startsWith(`${alias}=`) || + (alias.startsWith('-') && + !alias.startsWith('--') && + token.startsWith(alias) && + token.length > alias.length) + ) +} + +function findOptionOccurrence( + tokens: string[], + aliases: readonly string[], + stopAtTerminator: boolean +): { index: number; consumed: number } | null { + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index] + if (stopAtTerminator && token === '--') { + break + } + if (!matchesOption(token, aliases)) { + continue + } + const nextToken = tokens[index + 1] + const consumesNext = + aliases.includes(token) && nextToken !== undefined && !nextToken.startsWith('-') + return { index, consumed: consumesNext ? 2 : 1 } + } + return null +} + +function applyRecipeOptionOverride(args: { + generatedArgs: string[] + recipeArgs: string[] + aliases: readonly string[] +}): { generatedArgs: string[]; recipeArgs: string[] } { + const recipeOption = findOptionOccurrence(args.recipeArgs, args.aliases, true) + const generatedOption = findOptionOccurrence(args.generatedArgs, args.aliases, false) + if (!recipeOption || !generatedOption) { + return { generatedArgs: args.generatedArgs, recipeArgs: args.recipeArgs } + } + + const overrideTokens = args.recipeArgs.slice( + recipeOption.index, + recipeOption.index + recipeOption.consumed + ) + return { + generatedArgs: [ + ...args.generatedArgs.slice(0, generatedOption.index), + ...overrideTokens, + ...args.generatedArgs.slice(generatedOption.index + generatedOption.consumed) + ], + recipeArgs: [ + ...args.recipeArgs.slice(0, recipeOption.index), + ...args.recipeArgs.slice(recipeOption.index + recipeOption.consumed) + ] + } +} + function insertAdditionalAgentArgs(args: { baseArgs: string[] agentArgs: string[] @@ -164,9 +227,19 @@ export function planCommitMessageGeneration( if (!agentArgs.ok) { return agentArgs } + // Why: Codex rejects repeated singleton model flags. Recipe CLI arguments + // are the more specific setting, so they replace Orca's generated model. + const overriddenArgs = + input.agentId === 'codex' + ? applyRecipeOptionOverride({ + generatedArgs: baseArgs, + recipeArgs: agentArgs.args, + aliases: CODEX_MODEL_OPTION_ALIASES + }) + : { generatedArgs: baseArgs, recipeArgs: agentArgs.args } const args = insertAdditionalAgentArgs({ - baseArgs, - agentArgs: agentArgs.args, + baseArgs: overriddenArgs.generatedArgs, + agentArgs: overriddenArgs.recipeArgs, promptDelivery: spec.promptDelivery, prompt: argvPrompt })