feat: add --reasoning CLI flag (#314)
This PR adds a new CLI flag: `--reasoning`, which allows users to customize the reasoning effort level (`low`, `medium`, or `high`) used by OpenAI's `o` models. By introducing the `--reasoning` flag, users gain more flexibility when working with the models. It enables optimization for either speed or depth of reasoning, depending on specific use cases. This PR resolves #107 - **Flag**: `--reasoning` - **Accepted Values**: `low`, `medium`, `high` - **Default Behavior**: If not specified, the model uses the default reasoning level. ## Example Usage ```bash codex --reasoning=low "Write a simple function to calculate factorial" --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: yashrwealthy <yash.rastogi@wealthy.in> Co-authored-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import type { ApprovalPolicy } from "./approvals";
|
|||||||
import type { CommandConfirmation } from "./utils/agent/agent-loop";
|
import type { CommandConfirmation } from "./utils/agent/agent-loop";
|
||||||
import type { AppConfig } from "./utils/config";
|
import type { AppConfig } from "./utils/config";
|
||||||
import type { ResponseItem } from "openai/resources/responses/responses";
|
import type { ResponseItem } from "openai/resources/responses/responses";
|
||||||
|
import type { ReasoningEffort } from "openai/resources.mjs";
|
||||||
|
|
||||||
import App from "./app";
|
import App from "./app";
|
||||||
import { runSinglePass } from "./cli-singlepass";
|
import { runSinglePass } from "./cli-singlepass";
|
||||||
@@ -160,6 +161,12 @@ const cli = meow(
|
|||||||
"Disable truncation of command stdout/stderr messages (show everything)",
|
"Disable truncation of command stdout/stderr messages (show everything)",
|
||||||
aliases: ["no-truncate"],
|
aliases: ["no-truncate"],
|
||||||
},
|
},
|
||||||
|
reasoning: {
|
||||||
|
type: "string",
|
||||||
|
description: "Set the reasoning effort level (low, medium, high)",
|
||||||
|
choices: ["low", "medium", "high"],
|
||||||
|
default: "high",
|
||||||
|
},
|
||||||
// Notification
|
// Notification
|
||||||
notify: {
|
notify: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@@ -292,6 +299,8 @@ config = {
|
|||||||
...config,
|
...config,
|
||||||
model: model ?? config.model,
|
model: model ?? config.model,
|
||||||
notify: Boolean(cli.flags.notify),
|
notify: Boolean(cli.flags.notify),
|
||||||
|
reasoningEffort:
|
||||||
|
(cli.flags.reasoning as ReasoningEffort | undefined) ?? "high",
|
||||||
flexMode: Boolean(cli.flags.flexMode),
|
flexMode: Boolean(cli.flags.flexMode),
|
||||||
provider,
|
provider,
|
||||||
disableResponseStorage:
|
disableResponseStorage:
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ export class AgentLoop {
|
|||||||
try {
|
try {
|
||||||
let reasoning: Reasoning | undefined;
|
let reasoning: Reasoning | undefined;
|
||||||
if (this.model.startsWith("o")) {
|
if (this.model.startsWith("o")) {
|
||||||
reasoning = { effort: "high" };
|
reasoning = { effort: this.config.reasoningEffort ?? "high" };
|
||||||
if (this.model === "o3" || this.model === "o4-mini") {
|
if (this.model === "o3" || this.model === "o4-mini") {
|
||||||
reasoning.summary = "auto";
|
reasoning.summary = "auto";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// compiled `dist/` output used by the published CLI.
|
// compiled `dist/` output used by the published CLI.
|
||||||
|
|
||||||
import type { FullAutoErrorMode } from "./auto-approval-mode.js";
|
import type { FullAutoErrorMode } from "./auto-approval-mode.js";
|
||||||
|
import type { ReasoningEffort } from "openai/resources.mjs";
|
||||||
|
|
||||||
import { AutoApprovalMode } from "./auto-approval-mode.js";
|
import { AutoApprovalMode } from "./auto-approval-mode.js";
|
||||||
import { log } from "./logger/log.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;
|
parseInt(process.env["OPENAI_TIMEOUT_MS"] || "0", 10) || undefined;
|
||||||
export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || "";
|
export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || "";
|
||||||
export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || "";
|
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_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || "";
|
||||||
export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || "";
|
export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || "";
|
||||||
|
|
||||||
@@ -142,6 +145,9 @@ export type StoredConfig = {
|
|||||||
saveHistory?: boolean;
|
saveHistory?: boolean;
|
||||||
sensitivePatterns?: Array<string>;
|
sensitivePatterns?: Array<string>;
|
||||||
};
|
};
|
||||||
|
/** User-defined safe commands */
|
||||||
|
safeCommands?: Array<string>;
|
||||||
|
reasoningEffort?: ReasoningEffort;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimal config written on first run. An *empty* model string ensures that
|
// Minimal config written on first run. An *empty* model string ensures that
|
||||||
@@ -165,6 +171,7 @@ export type AppConfig = {
|
|||||||
approvalMode?: AutoApprovalMode;
|
approvalMode?: AutoApprovalMode;
|
||||||
fullAutoErrorMode?: FullAutoErrorMode;
|
fullAutoErrorMode?: FullAutoErrorMode;
|
||||||
memory?: MemoryConfig;
|
memory?: MemoryConfig;
|
||||||
|
reasoningEffort?: ReasoningEffort;
|
||||||
/** Whether to enable desktop notifications for responses */
|
/** Whether to enable desktop notifications for responses */
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
|
|
||||||
@@ -366,6 +373,7 @@ export const loadConfig = (
|
|||||||
notify: storedConfig.notify === true,
|
notify: storedConfig.notify === true,
|
||||||
approvalMode: storedConfig.approvalMode,
|
approvalMode: storedConfig.approvalMode,
|
||||||
disableResponseStorage: storedConfig.disableResponseStorage ?? false,
|
disableResponseStorage: storedConfig.disableResponseStorage ?? false,
|
||||||
|
reasoningEffort: storedConfig.reasoningEffort,
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -480,6 +488,7 @@ export const saveConfig = (
|
|||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
providers: config.providers,
|
providers: config.providers,
|
||||||
approvalMode: config.approvalMode,
|
approvalMode: config.approvalMode,
|
||||||
|
reasoningEffort: config.reasoningEffort,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add history settings if they exist
|
// Add history settings if they exist
|
||||||
|
|||||||
121
codex-cli/tests/config_reasoning.test.ts
Normal file
121
codex-cli/tests/config_reasoning.test.ts
Normal file
@@ -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<ReasoningEffort> = ["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
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user