diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 4ddd7d49..ce87fb00 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -10,6 +10,7 @@ import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; import type { ResponseItem } from "openai/resources/responses/responses"; +import type { ReasoningEffort } from "openai/resources.mjs"; import App from "./app"; import { runSinglePass } from "./cli-singlepass"; @@ -160,6 +161,12 @@ const cli = meow( "Disable truncation of command stdout/stderr messages (show everything)", aliases: ["no-truncate"], }, + reasoning: { + type: "string", + description: "Set the reasoning effort level (low, medium, high)", + choices: ["low", "medium", "high"], + default: "high", + }, // Notification notify: { type: "boolean", @@ -292,6 +299,8 @@ config = { ...config, model: model ?? config.model, notify: Boolean(cli.flags.notify), + reasoningEffort: + (cli.flags.reasoning as ReasoningEffort | undefined) ?? "high", flexMode: Boolean(cli.flags.flexMode), provider, disableResponseStorage: diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index 3ed8c9f1..53da6979 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -676,7 +676,7 @@ export class AgentLoop { try { let reasoning: Reasoning | undefined; if (this.model.startsWith("o")) { - reasoning = { effort: "high" }; + reasoning = { effort: this.config.reasoningEffort ?? "high" }; if (this.model === "o3" || this.model === "o4-mini") { reasoning.summary = "auto"; } diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 6d7d682b..fb2d19c5 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -7,6 +7,7 @@ // compiled `dist/` output used by the published CLI. import type { FullAutoErrorMode } from "./auto-approval-mode.js"; +import type { ReasoningEffort } from "openai/resources.mjs"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { log } from "./logger/log.js"; @@ -62,6 +63,8 @@ export const OPENAI_TIMEOUT_MS = parseInt(process.env["OPENAI_TIMEOUT_MS"] || "0", 10) || undefined; export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || ""; export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || ""; + +export const DEFAULT_REASONING_EFFORT = "high"; export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || ""; export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || ""; @@ -142,6 +145,9 @@ export type StoredConfig = { saveHistory?: boolean; sensitivePatterns?: Array; }; + /** User-defined safe commands */ + safeCommands?: Array; + reasoningEffort?: ReasoningEffort; }; // Minimal config written on first run. An *empty* model string ensures that @@ -165,6 +171,7 @@ export type AppConfig = { approvalMode?: AutoApprovalMode; fullAutoErrorMode?: FullAutoErrorMode; memory?: MemoryConfig; + reasoningEffort?: ReasoningEffort; /** Whether to enable desktop notifications for responses */ notify?: boolean; @@ -366,6 +373,7 @@ export const loadConfig = ( notify: storedConfig.notify === true, approvalMode: storedConfig.approvalMode, disableResponseStorage: storedConfig.disableResponseStorage ?? false, + reasoningEffort: storedConfig.reasoningEffort, }; // ----------------------------------------------------------------------- @@ -480,6 +488,7 @@ export const saveConfig = ( provider: config.provider, providers: config.providers, approvalMode: config.approvalMode, + reasoningEffort: config.reasoningEffort, }; // Add history settings if they exist diff --git a/codex-cli/tests/config_reasoning.test.ts b/codex-cli/tests/config_reasoning.test.ts new file mode 100644 index 00000000..b7ab67ae --- /dev/null +++ b/codex-cli/tests/config_reasoning.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + loadConfig, + DEFAULT_REASONING_EFFORT, + saveConfig, +} from "../src/utils/config"; +import type { ReasoningEffort } from "openai/resources.mjs"; +import * as fs from "fs"; + +// Mock the fs module +vi.mock("fs", () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + mkdirSync: vi.fn(), +})); + +// Mock path.dirname +vi.mock("path", async () => { + const actual = await vi.importActual("path"); + return { + ...actual, + dirname: vi.fn().mockReturnValue("/mock/dir"), + }; +}); + +describe("Reasoning Effort Configuration", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should have "high" as the default reasoning effort', () => { + expect(DEFAULT_REASONING_EFFORT).toBe("high"); + }); + + it("should use default reasoning effort when not specified in config", () => { + // Mock fs.existsSync to return true for config file + vi.mocked(fs.existsSync).mockImplementation(() => true); + + // Mock fs.readFileSync to return a JSON with no reasoningEffort + vi.mocked(fs.readFileSync).mockImplementation(() => + JSON.stringify({ model: "test-model" }), + ); + + const config = loadConfig("/mock/config.json", "/mock/instructions.md"); + + // Config should not have reasoningEffort explicitly set + expect(config.reasoningEffort).toBeUndefined(); + }); + + it("should load reasoningEffort from config file", () => { + // Mock fs.existsSync to return true for config file + vi.mocked(fs.existsSync).mockImplementation(() => true); + + // Mock fs.readFileSync to return a JSON with reasoningEffort + vi.mocked(fs.readFileSync).mockImplementation(() => + JSON.stringify({ + model: "test-model", + reasoningEffort: "low" as ReasoningEffort, + }), + ); + + const config = loadConfig("/mock/config.json", "/mock/instructions.md"); + + // Config should have the reasoningEffort from the file + expect(config.reasoningEffort).toBe("low"); + }); + + it("should support all valid reasoning effort values", () => { + // Valid values for ReasoningEffort + const validEfforts: Array = ["low", "medium", "high"]; + + for (const effort of validEfforts) { + // Mock fs.existsSync to return true for config file + vi.mocked(fs.existsSync).mockImplementation(() => true); + + // Mock fs.readFileSync to return a JSON with reasoningEffort + vi.mocked(fs.readFileSync).mockImplementation(() => + JSON.stringify({ + model: "test-model", + reasoningEffort: effort, + }), + ); + + const config = loadConfig("/mock/config.json", "/mock/instructions.md"); + + // Config should have the correct reasoningEffort + expect(config.reasoningEffort).toBe(effort); + } + }); + + it("should preserve reasoningEffort when saving configuration", () => { + // Setup + vi.mocked(fs.existsSync).mockReturnValue(false); + + // Create config with reasoningEffort + const configToSave = { + model: "test-model", + instructions: "", + reasoningEffort: "medium" as ReasoningEffort, + notify: false, + }; + + // Act + saveConfig(configToSave, "/mock/config.json", "/mock/instructions.md"); + + // Assert + expect(fs.writeFileSync).toHaveBeenCalledWith( + "/mock/config.json", + expect.stringContaining('"model"'), + "utf-8", + ); + + // Note: Current implementation of saveConfig doesn't save reasoningEffort, + // this test would need to be updated if that functionality is added + }); +});