OpenAI's Codex CLI (and the GPT-5 model family more broadly) ships a tool
called apply_patch for editing files. Unlike almost every other tool in
the ecosystem, its argument isn't JSON — it's raw text, constrained by a
formal grammar. This note covers what that means, exactly how it's wired
into the API, and what happens when you actually probe it against real
endpoints.
Classic tool calling asks the model to emit a JSON object as the tool's arguments. For short, flat arguments that's fine. It gets expensive for large, syntax-heavy, multi-line content — a diff, a source file, a SQL query — because every embedded quote, backslash, and newline has to be escaped correctly across potentially hundreds of lines. The failure mode is that models drop or mangle escapes under length pressure, and the payload fails to parse.
OpenAI's answer, introduced alongside GPT-5, is a second tool type:
custom tools. A custom tool's output is raw text instead of a JSON
object — the model writes x = "old value" literally, not
x = \"old value\". Optionally, that raw text can be constrained by a
grammar (Lark or regex), which goes a step further than just avoiding
escaping: with format: {type: "grammar", ...}, the API does constrained
decoding, restricting the token sampler to only ever produce tokens the
grammar allows. A malformed patch becomes structurally impossible to emit,
not just less likely — a materially stronger guarantee than JSON Schema,
which is advisory (the model can still emit invalid JSON you have to
detect and reject).
apply_patch is the flagship example of this: a tool literally described
in OpenAI's own source as "well-suited for GPT-5 models," built as a
freeform/grammar custom tool from day one.
Documentation:
- OpenAI: Function calling guide — Custom tools
- OpenAI Cookbook: GPT-5 new params and tools — explicitly states the JSON-escaping rationale and the constrained-decoding advantage over JSON Schema
- OpenAI Cookbook:
apply_patch.pyreference implementation
A patch is wrapped in a *** Begin Patch / *** End Patch envelope
containing one or more file directives:
*** Begin Patch
*** Add File: hello.txt
+Hello world
*** Update File: src/app.py
*** Move to: src/main.py
@@ def greet():
-print("Hi")
+print("Hello, world!")
*** Delete File: obsolete.txt
*** End Patch
*** Add File: <path>— every following line is a+line: the new file's full content.*** Delete File: <path>— nothing follows.*** Update File: <path>— optionally followed by*** Move to: <path>(rename), then zero or more hunks.- Each hunk starts with
@@(optionally followed by a context anchor, e.g. a function signature, when disambiguation is needed in a larger file), then-/+/(context) lines, and may end with*** End of File.
This is not unified diff. There's no --- a/file / +++ b/file header
pair, no @@ -l,s +l,s @@ line-number/hunk-size metadata — the @@ marker
carries no numbers at all, just an optional textual anchor. The file
action and path live in the *** directive line instead of the header.
Source of truth:
apply_patch.lark
in openai/codex, upstream (unmodified):
start: begin_patch hunk+ end_patch
begin_patch: "*** Begin Patch" LF
end_patch: "*** End Patch" LF?
hunk: add_hunk | delete_hunk | update_hunk
add_hunk: "*** Add File: " filename LF add_line+
delete_hunk: "*** Delete File: " filename LF
update_hunk: "*** Update File: " filename LF change_move? change?
filename: /(.+)/
add_line: "+" /(.*)/ LF -> line
change_move: "*** Move to: " filename LF
change: (change_context | change_line)+ eof_line?
change_context: ("@@" | "@@ " /(.+)/) LF
change_line: ("+" | "-" | " ") /(.*)/ LF
eof_line: "*** End of File" LF
%import common.LFapply_patch isn't the only grammar-based freeform tool in codex. There's
exactly one other: exec (the "code mode" tool, PUBLIC_TOOL_NAME = "exec"
in codex-rs/core/src/tools/code_mode/execute_spec.rs), whose grammar is
almost unconstrained:
start: pragma_source | plain_source
pragma_source: PRAGMA_LINE NEWLINE SOURCE
plain_source: SOURCE
PRAGMA_LINE: /[ \t]*\/\/ @exec:[^\r\n]*/
NEWLINE: /\r?\n/
SOURCE: /[\s\S]+/It only recognizes an optional // @exec:... pragma line before raw
source code — there's essentially nothing to validate structurally. Its
entire purpose is avoiding JSON-escaping cost for arbitrary source code,
not enforcing shape (unlike apply_patch, which does both).
The upstream grammar uses zero-width regex terminals — /(.*)/ can match
an empty string. Python's lark package's Earley engine flatly rejects
grammars containing those (GrammarError: Dynamic Earley doesn't allow zero-width regexps, in every parser/lexer combination — earley+basic,
earley+dynamic_complete, and lalr all fail the same way). This
appears to be a lark-Python-specific restriction, not a property of the
grammar itself (Codex's own Rust implementation obviously accepts it fine).
A fix is available at <lark-parser/lark#1639