From 45db24bacfb6564731c34a54e5ebed3ba33b2fd7 Mon Sep 17 00:00:00 2001 From: Michael Grundberg Date: Thu, 1 Oct 2026 16:59:46 +0200 Subject: [PATCH] feat(codex): start a codex session at a chosen reasoning effort codexConfig takes a `reasoningEffort`, one of the levels codex accepts, and the session starts with `--config model_reasoning_effort=`. The registry declares one literal per level, gated on the enum, because an argv token cannot splice a value into a literal. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/config/cli-registry/stock.ts | 10 ++++++ src/types/session.ts | 12 +++++++ src/web/schemas.ts | 2 ++ test/cli-registry-spawn-golden.test.ts | 13 ++++++++ test/codex-reasoning-effort-schema.test.ts | 38 ++++++++++++++++++++++ 5 files changed, 75 insertions(+) create mode 100644 test/codex-reasoning-effort-schema.test.ts diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index 465da26a..87651161 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -11,6 +11,7 @@ */ import type { CliEntry } from './types.js'; +import { CODEX_REASONING_EFFORTS } from '../../types/session.js'; const HOME_DIRS = { local: '~/.local/bin', @@ -532,6 +533,7 @@ const CODEX: CliEntry = { bypassApprovals: { type: 'bool' }, animations: { type: 'bool' }, model: { type: 'token', pattern: 'model' }, + reasoningEffort: { type: 'enum', values: [...CODEX_REASONING_EFFORTS] }, resumeId: { type: 'token', pattern: 'id' }, }, variants: [ @@ -543,6 +545,14 @@ const CODEX: CliEntry = { { flag: '--config', value: 'tui.animations=true', when: { param: 'animations', is: true } }, { flag: '--config', value: 'tui.animations=false', when: { param: 'animations', is: false } }, { flag: '--model', valueFrom: 'model', when: { param: 'model', state: 'set' } }, + // One literal per level: an argv token cannot splice a value into a literal, and + // `model_reasoning_effort=` is a single `--config` value. The enum above is + // what admits a level, so an unknown one emits nothing. + ...CODEX_REASONING_EFFORTS.map((level) => ({ + flag: '--config', + value: `model_reasoning_effort=${level}`, + when: { param: 'reasoningEffort', is: level }, + })), { lit: 'resume', when: { param: 'resumeId', state: 'set' } }, { valueFrom: 'resumeId', when: { param: 'resumeId', state: 'set' } }, ], diff --git a/src/types/session.ts b/src/types/session.ts index 49249aff..1b1ae9b7 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -377,6 +377,16 @@ export function isEffortLevel(value: string | undefined): value is EffortLevel { return value !== undefined && (EFFORT_LEVELS as readonly string[]).includes(value); } +/** + * Reasoning effort levels codex accepts as `model_reasoning_effort` (codex-cli 0.154.0). + * Which of them a given model honours is codex's business; Codeman only keeps the value + * to a known word, since it lands in the launch argv. + */ +export const CODEX_REASONING_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'] as const; + +/** Codex reasoning effort for a session, passed as `--config model_reasoning_effort=` */ +export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number]; + /** OpenCode session configuration */ export interface OpenCodeConfig { /** Model identifier (e.g., "anthropic/claude-sonnet-4-5", "openai/gpt-5.2", "ollama/codellama") */ @@ -398,6 +408,8 @@ export type CodexRenderMode = 'hybrid'; export interface CodexConfig { /** Model identifier (e.g., "gpt-5", "o4-mini"). Passed via --model. */ model?: string; + /** Reasoning effort for this session. Passed via --config model_reasoning_effort=. */ + reasoningEffort?: CodexReasoningEffort; /** Resume a previous codex conversation by session id (passed via --resume) */ resumeSessionId?: string; /** Bypass approval prompts (passes --dangerously-bypass-approvals-and-sandbox) */ diff --git a/src/web/schemas.ts b/src/web/schemas.ts index c12a83a9..e73d7e85 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -18,6 +18,7 @@ import { MIN_TERMINAL_SCROLLBACK_LINES, } from '../config/terminal-history.js'; import { MAX_EDITABLE_BYTES } from '../config/file-editing.js'; +import { CODEX_REASONING_EFFORTS } from '../types/session.js'; import { MIN_MATCH_LENGTH, MAX_MATCH_LENGTH } from '../config/agent-wait.js'; import { MAX_WAKE_MACS } from '../config/remote-wake-limits.js'; import { MAX_INPUT_LENGTH } from '../config/terminal-limits.js'; @@ -298,6 +299,7 @@ const CodexConfigSchema = z .max(100) .regex(/^[a-zA-Z0-9._\-/]+$/) .optional(), + reasoningEffort: z.enum(CODEX_REASONING_EFFORTS).optional(), resumeSessionId: z .string() .max(100) diff --git a/test/cli-registry-spawn-golden.test.ts b/test/cli-registry-spawn-golden.test.ts index 72efee78..3883018c 100644 --- a/test/cli-registry-spawn-golden.test.ts +++ b/test/cli-registry-spawn-golden.test.ts @@ -24,6 +24,7 @@ import { describe, it, expect } from 'vitest'; import { getCli } from '../src/config/cli-registry/registry.js'; import { buildSpawnCommandFromRegistry, type SpawnBridgeOptions } from '../src/session-cli-registry-bridge.js'; +import { CODEX_REASONING_EFFORTS } from '../src/types/session.js'; /** A fixed session id, so `--session-id` is stable across runs. */ const SID = '0f9c2b14-1111-2222-3333-444455556666'; @@ -131,6 +132,18 @@ describe('codex', () => { it('resumes with a POSITIONAL subcommand, not a flag', () => { expect(cx({ model: 'gpt-5', resumeSessionId: 'roll_42' })).toBe('codex --model gpt-5 resume roll_42'); }); + + it('sends reasoning effort as one model_reasoning_effort config value, for every level', () => { + for (const level of CODEX_REASONING_EFFORTS) { + expect(cx({ reasoningEffort: level })).toBe(`codex --config model_reasoning_effort=${level}`); + } + }); + + it('keeps reasoning effort ahead of the resume subcommand', () => { + expect(cx({ model: 'gpt-5', reasoningEffort: 'high', resumeSessionId: 'roll_42' })).toBe( + 'codex --model gpt-5 --config model_reasoning_effort=high resume roll_42' + ); + }); }); describe('gemini', () => { diff --git a/test/codex-reasoning-effort-schema.test.ts b/test/codex-reasoning-effort-schema.test.ts new file mode 100644 index 00000000..a216472f --- /dev/null +++ b/test/codex-reasoning-effort-schema.test.ts @@ -0,0 +1,38 @@ +/** + * @fileoverview `codexConfig.reasoningEffort` on the create routes. + * + * The level becomes part of a `--config model_reasoning_effort=` launch token, so the + * schema admits only the words codex knows; anything else fails the request rather than + * reaching the argv. + */ + +import { describe, it, expect } from 'vitest'; +import { CreateSessionSchema, QuickStartSchema } from '../src/web/schemas.js'; +import { CODEX_REASONING_EFFORTS } from '../src/types/session.js'; + +describe('codexConfig.reasoningEffort', () => { + it('accepts every level codex knows on both create routes', () => { + for (const level of CODEX_REASONING_EFFORTS) { + const created = CreateSessionSchema.parse({ + workingDir: '/tmp', + mode: 'codex', + codexConfig: { reasoningEffort: level }, + }); + expect(created.codexConfig?.reasoningEffort).toBe(level); + const quick = QuickStartSchema.parse({ + caseName: 'work', + mode: 'codex', + codexConfig: { reasoningEffort: level }, + }); + expect(quick.codexConfig?.reasoningEffort).toBe(level); + } + }); + + it('rejects a level codex does not know, and anything shaped like shell', () => { + for (const reasoningEffort of ['bogus', 'HIGH', 'high; rm -rf /', '']) { + expect(() => + CreateSessionSchema.parse({ workingDir: '/tmp', mode: 'codex', codexConfig: { reasoningEffort } }) + ).toThrow(); + } + }); +});