feat: add codex reasoning effort option (#297)

This commit is contained in:
ROOOO
2026-03-17 22:55:55 +08:00
committed by GitHub
parent caa76826f4
commit cb09f0b898
18 changed files with 107 additions and 6 deletions
+2 -1
View File
@@ -102,7 +102,7 @@ export class ApiMachineClient {
setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void { setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void {
this.rpcHandlerManager.registerHandler('spawn-happy-session', async (params: any) => { this.rpcHandlerManager.registerHandler('spawn-happy-session', async (params: any) => {
const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, yolo, token, sessionType, worktreeName } = params || {} const { directory, sessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, modelReasoningEffort, yolo, token, sessionType, worktreeName } = params || {}
if (!directory) { if (!directory) {
throw new Error('Directory is required') throw new Error('Directory is required')
@@ -116,6 +116,7 @@ export class ApiMachineClient {
approvedNewDirectoryCreation, approvedNewDirectoryCreation,
agent, agent,
model, model,
modelReasoningEffort,
yolo, yolo,
token, token,
sessionType, sessionType,
+1
View File
@@ -14,6 +14,7 @@ export interface EnhancedMode {
permissionMode: PermissionMode; permissionMode: PermissionMode;
model?: string; model?: string;
collaborationMode: CodexCollaborationMode; collaborationMode: CodexCollaborationMode;
modelReasoningEffort?: string;
} }
interface LoopOptions { interface LoopOptions {
+4
View File
@@ -21,6 +21,7 @@ export async function runCodex(opts: {
permissionMode?: PermissionMode; permissionMode?: PermissionMode;
resumeSessionId?: string; resumeSessionId?: string;
model?: string; model?: string;
modelReasoningEffort?: string;
}): Promise<void> { }): Promise<void> {
const workingDirectory = getInvokedCwd(); const workingDirectory = getInvokedCwd();
const startedBy = opts.startedBy ?? 'terminal'; const startedBy = opts.startedBy ?? 'terminal';
@@ -45,6 +46,7 @@ export async function runCodex(opts: {
const messageQueue = new MessageQueue2<EnhancedMode>((mode) => hashObject({ const messageQueue = new MessageQueue2<EnhancedMode>((mode) => hashObject({
permissionMode: mode.permissionMode, permissionMode: mode.permissionMode,
model: mode.model, model: mode.model,
modelReasoningEffort: mode.modelReasoningEffort,
collaborationMode: mode.collaborationMode collaborationMode: mode.collaborationMode
})); }));
@@ -53,6 +55,7 @@ export async function runCodex(opts: {
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default'; let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
let currentModel = opts.model; let currentModel = opts.model;
const currentModelReasoningEffort = opts.modelReasoningEffort;
let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default'; let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default';
const lifecycle = createRunnerLifecycle({ const lifecycle = createRunnerLifecycle({
@@ -105,6 +108,7 @@ export async function runCodex(opts: {
const enhancedMode: EnhancedMode = { const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default', permissionMode: messagePermissionMode ?? 'default',
model: currentModel, model: currentModel,
modelReasoningEffort: currentModelReasoningEffort,
collaborationMode: currentCollaborationMode collaborationMode: currentCollaborationMode
}; };
const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments); const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments);
@@ -64,6 +64,22 @@ describe('appServerConfig', () => {
}); });
}); });
it('passes model reasoning effort via thread config', () => {
const params = buildThreadStartParams({
mode: { permissionMode: 'default', modelReasoningEffort: 'xhigh', collaborationMode: 'default' },
mcpServers
});
expect(params.config).toEqual({
'mcp_servers.hapi': {
command: 'node',
args: ['mcp']
},
developer_instructions: codexSystemPrompt,
model_reasoning_effort: 'xhigh'
});
});
it('builds turn params with mode defaults', () => { it('builds turn params with mode defaults', () => {
const params = buildTurnStartParams({ const params = buildTurnStartParams({
threadId: 'thread-1', threadId: 'thread-1',
+2 -1
View File
@@ -84,7 +84,8 @@ export function buildThreadStartParams(args: {
} = resolveInstructions(args); } = resolveInstructions(args);
const configWithInstructions = { const configWithInstructions = {
...config, ...config,
developer_instructions: resolvedDeveloperInstructions developer_instructions: resolvedDeveloperInstructions,
...(args.mode.modelReasoningEffort ? { model_reasoning_effort: args.mode.modelReasoningEffort } : {})
}; };
const params: ThreadStartParams = { const params: ThreadStartParams = {
+7
View File
@@ -18,6 +18,7 @@ export const codexCommand: CommandDefinition = {
permissionMode?: CodexPermissionMode permissionMode?: CodexPermissionMode
resumeSessionId?: string resumeSessionId?: string
model?: string model?: string
modelReasoningEffort?: string
} = {} } = {}
const unknownArgs: string[] = [] const unknownArgs: string[] = []
@@ -44,6 +45,12 @@ export const codexCommand: CommandDefinition = {
} }
options.model = model options.model = model
unknownArgs.push('--model', model) unknownArgs.push('--model', model)
} else if (arg === '--model-reasoning-effort') {
const effort = commandArgs[++i]
if (!effort) {
throw new Error('Missing --model-reasoning-effort value')
}
options.modelReasoningEffort = effort
} else { } else {
unknownArgs.push(arg) unknownArgs.push(arg)
} }
+1
View File
@@ -6,6 +6,7 @@ export interface SpawnSessionOptions {
approvedNewDirectoryCreation?: boolean approvedNewDirectoryCreation?: boolean
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
model?: string model?: string
modelReasoningEffort?: string
yolo?: boolean yolo?: boolean
token?: string token?: string
sessionType?: 'simple' | 'worktree' sessionType?: 'simple' | 'worktree'
+3
View File
@@ -361,6 +361,9 @@ export async function startRunner(): Promise<void> {
if (options.model && agent !== 'opencode') { if (options.model && agent !== 'opencode') {
args.push('--model', options.model); args.push('--model', options.model);
} }
if (options.modelReasoningEffort && agent === 'codex') {
args.push('--model-reasoning-effort', options.modelReasoningEffort);
}
if (yolo) { if (yolo) {
args.push('--yolo'); args.push('--yolo');
} }
+2 -1
View File
@@ -109,6 +109,7 @@ export class RpcGateway {
directory: string, directory: string,
agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude', agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude',
model?: string, model?: string,
modelReasoningEffort?: string,
yolo?: boolean, yolo?: boolean,
sessionType?: 'simple' | 'worktree', sessionType?: 'simple' | 'worktree',
worktreeName?: string, worktreeName?: string,
@@ -118,7 +119,7 @@ export class RpcGateway {
const result = await this.machineRpc( const result = await this.machineRpc(
machineId, machineId,
'spawn-happy-session', 'spawn-happy-session',
{ type: 'spawn-in-directory', directory, agent, model, yolo, sessionType, worktreeName, resumeSessionId } { type: 'spawn-in-directory', directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId }
) )
if (result && typeof result === 'object') { if (result && typeof result === 'object') {
const obj = result as Record<string, unknown> const obj = result as Record<string, unknown>
+2 -1
View File
@@ -310,12 +310,13 @@ export class SyncEngine {
directory: string, directory: string,
agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude', agent: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' = 'claude',
model?: string, model?: string,
modelReasoningEffort?: string,
yolo?: boolean, yolo?: boolean,
sessionType?: 'simple' | 'worktree', sessionType?: 'simple' | 'worktree',
worktreeName?: string, worktreeName?: string,
resumeSessionId?: string resumeSessionId?: string
): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> { ): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> {
return await this.rpcGateway.spawnSession(machineId, directory, agent, model, yolo, sessionType, worktreeName, resumeSessionId) return await this.rpcGateway.spawnSession(machineId, directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId)
} }
async resumeSession(sessionId: string, namespace: string): Promise<ResumeSessionResult> { async resumeSession(sessionId: string, namespace: string): Promise<ResumeSessionResult> {
+2
View File
@@ -8,6 +8,7 @@ const spawnBodySchema = z.object({
directory: z.string().min(1), directory: z.string().min(1),
agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(), agent: z.enum(['claude', 'codex', 'cursor', 'gemini', 'opencode']).optional(),
model: z.string().optional(), model: z.string().optional(),
modelReasoningEffort: z.string().optional(),
yolo: z.boolean().optional(), yolo: z.boolean().optional(),
sessionType: z.enum(['simple', 'worktree']).optional(), sessionType: z.enum(['simple', 'worktree']).optional(),
worktreeName: z.string().optional() worktreeName: z.string().optional()
@@ -54,6 +55,7 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho
parsed.data.directory, parsed.data.directory,
parsed.data.agent, parsed.data.agent,
parsed.data.model, parsed.data.model,
parsed.data.modelReasoningEffort,
parsed.data.yolo, parsed.data.yolo,
parsed.data.sessionType, parsed.data.sessionType,
parsed.data.worktreeName parsed.data.worktreeName
+2 -1
View File
@@ -381,13 +381,14 @@ export class ApiClient {
directory: string, directory: string,
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode', agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode',
model?: string, model?: string,
modelReasoningEffort?: string,
yolo?: boolean, yolo?: boolean,
sessionType?: 'simple' | 'worktree', sessionType?: 'simple' | 'worktree',
worktreeName?: string worktreeName?: string
): Promise<SpawnResponse> { ): Promise<SpawnResponse> {
return await this.request<SpawnResponse>(`/api/machines/${encodeURIComponent(machineId)}/spawn`, { return await this.request<SpawnResponse>(`/api/machines/${encodeURIComponent(machineId)}/spawn`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ directory, agent, model, yolo, sessionType, worktreeName }) body: JSON.stringify({ directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName })
}) })
} }
@@ -0,0 +1,37 @@
import type { AgentType, CodexReasoningEffort } from './types'
import { CODEX_REASONING_EFFORT_OPTIONS } from './types'
import { useTranslation } from '@/lib/use-translation'
export function ReasoningEffortSelector(props: {
agent: AgentType
value: CodexReasoningEffort
isDisabled: boolean
onChange: (value: CodexReasoningEffort) => void
}) {
const { t } = useTranslation()
if (props.agent !== 'codex') {
return null
}
return (
<div className="flex flex-col gap-1.5 px-3 py-3">
<label className="text-xs font-medium text-[var(--app-hint)]">
{t('newSession.reasoningEffort')}{' '}
<span className="font-normal">({t('newSession.model.optional')})</span>
</label>
<select
value={props.value}
onChange={(e) => props.onChange(e.target.value as CodexReasoningEffort)}
disabled={props.isDisabled}
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
>
{CODEX_REASONING_EFFORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
)
}
+13 -1
View File
@@ -7,12 +7,13 @@ import { useSessions } from '@/hooks/queries/useSessions'
import { useActiveSuggestions, type Suggestion } from '@/hooks/useActiveSuggestions' import { useActiveSuggestions, type Suggestion } from '@/hooks/useActiveSuggestions'
import { useDirectorySuggestions } from '@/hooks/useDirectorySuggestions' import { useDirectorySuggestions } from '@/hooks/useDirectorySuggestions'
import { useRecentPaths } from '@/hooks/useRecentPaths' import { useRecentPaths } from '@/hooks/useRecentPaths'
import type { AgentType, SessionType } from './types' import type { AgentType, CodexReasoningEffort, SessionType } from './types'
import { ActionButtons } from './ActionButtons' import { ActionButtons } from './ActionButtons'
import { AgentSelector } from './AgentSelector' import { AgentSelector } from './AgentSelector'
import { DirectorySection } from './DirectorySection' import { DirectorySection } from './DirectorySection'
import { MachineSelector } from './MachineSelector' import { MachineSelector } from './MachineSelector'
import { ModelSelector } from './ModelSelector' import { ModelSelector } from './ModelSelector'
import { ReasoningEffortSelector } from './ReasoningEffortSelector'
import { import {
loadPreferredAgent, loadPreferredAgent,
loadPreferredYoloMode, loadPreferredYoloMode,
@@ -43,6 +44,7 @@ export function NewSession(props: {
const [pathExistence, setPathExistence] = useState<Record<string, boolean>>({}) const [pathExistence, setPathExistence] = useState<Record<string, boolean>>({})
const [agent, setAgent] = useState<AgentType>(loadPreferredAgent) const [agent, setAgent] = useState<AgentType>(loadPreferredAgent)
const [model, setModel] = useState('auto') const [model, setModel] = useState('auto')
const [modelReasoningEffort, setModelReasoningEffort] = useState<CodexReasoningEffort>('default')
const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode) const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode)
const [sessionType, setSessionType] = useState<SessionType>('simple') const [sessionType, setSessionType] = useState<SessionType>('simple')
const [worktreeName, setWorktreeName] = useState('') const [worktreeName, setWorktreeName] = useState('')
@@ -220,11 +222,15 @@ export function NewSession(props: {
setError(null) setError(null)
try { try {
const resolvedModel = model !== 'auto' && agent !== 'opencode' ? model : undefined const resolvedModel = model !== 'auto' && agent !== 'opencode' ? model : undefined
const resolvedModelReasoningEffort = agent === 'codex' && modelReasoningEffort !== 'default'
? modelReasoningEffort
: undefined
const result = await spawnSession({ const result = await spawnSession({
machineId, machineId,
directory: directory.trim(), directory: directory.trim(),
agent, agent,
model: resolvedModel, model: resolvedModel,
modelReasoningEffort: resolvedModelReasoningEffort,
yolo: yoloMode, yolo: yoloMode,
sessionType, sessionType,
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
@@ -294,6 +300,12 @@ export function NewSession(props: {
isDisabled={isFormDisabled} isDisabled={isFormDisabled}
onModelChange={setModel} onModelChange={setModel}
/> />
<ReasoningEffortSelector
agent={agent}
value={modelReasoningEffort}
isDisabled={isFormDisabled}
onChange={setModelReasoningEffort}
/>
<YoloToggle <YoloToggle
yoloMode={yoloMode} yoloMode={yoloMode}
isDisabled={isFormDisabled} isDisabled={isFormDisabled}
+9
View File
@@ -1,5 +1,6 @@
export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
export type SessionType = 'simple' | 'worktree' export type SessionType = 'simple' | 'worktree'
export type CodexReasoningEffort = 'default' | 'low' | 'medium' | 'high' | 'xhigh'
export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]> = { export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]> = {
claude: [ claude: [
@@ -27,3 +28,11 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
], ],
opencode: [], opencode: [],
} }
export const CODEX_REASONING_EFFORT_OPTIONS: { value: CodexReasoningEffort; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'xhigh', label: 'XHigh' },
]
@@ -8,6 +8,7 @@ type SpawnInput = {
directory: string directory: string
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
model?: string model?: string
modelReasoningEffort?: string
yolo?: boolean yolo?: boolean
sessionType?: 'simple' | 'worktree' sessionType?: 'simple' | 'worktree'
worktreeName?: string worktreeName?: string
@@ -30,6 +31,7 @@ export function useSpawnSession(api: ApiClient | null): {
input.directory, input.directory,
input.agent, input.agent,
input.model, input.model,
input.modelReasoningEffort,
input.yolo, input.yolo,
input.sessionType, input.sessionType,
input.worktreeName input.worktreeName
+1
View File
@@ -104,6 +104,7 @@ export default {
'newSession.agent': 'Agent', 'newSession.agent': 'Agent',
'newSession.model': 'Model', 'newSession.model': 'Model',
'newSession.model.optional': 'optional', 'newSession.model.optional': 'optional',
'newSession.reasoningEffort': 'Reasoning effort',
'newSession.yolo': 'YOLO mode', 'newSession.yolo': 'YOLO mode',
'newSession.yolo.title': 'Bypass approvals and sandbox', 'newSession.yolo.title': 'Bypass approvals and sandbox',
'newSession.yolo.desc': 'Uses dangerous agent flags when spawning.', 'newSession.yolo.desc': 'Uses dangerous agent flags when spawning.',
+1
View File
@@ -106,6 +106,7 @@ export default {
'newSession.agent': '代理', 'newSession.agent': '代理',
'newSession.model': '模型', 'newSession.model': '模型',
'newSession.model.optional': '可选', 'newSession.model.optional': '可选',
'newSession.reasoningEffort': '推理强度',
'newSession.yolo': 'YOLO 模式', 'newSession.yolo': 'YOLO 模式',
'newSession.yolo.title': '跳过审批和沙箱', 'newSession.yolo.title': '跳过审批和沙箱',
'newSession.yolo.desc': '启动时使用危险的代理标志。', 'newSession.yolo.desc': '启动时使用危险的代理标志。',