fix(recipes): let Codex model args override defaults (#8773)

* fix(recipes): let Codex model args override defaults

* fix(recipes): harden Codex model override forms
This commit is contained in:
Neil
2026-07-14 15:14:17 -07:00
committed by GitHub
parent a5cea59933
commit 699f709c3f
2 changed files with 147 additions and 9 deletions
+72 -7
View File
@@ -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'
]
}
})
})
+75 -2
View File
@@ -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
})