feat: present codex edit tool as apply_patch

This commit is contained in:
Hannes Rudolph 2025-12-24 16:13:48 -07:00
parent d2f5dfd24c
commit 0d5bd7ac4e
4 changed files with 139 additions and 19 deletions

View file

@ -7,6 +7,26 @@ describe("NativeToolCallParser", () => {
})
describe("parseToolCall", () => {
it("should resolve apply_patch to edit_file and preserve originalName", () => {
const toolCall = {
id: "toolu_apply_patch_123",
name: "apply_patch" as any,
arguments: JSON.stringify({
patch: "*** Begin Patch\n*** End Patch",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
expect(result.name).toBe("edit_file")
expect(result.originalName).toBe("apply_patch")
expect(result.nativeArgs).toEqual({ patch: "*** Begin Patch\n*** End Patch" })
}
})
describe("read_file tool", () => {
it("should handle line_ranges as tuples (new format)", () => {
const toolCall = {

View file

@ -11,7 +11,7 @@ import * as toolsModule from "../../../../shared/tools"
// and replaced with the unified "edit_file" tool.
describe("filterNativeToolsForMode", () => {
// Use edit_file_roo as the default edit tool variant in mock tools
// Include all edit tool variants to mirror the production native tools array.
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
@ -37,6 +37,38 @@ describe("filterNativeToolsForMode", () => {
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_anthropic",
description: "Edit file (Anthropic variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_grok",
description: "Edit file (Grok variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_gemini",
description: "Edit file (Gemini variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_codex",
description: "Edit file (Codex variant)",
parameters: {},
},
},
{
type: "function",
function: {
@ -123,10 +155,11 @@ describe("filterNativeToolsForMode", () => {
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
// Should include all tools (code mode has all groups)
// Note: edit_file_roo gets renamed to edit_file
// Note: edit tool variants get renamed to a single display name.
// Default is "edit_file" (roo variant), but codex can be presented as "apply_patch".
expect(toolNames).toContain("read_file")
expect(toolNames).toContain("write_to_file")
expect(toolNames).toContain("edit_file") // Unified edit tool
expect(toolNames).toContain("edit_file") // Unified edit tool (default)
expect(toolNames).not.toContain("edit_file_roo") // Variant name should be renamed
expect(toolNames).toContain("execute_command")
expect(toolNames).toContain("browser_action")
@ -134,6 +167,29 @@ describe("filterNativeToolsForMode", () => {
expect(toolNames).toContain("attempt_completion")
})
it("should present codex edit tool variant as apply_patch when editToolVariant is codex", () => {
const codeMode: ModeConfig = {
slug: "code",
name: "Code",
roleDefinition: "Test",
groups: ["read", "edit", "browser", "command", "mcp"] as const,
}
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
modelInfo: {
contextWindow: 100000,
supportsPromptCache: false,
editToolVariant: "codex",
},
})
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).toContain("apply_patch")
expect(toolNames).not.toContain("edit_file")
expect(toolNames).not.toContain("edit_file_codex")
})
it("should always include always-available tools regardless of mode groups", () => {
const restrictiveMode: ModeConfig = {
slug: "restrictive",
@ -718,6 +774,38 @@ describe("filterMcpToolsForMode", () => {
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_anthropic",
description: "Edit file (Anthropic variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_grok",
description: "Edit file (Grok variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_gemini",
description: "Edit file (Gemini variant)",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file_codex",
description: "Edit file (Codex variant)",
parameters: {},
},
},
{
type: "function",
function: {

View file

@ -8,22 +8,30 @@ import type { McpHub } from "../../../services/mcp/McpHub"
import { isToolAllowedForMode } from "../../../core/tools/validateToolUse"
/**
* Mapping from edit tool variant to internal tool name.
* These are the tools that will be selected based on modelInfo.editToolVariant.
* Central edit tool configuration.
*
* `toolName` is the internal native tool name.
* `displayName` is the name shown to (and called by) the model.
*
* By default, edit tool variants are presented as "edit_file".
* The codex variant is presented as "apply_patch" so:
* - the model calls `apply_patch`
* - the tool is stored in API conversation history as `apply_patch`
* - execution is still routed through the canonical `edit_file` tool via aliases
*/
const EDIT_TOOL_VARIANT_MAP: Record<EditToolVariant, string> = {
roo: "edit_file_roo",
anthropic: "edit_file_anthropic",
grok: "edit_file_grok",
gemini: "edit_file_gemini",
codex: "edit_file_codex",
const EDIT_TOOL_VARIANTS: Record<EditToolVariant, { toolName: string; displayName: string }> = {
roo: { toolName: "edit_file_roo", displayName: "edit_file" },
anthropic: { toolName: "edit_file_anthropic", displayName: "edit_file" },
grok: { toolName: "edit_file_grok", displayName: "edit_file" },
gemini: { toolName: "edit_file_gemini", displayName: "edit_file" },
codex: { toolName: "edit_file_codex", displayName: "apply_patch" },
}
/**
* All edit tool variant names that should be filtered.
* Only one of these (based on editToolVariant) will be included and renamed to "edit_file".
* Only one of these (based on editToolVariant) will be included and renamed to the configured display name.
*/
const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANT_MAP))
const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANTS).map((variant) => variant.toolName))
/**
* Reverse lookup map - maps alias name to canonical tool name.
@ -326,7 +334,8 @@ export function filterNativeToolsForMode(
// Determine which edit tool variant to use (default: "roo")
const editToolVariant: EditToolVariant = modelInfo?.editToolVariant ?? "roo"
const selectedEditToolName = EDIT_TOOL_VARIANT_MAP[editToolVariant]
const selectedEditToolName = EDIT_TOOL_VARIANTS[editToolVariant].toolName
const editToolDisplayName = EDIT_TOOL_VARIANTS[editToolVariant].displayName
// Check if diffs are disabled - if so, skip edit tool entirely
const diffEnabled = settings?.diffEnabled !== false
@ -358,8 +367,9 @@ export function filterNativeToolsForMode(
// 3. Mode has "edit" group
// 4. edit_file is not excluded by model config
if (toolName === selectedEditToolName && diffEnabled && modeHasEditGroup && !isEditFileExcluded) {
// Rename the selected variant to "edit_file" so LLM always sees that name
filteredTools.push(getOrCreateRenamedTool(tool, "edit_file"))
// Rename the selected variant to the configured display name.
// The tool will still execute via the unified `edit_file` tool name through aliases.
filteredTools.push(getOrCreateRenamedTool(tool, editToolDisplayName))
}
continue
}

View file

@ -339,12 +339,14 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
* When a model calls a tool by its alias, the system resolves it to the canonical name for execution,
* but preserves the alias in API conversation history for consistency.
*
* Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace, apply_patch)
* have been removed. Native protocol now uses editToolVariant to select the edit tool schema,
* and all edit tools are presented to the LLM as "edit_file". XML protocol still uses apply_diff directly.
* Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace)
* have been removed. Native protocol uses editToolVariant to select the edit tool schema.
* The codex variant can optionally be presented to the LLM as "apply_patch".
* XML protocol still uses apply_diff directly.
*/
export const TOOL_ALIASES: Record<string, ToolName> = {
write_file: "write_to_file",
apply_patch: "edit_file",
} as const
export type DiffResult =