Files
orca/src/shared/commit-message-agent-specs-primary.ts
T
Nawapat BuakoetandJinjing 1d010ce2f8 fix(source-control): default Codex to GPT-5.6 Terra low (#24495)
* fix(source-control): use Codex's configured model by default

Source Control AI pinned Codex to gpt-5.5, which Codex retires on
2026-10-14. Any path that still resolves to that slug would then break
commit-message and PR-field generation.

Follow the Antigravity precedent (#21606): add a "Config default" entry
for Codex, make it the default, and omit --model when it is selected so
`codex exec` uses the model from the user's Codex config or Codex's own
default. Explicit model choices still pass --model.

Older remote servers would still build `--model default`, so advertise
git.codex-configured-model.v1 and have clients refuse the sentinel for
servers without it, the same way Antigravity is gated. The gate now
covers both agents, and the two capabilities live in their own module
because protocol-version.ts is at the max-lines limit.

Written with AI assistance (Claude Code).

Fixes #24481

* fix(source-control): honor -m, repo overrides and low effort for Codex

Review on #24495 found three gaps in the configured-model change.
The remote compatibility gate only recognized --model, so a recipe
passing Codex's -m short flag was rejected on older servers. The gate
also ignored the repository's per-operation model override that the
server applies. And moving Codex to Config default dropped the low
reasoning effort the pinned model used, which would change cost and
latency for users whose Codex config sets a higher effort.

* fix(source-control): match gate repo and model checks to the server

Repo ids can repeat across hosts, so the configured-model gate now reads
the repo row for the worktree's host instead of the first id match, the
same row the server applies. A recipe passing --model default or
-m default no longer counts as an explicit model, since an older server
still forwards that sentinel to the Codex CLI.

* fix(source-control): read only the worktree host's repo row in the gate

A worktree that names its own host must not fall back to the runtime
host's repo row, since the server applies the row for the worktree's
host. Also cover an environment id that needs URL encoding.

* fix(source-control): default Codex to GPT-5.6 Terra low

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-10-03 10:14:05 -07:00

248 lines
7.9 KiB
TypeScript

import type { TuiAgent } from './tui-agent'
import type {
CommitMessageAgentSpec,
CommitMessageModel,
ThinkingLevel
} from './commit-message-agent-spec'
import { CLAUDE_MODEL_LIST_ARGS, CLAUDE_MODEL_LIST_STDIN } from './claude-model-list-probe'
type PrimaryAgentSpecDeps = {
CLAUDE_THINKING_LEVELS: ThinkingLevel[]
OPENAI_THINKING_LEVELS: ThinkingLevel[]
parseClaudeModels: (stdout: string) => CommitMessageModel[]
parseCodexModels: (stdout: string) => CommitMessageModel[]
parseLineModels: (stdout: string) => CommitMessageModel[]
parsePiModels: (stdout: string) => CommitMessageModel[]
withOpenAiThinking: (
id: string
) => Pick<CommitMessageModel, 'thinkingLevels' | 'defaultThinkingLevel'>
}
export function buildPrimaryCommitMessageAgentSpecs({
CLAUDE_THINKING_LEVELS,
OPENAI_THINKING_LEVELS,
parseClaudeModels,
parseCodexModels,
parseLineModels,
parsePiModels,
withOpenAiThinking
}: PrimaryAgentSpecDeps): Partial<Record<TuiAgent, CommitMessageAgentSpec>> {
return {
claude: {
id: 'claude',
label: 'Claude',
binary: 'claude',
// Why: diffs can be large and `claude -p` reads from stdin natively when no
// positional prompt is provided.
promptDelivery: 'stdin',
buildArgs: ({ model, thinkingLevel }) => [
'-p',
'--output-format',
'text',
'--model',
model,
'--permission-mode',
'plan',
...(thinkingLevel ? ['--effort', thinkingLevel] : [])
],
modelSource: 'dynamic',
// Why: the Claude CLI has no listing subcommand; one list_models control
// request over --print stream-json returns the /model picker catalog.
// Older CLIs answer with a control error and exit 0, keeping the fallback.
modelDiscovery: {
binary: 'claude',
args: [...CLAUDE_MODEL_LIST_ARGS],
stdinPayload: CLAUDE_MODEL_LIST_STDIN,
parse: parseClaudeModels
},
models: [
{
// Why: Claude Code aliases track the account/provider's supported
// model IDs; hardcoded version IDs can be rejected by Bedrock/Vertex.
id: 'haiku',
label: 'Haiku'
},
{
id: 'sonnet',
label: 'Sonnet',
thinkingLevels: CLAUDE_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'opus',
label: 'Opus',
thinkingLevels: CLAUDE_THINKING_LEVELS,
defaultThinkingLevel: 'low'
}
],
defaultModelId: 'sonnet'
},
codex: {
id: 'codex',
label: 'Codex',
binary: 'codex',
// Why: `codex exec` reads stdin when no prompt arg is supplied. Commit
// prompts include large staged diffs, so argv would exceed Windows and
// some SSH/POSIX command-line limits.
promptDelivery: 'stdin',
buildArgs: ({ model, thinkingLevel }) => [
'exec',
// Why: commit-message generation needs text only, not a persisted agent
// session or workspace writes. Match the safe git-text mode used by
// local-first coding agents.
'--ephemeral',
'--skip-git-repo-check',
'-s',
'read-only',
'--model',
model,
...(thinkingLevel ? ['-c', `model_reasoning_effort=${thinkingLevel}`] : [])
],
// `-c` is intentionally absent: Codex accepts repeated overrides.
singletonOptions: [['--model', '-m']],
modelSource: 'dynamic',
modelDiscovery: {
binary: 'codex',
args: ['debug', 'models'],
parse: parseCodexModels
},
// Why: ordered to match the official `codex` model picker — descending
// by version so the frontier model lands on top and legacy models trail.
models: [
{
id: 'gpt-5.6-terra',
label: 'GPT-5.6 Terra',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'gpt-5.5',
label: 'GPT-5.5',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'gpt-5.4',
label: 'GPT-5.4',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'gpt-5.4-mini',
label: 'GPT-5.4 Mini',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'gpt-5.3-codex',
label: 'GPT-5.3 Codex',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
// Why: Codex's Spark variant accepts `model_reasoning_effort` (the
// CLI banner reports "reasoning effort: medium" by default); the
// gating that surfaces "model not supported" is on the account
// tier, not the effort flag.
id: 'gpt-5.3-codex-spark',
label: 'GPT-5.3 Codex Spark',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
},
{
id: 'gpt-5.2',
label: 'GPT-5.2',
thinkingLevels: OPENAI_THINKING_LEVELS,
defaultThinkingLevel: 'low'
}
],
defaultModelId: 'gpt-5.6-terra'
},
opencode: {
id: 'opencode',
label: 'OpenCode',
binary: 'opencode',
// Why: Source Control AI prompts can include large staged diffs; OpenCode
// accepts the prompt on stdin, which avoids cross-platform argv limits.
promptDelivery: 'stdin',
buildArgs: ({ model, thinkingLevel }) => [
'run',
...(model && model !== 'default' ? ['--model', model] : []),
'--agent',
'build',
'--format',
'json',
...(thinkingLevel ? ['--variant', thinkingLevel] : [])
],
singletonOptions: [['--model', '-m'], ['--agent'], ['--format'], ['--variant']],
modelSource: 'dynamic',
modelDiscovery: { binary: 'opencode', args: ['models'], parse: parseLineModels },
models: [
{ id: 'default', label: 'Config default' },
{
id: 'opencode/gpt-5.4-mini',
label: 'OpenCode GPT 5.4 Mini',
...withOpenAiThinking('gpt-5.4-mini')
}
],
defaultModelId: 'default'
},
opencode2: {
id: 'opencode2',
label: 'OpenCode 2',
binary: 'opencode2',
promptDelivery: 'stdin',
buildArgs: ({ model, thinkingLevel }) => [
'run',
...(model && model !== 'default'
? ['--model', thinkingLevel ? `${model}#${thinkingLevel}` : model]
: []),
'--agent',
'build',
'--format',
'json'
],
singletonOptions: [['--model', '-m'], ['--agent'], ['--format']],
modelSource: 'dynamic',
modelDiscovery: { binary: 'opencode2', args: ['models'], parse: parseLineModels },
models: [
{ id: 'default', label: 'Config default' },
{
id: 'opencode/gpt-5.4-mini',
label: 'OpenCode GPT 5.4 Mini',
...withOpenAiThinking('gpt-5.4-mini')
}
],
defaultModelId: 'default'
},
pi: {
id: 'pi',
label: 'Pi',
binary: 'pi',
promptDelivery: 'stdin',
buildArgs: ({ model, thinkingLevel }) => [
'--print',
'--no-session',
'--no-tools',
'--no-skills',
'--no-context-files',
'--mode',
'text',
...(model && model !== 'default' ? ['--model', model] : []),
...(thinkingLevel ? ['--thinking', thinkingLevel] : [])
],
modelSource: 'dynamic',
modelDiscovery: { binary: 'pi', args: ['--list-models'], parse: parsePiModels },
models: [
{
// Why: the unqualified choice lets Pi use its configured provider and
// avoids forcing GitHub Copilot credentials during automation.
id: 'default',
label: 'Config default'
}
],
defaultModelId: 'default'
}
}
}