apx
A Codex Responses router that lets agents edit with compact selectors and replacement text instead of emitting full patches.
apx-router sits between Codex and the Responses API. It adds constrained
functions.apx, resolves successful scripts against the declared workspace, and hands
Codex a real apply_patch carrier so sandbox checks and the normal diff UI remain intact.
The default prefer mode retains native apply_patch as a fallback; exclusive mode
removes it for maximum output-token savings.
This project is an independent MIT-licensed derivative of
yusing/hpatch, preserving its history, architecture,
and attribution while adding portability and operational hardening.
TL;DR:
Why apx?
A direct Code Mode edit makes the model repeat patch framing, old context, replacement text, and the JavaScript carrier. The router moves patch reconstruction out of model output:
flowchart LR
subgraph output["Alternative model-output payloads"]
H["apx path<br/>functions.apx + selectors + replacement"]
A["apply_patch baseline<br/>functions.exec + JavaScript carrier<br/>+ old context + replacement + patch framing"]
end
subgraph router["Router and Codex after model output"]
B["Router reads the immutable<br/>workspace baseline"]
C["Router generates the<br/>apply_patch envelope"]
D["Codex applies the patch<br/>sandbox checks + normal diff"]
end
H --> B --> C --> D
A --> D
What the agent has to output: left versus right
Both paths eventually apply the same patch. The saving comes from how much of that patch
the model must generate before the host can act:
| Model-output component |
apx — compact side |
native apply_patch — expanded side |
| Tool and wrapper syntax |
functions.apx |
functions.exec, JavaScript call, template literal, result plumbing |
| File parameter |
in parser.go |
*** Update File: parser.go inside a full patch envelope |
| Selection parameter |
rsel 50:60 |
@@ plus the old lines needed to identify and validate the hunk |
| New input |
11 replacement lines, emitted once |
the same 11 replacement lines, each with a + prefix |
| Old input repeated by the model |
none; the router reads lines 50–60 from the immutable baseline |
all 11 old lines, each with a - prefix |
| Framing around the replacement |
type <<PATCH and one closing PATCH |
*** Begin Patch, update header, hunk header, prefixes, *** End Patch, and JavaScript closing lines |
| Lines in the example below |
16 total: 11 content + 5 syntax/parameters |
30 total: 11 new + 11 old + 8 syntax/wrapper lines |
So apx does not make the replacement itself shorter. It removes repeated old input and
moves generic patch/JavaScript syntax out of model output. Short fragment edits save less;
large replacements, repeated edits, and long unchanged boundaries save more. The
repository comparison currently measures 108 apx tokens versus 223 apply_patch
tokens across six scenarios: 115 output tokens saved, or 51.6%.
The patch is not eliminated: the router generates it after inference. Savings are the difference between the two model-output payload estimates shown above. State reports, rejection diagnostics, and tool-definition costs are tracked separately as input overhead. In exclusive mode the report also accounts for the displaced native definition. These are reproducible GPT-5 token estimates, not provider billing totals.
For an 11-line function replacement, apx asks the model for this:
functions.apx
in parser.go
rsel 50:60
type <<PATCH
func parse(input []byte) (Document, error) {
tokens, err := tokenize(input)
if err != nil {
return Document{}, fmt.Errorf("tokenize: %w", err)
}
document, err := buildDocument(tokens)
if err != nil {
return Document{}, fmt.Errorf("build document: %w", err)
}
return document, nil
}
PATCH
The same edit through direct apply_patch in Code Mode:
functions.exec
const result = await tools.apply_patch(`*** Begin Patch
*** Update File: parser.go
@@
-func parse(input []byte) (Document, error) {
- tokens := tokenize(input)
- if len(tokens) == 0 {
- return Document{}, errEmptyInput
- }
- document := buildDocument(tokens)
- if document.Empty() {
- return Document{}, errEmptyDocument
- }
- return document, nil
-}
+func parse(input []byte) (Document, error) {
+ tokens, err := tokenize(input)
+ if err != nil {
+ return Document{}, fmt.Errorf("tokenize: %w", err)
+ }
+ document, err := buildDocument(tokens)
+ if err != nil {
+ return Document{}, fmt.Errorf("build document: %w", err)
+ }
+ return document, nil
+}
*** End Patch
`);
text(result);
The direct call repeats all 11 old lines, then writes the same 11 new lines plus patch framing and the JavaScript carrier. apx writes the new function once; the router reads the old region from the baseline.
The router also supplies functions.apx to the provider with a Lark grammar. As the model writes the tool call, only tokens that can still lead to a valid script are allowed. Bad syntax never becomes a finished tool call, so there is no generate-reject-retry cycle for it. Grammar is syntax only: a valid script can still fail for missing files, ambiguous selectors, or conflicting edits, and those failures stay atomic.
Requirements
- A release binary, or Go 1.26 or newer when installing from source
- For the router: Codex CLI with ChatGPT file auth (
codex login, credentials at ~/.codex/auth.json or $CODEX_HOME/auth.json)
Codex router
The router listens on HTTP, rewrites Responses traffic so Codex calls functions.apx, evaluates scripts against the workspace declared in x-codex-turn-metadata, and returns the translated apply_patch carrier to Codex. You see a real diff in the UI rather than a silent file rewrite.
On each request it installs a standalone functions.apx tool. --tool-mode prefer
retains Code Mode's native apply_patch; --tool-mode exclusive strips that native
section. The history may still label an apx apply step as apply_patch because that is
what Codex ultimately runs.
Defaults:
| Setting |
Default |
| Listen |
127.0.0.1:8080 (--listen) |
| Tool mode |
prefer (--tool-mode prefer|exclusive) |
| Upstream start timeout |
10m (--timeout) |
| Metrics / hooks |
$XDG_CONFIG_HOME/apx or ~/.config/apx |
| Endpoints |
POST /v1/responses, GET /healthz, GET / (dashboard), GET /api/metrics |
The process must run as your login user so it can open the absolute workspace paths Codex
sends. Codex-managed authentication is forwarded per request and is never printed by
doctor. Non-loopback listeners are rejected unless --allow-non-loopback is explicit,
including a hostname that resolves to a non-loopback bind address. Writes to a
final-component symbolic link reject so standalone and router modes cannot produce
different filesystem outcomes.
Install the binary
go install github.com/tuanhung303/apx/cmd/apx-router@latest
Default install path is ~/go/bin/apx-router (or $GOBIN/apx-router if GOBIN is set). Ensure that directory is on your PATH.
Tagged releases also publish self-contained macOS and Linux archives for amd64 and arm64.
macOS LaunchAgent
Install, inspect, or remove a user LaunchAgent without editing the base Codex config:
apx-router service install
apx-router service status
apx-router service uninstall
The generated service binds to loopback in prefer mode. A static reference template is
available at contrib/launchd/io.github.tuanhung303.apx-router.plist.
Linux systemd user service
The unit template is published at
contrib/systemd/apx-router.service
(raw URL works after the repo is on GitHub):
mkdir -p ~/.config/systemd/user
curl -fsSL https://raw.githubusercontent.com/tuanhung303/apx/main/contrib/systemd/apx-router.service \
-o ~/.config/systemd/user/apx-router.service
systemctl --user daemon-reload
systemctl --user enable --now apx-router.service
systemctl --user status apx-router.service
Optional: keep the service after logout:
loginctl enable-linger "$USER"
One-shot without a service:
apx-router --listen 127.0.0.1:8080 --tool-mode prefer
If auth lives outside ~/.codex, or the binary is not in ~/go/bin, use a drop-in:
systemctl --user edit apx-router.service
[Service]
Environment=CODEX_HOME=%h/.codex
# ExecStart=
# ExecStart=%h/.local/bin/apx-router --listen 127.0.0.1:9090
Then systemctl --user daemon-reload && systemctl --user restart apx-router.service.
Point Codex at the router
Copy contrib/codex/apx.config.toml to
~/.codex/apx.config.toml, replace its absolute instruction path, and keep the provider
isolated from your base Codex configuration:
[model_providers.apx]
name = "apx"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = true
Start a CLI session with the dedicated profile:
codex --profile apx
For Codex Desktop, launch a fresh app process with equivalent -c provider overrides or
place the provider in the base config. Existing tasks cannot acquire a new tool surface
mid-turn.
Start sessions from a git worktree: the router requires exactly one usable absolute workspace root in turn metadata.
Useful checks:
systemctl --user status apx-router.service
journalctl --user -u apx-router.service -f
apx-router doctor
apx-router doctor --json
curl -sS http://127.0.0.1:8080/healthz
curl -sS http://127.0.0.1:8080/api/metrics
# open http://127.0.0.1:8080/ for the local dashboard
Agent-managed prompt update and reversion
The router exposes functions.apx, but Codex’s default base prompt still tells the model
to use apply_patch. In prefer mode both are intentionally visible. Point Codex at a
custom base-instructions file and replace its file-editing section so the model chooses
apx first and native apply_patch only as fallback.
Instruction for an agent reading this README: when the user asks you to enable the
apx prompt, perform the update procedure below instead of merely describing it. When
the user asks to disable or revert apx, perform the reversion procedure. Resolve paths
first, preserve existing files, make no change to the base config.toml, and report
exactly what was created, backed up, restored, or removed. A running Codex process
cannot acquire or lose this tool surface mid-turn, so finish by telling the user to
start a fresh task or restart Codex.
Prompt update
-
Resolve CODEX_HOME to ${CODEX_HOME:-$HOME/.codex} and the checked-out apx
repository to an absolute path. Verify that these source files exist:
contrib/codex/apx.config.toml
contrib/codex/file-editing-instructions.md
-
Before changing anything, create a timestamped backup directory under
$CODEX_HOME/apx-backups/. Copy any existing $CODEX_HOME/apx.config.toml and
$CODEX_HOME/apx-instructions.md into it with their modes preserved. Do not copy,
print, or modify $CODEX_HOME/auth.json.
-
Fetch or locate the default base prompt that matches the installed Codex version.
Keep the entire prompt except its file-editing guidance. Useful upstream references:
-
Write the result to $CODEX_HOME/apx-instructions.md. In the section usually named
## File editing or ## File editing constraints, replace only the native
apply_patch preference with
contrib/codex/file-editing-instructions.md.
Preserve dirty-worktree rules, destructive-action rules, sandbox rules, and every
unrelated section. Confirm that the resulting file contains exactly one
## File editing section, mentions functions.apx, and retains native
apply_patch as the prefer-mode fallback.
-
Create $CODEX_HOME/apx.config.toml from
contrib/codex/apx.config.toml, replacing its
placeholder with the absolute path to $CODEX_HOME/apx-instructions.md. This is a
layered profile file; do not add these settings to the user’s base
$CODEX_HOME/config.toml.
model_provider = "apx"
model_instructions_file = "/absolute/path/to/.codex/apx-instructions.md"
[model_providers.apx]
name = "apx"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
requires_openai_auth = true
- Validate without exposing credentials:
apx-router doctor --json
codex --profile apx doctor --json
Then start a fresh codex --profile apx task and confirm that both
functions.apx and, in prefer mode, native apply_patch are available.
Do not rely on project AGENTS.md alone: the stock base prompt still steers file
edits toward apply_patch.
Prompt reversion
- Stop the apx-profile Codex process. Resolve the same
CODEX_HOME; do not touch
$CODEX_HOME/config.toml or $CODEX_HOME/auth.json.
- Find the backup directory created by the corresponding prompt update. If it contains
a prior
apx.config.toml or apx-instructions.md, restore those exact files and
modes. If a file did not exist before the update, remove only the apx-created current
file instead of inventing a replacement.
- Do not delete the backup automatically. Report its path so the user can remove it
after verifying the restoration.
- Start a fresh normal Codex task without
--profile apx. Verify that the router
profile is no longer active. Reverting the prompt does not uninstall the binaries or
service; run apx-router service uninstall separately only when the user explicitly
asks to remove the service.
Standalone CLI
Install the CLI (requires Go 1.26+; binary lands in $(go env GOPATH)/bin or $GOBIN):
go install github.com/tuanhung303/apx/cmd/apx@latest
Apply a script (writes only after the full script validates and stages; success report on stderr):
apx <<'EOF'
in src/app.go
tsel 12 "oldName"
type "newName"
EOF
Translate to an OpenAI apply_patch envelope without touching files (patch on stdout, pending report on stderr):
apx translate <<'EOF'
new message.txt
type "hello world\n"
EOF
For router and translate mode, new-file content must end with \n; OpenAI Add File
patches cannot faithfully represent empty or unterminated files, so apx rejects those
translations instead of silently changing bytes.
Script paths are workspace-relative.
| Surface |
Workspace root |
Path base inside root |
| Standalone CLI |
Process current directory, or absolute --root |
., or --cwd (relative or absolute, must stay under root) |
| Codex router |
The single usable absolute path from x-codex-turn-metadata |
Root itself (no CLI flags) |
Translated patches always use root-relative paths. Details: apx --help.
| Mode |
Mutates files? |
stdout |
stderr |
apx |
Yes, after full validation |
empty on success |
final-state report |
apx translate |
No |
apply_patch envelope |
pending final-state report |
apx gain |
No |
metrics report |
empty on success |
--help / --tool-help / --version |
No |
help or version |
empty |
Built-in references:
apx --help
apx --tool-help
apx translate --help
apx --version
Editing language (summary)
Authoritative guidance: apx --help and apx --tool-help. Contract: doc/spec/interface.md.
Selectors (minimize the complete selector-plus-replacement output):
tsel FROM_LINE "TEXT" [N]: a fragment, or one replacement at multiple sites.
bsel "START" "END": a multiline partial region identified by unique boundary literals; both are consumed.
rsel START:END: a region where every selected line is re-emitted anyway.
sel LINE START:END: a later identical match on one line, with verified coordinates.
Common commands: in / new / mv / rm, type "…" or type <<PATCH … PATCH, del, copy / cut / paste, commit.
Rules worth remembering:
- Omit leading whitespace in
tsel / bsel anchors unless it disambiguates the match.
- First
in of a file freezes an immutable baseline; selectors use that baseline until commit.
- Disjoint edits can land together; overlapping replacements fail the whole script atomically.
- Rejection changes nothing. Router-owned retries can replace, delete, or insert failed commands by index without resending the full script.
Multiline example:
in parser.go
rsel 50:60
type <<PATCH
func parse(input []byte) (Document, error) {
tokens, err := tokenize(input)
if err != nil {
return Document{}, fmt.Errorf("tokenize: %w", err)
}
document, err := buildDocument(tokens)
if err != nil {
return Document{}, fmt.Errorf("build document: %w", err)
}
return document, nil
}
PATCH
Metrics
Successful CLI and router edits record paired GPT-5 output-token estimates for:
- apx:
functions.apx + the model-emitted script
- baseline:
functions.exec + a fixed program that calls tools.apply_patch with the translated envelope
Failed calls charge the full rejected apx payload against an empty-patch baseline. Final-state reports, diagnostics, and once-per-session tool definitions are tracked as input overhead separately.
apx gain
These are reproducible payload estimates, not provider billing totals. They omit reasoning tokens, commentary, and host-specific framing.
Hand-authored scenario comparison (does not update apx gain):
go run ./compare
How it works
CLI: resolve workspace (--root / --cwd or process cwd) → parse script → evaluate against an in-memory baseline → stage multi-file result → commit (normal mode) or emit one apply_patch envelope (translate).
Router: accept an authenticated POST /v1/responses → expose functions.apx
alongside or instead of Code Mode apply_patch → translate against the single usable
workspace from Codex metadata → return a carrier that makes Codex apply the real patch
and surface the diff.
Workspace selection is host-owned. Zero or multiple usable roots fail closed. Codex still enforces sandbox permissions on apply.
Project structure
.
├── cmd/
│ ├── apx/ # CLI entry point and command-line contract
│ └── apx-router/ # Router process entry point
├── internal/
│ ├── router/ # Responses proxy, apx translation, auth, metrics, and dashboard
│ ├── apxsyntax/ # Shared quoted-string and heredoc parsing
│ └── patchtest/ # Test helper for applying translated patch envelopes
├── compare/ # Hand-authored apx vs. apply_patch token scenarios
├── contrib/ # Codex guidance plus launchd and systemd service templates
├── doc/ # Product brief, interface specification, and architecture index
├── *.go # Core parser, editor, workspace, transaction, translation, hooks, and metrics
├── tool_description.md # Embedded function-tool instructions
└── tool_grammar.lark # Embedded constrained-decoding grammar
Tests live beside the packages they exercise. The root apx package is the reusable engine; cmd/apx and internal/router call it rather than maintaining separate editing implementations. The router dashboard is embedded from internal/router/dashboard.html.
Documentation
Library use: module path github.com/tuanhung303/apx. Importable as a library (apx.Translate, apx.Workspace, host metrics helpers); hosts should open an *os.Root capability for the workspace before calling in.
Development
git clone https://github.com/tuanhung303/apx.git
cd apx
go test ./...
go vet ./...
go install ./cmd/apx ./cmd/apx-router