Metadata-Version: 2.4
Name: codex-relay
Version: 0.6.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Libraries
License-File: LICENSE
Summary: Responses API ↔ Chat Completions translation bridge for Codex CLI
Keywords: codex,llm,proxy,relay
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# codex-relay

A lightweight Rust proxy that translates the OpenAI **Responses API** (used by [Codex CLI](https://github.com/openai/codex)) into the **Chat Completions API**, letting Codex work with any OpenAI-compatible provider — DeepSeek, Kimi, Qwen, Mistral, Groq, xAI, OpenRouter, and more.

## Why

Codex CLI speaks the OpenAI Responses API, which is an OpenAI-proprietary stateful protocol. Every other provider exposes the standard Chat Completions API. `codex-relay` sits between Codex and your chosen provider, translating on the fly — no code changes to Codex required.

## Install

```bash
# From PyPI — prebuilt binary for your platform
pip install codex-relay

# From crates.io
cargo install codex-relay
```

## Quick start

**1. Start the relay**

```bash
CODEX_RELAY_UPSTREAM=https://api.deepseek.com/v1 \
CODEX_RELAY_API_KEY=$DEEPSEEK_API_KEY \
CODEX_RELAY_PORT=4446 \
codex-relay
```

On startup, the relay logs the available upstream models and prints a hint:

```
ℹ upstream models: deepseek-chat, deepseek-reasoner
⚠  To configure Codex with model metadata, run:  codex-relay --print-config --upstream ...
```

**2. Generate your Codex config**

```bash
codex-relay --print-config \
  --upstream https://api.deepseek.com/v1 \
  --api-key $DEEPSEEK_API_KEY \
  --model-catalog ~/.codex/codex-relay-models.json
```

This prints a ready-to-use `~/.codex/config.toml` snippet and writes a model
catalog containing both Codex's built-in models and every upstream model, so
you won't see the **"Model metadata … not found"** warning. The generated
entries inherit version-sensitive instructions and tool formats from the
installed Codex CLI. Use `--model-template <MODEL>` to select a different
bundled model as that template.

If you prefer to write the config by hand, here is the minimal form:

```toml
model = "deepseek-chat"
model_provider = "deepseek-relay"
model_catalog_json = "/home/user/.codex/codex-relay-models.json"

[model_providers.deepseek-relay]
name = "DeepSeek"
base_url = "http://127.0.0.1:4446/v1"
wire_api = "responses"
env_key = "DEEPSEEK_API_KEY"
```

> Recent Codex versions use `model_catalog_json`; the former
> `[model_properties]` config syntax is no longer supported. A custom catalog
> replaces the built-in catalog, so `codex-relay` starts with the catalog from
> your installed Codex version and appends upstream models instead of creating
> an incomplete replacement.

**3. Use Codex normally** — it routes through the relay transparently.

## CLI reference

| Flag | Env var | Default | Description |
|---|---|---|---|
| `--port` | `CODEX_RELAY_PORT` | `4444` | Listen port |
| `--bind` | `CODEX_RELAY_BIND` | `127.0.0.1` | IP address to bind the listener to (e.g. `0.0.0.0` to accept remote connections) |
| `--upstream` | `CODEX_RELAY_UPSTREAM` | `https://openrouter.ai/api/v1` | Upstream Chat Completions base URL |
| `--api-key` | `CODEX_RELAY_API_KEY` | _(empty)_ | API key forwarded to upstream |
| `--opencode-go-compat` | `CODEX_RELAY_OPENCODE_GO_COMPAT` | `false` | Enable OpenCode Go compatibility normalization and schema-reference stripping |
| `--max-request-bytes` | `CODEX_RELAY_MAX_REQUEST_BYTES` | `67108864` | Maximum inbound Responses request size |
| `--upstream-extra-params` | `CODEX_RELAY_UPSTREAM_EXTRA_PARAMS` | _(empty)_ | JSON object merged into each upstream request |
| `--drop-upstream-params` | `CODEX_RELAY_DROP_PARAMS` | _(empty)_ | JSON array of top-level upstream request parameters to remove |
| `--model-map` | `CODEX_RELAY_MODEL_MAP` | _(empty)_ | Comma-separated `source:target` model name translations |
| `--print-config` | _(none)_ | — | Print a Codex config snippet and exit |
| `--model-catalog` | _(none)_ | — | With `--print-config`, write a version-matched full model catalog to this path |
| `--model-template` | _(none)_ | first visible bundled model | Bundled Codex model whose instructions and tool formats generated entries inherit |
| `--record-corpus` | `CODEX_RELAY_RECORD_CORPUS` | _(off)_ | Append the conversation flow of every completed turn to daily JSONL files (OpenAI messages format) in this directory |
| `--session-ttl-hours` | `CODEX_RELAY_SESSION_TTL_HOURS` | `168` | Retain idle `previous_response_id` history and reasoning state for this many hours |
| `--max-sessions` | `CODEX_RELAY_MAX_SESSIONS` | `256` | Maximum completed response histories retained for continuation |
| `--max-session-memory-mb` | `CODEX_RELAY_MAX_SESSION_MEMORY_MB` | `512` | Approximate memory budget for retained session/reasoning state |

## Supported providers

| Provider | Base URL | Suggested port |
|---|---|---|
| DeepSeek | `https://api.deepseek.com/v1` | 4446 |
| Kimi (Moonshot) | `https://api.moonshot.cn/v1` | 4447 |
| GLM (Zhipu) | `https://open.bigmodel.cn/api/coding/paas/v4` | 4453 |
| Qwen | `https://dashscope.aliyuncs.com/compatible-mode/v1` | 4448 |
| Mistral | `https://api.mistral.ai/v1` | 4449 |
| Groq | `https://api.groq.com/openai/v1` | 4450 |
| xAI | `https://api.x.ai/v1` | 4451 |
| OpenRouter | `https://openrouter.ai/api/v1` | 4452 |

Any OpenAI-compatible endpoint works.

### Upstream request parameters

Some providers expose non-standard Chat Completions parameters. You can merge
top-level JSON fields into every upstream request, and optionally drop generated
top-level fields before the merge. For example, to disable DeepSeek V4
thinking/reasoning mode:

```bash
CODEX_RELAY_UPSTREAM_EXTRA_PARAMS='{"thinking":{"type":"disabled"}}' \
CODEX_RELAY_DROP_PARAMS='["reasoning_effort"]' \
codex-relay --upstream https://api.deepseek.com/v1 --api-key "$DEEPSEEK_API_KEY"
```

## Features

- **Streaming** — full SSE streaming with correct event sequencing
- **Tool calls** — accumulates streaming deltas and emits structured function_call items
- **Parallel tool calls** — consecutive function_call input items merged into one assistant message
- **Reasoning models** — streams `reasoning_content` (or the `reasoning` alias) as Responses reasoning summaries and preserves it across turns (Kimi k2.6, DeepSeek-R1, GLM). For GLM/Zhipu models the relay automatically sends `thinking: {"type": "enabled"}`, since GLM otherwise suppresses reasoning under Codex's system prompt
- **Model catalog** — proxies `/v1/models` from the upstream provider
- **Auto-config** — `--print-config` generates a complete Codex config with model metadata

### Shutdown

Ctrl-C, or SIGTERM on Unix, stops accepting new connections and waits for active
requests to finish, including streaming responses. Normal shutdown exits with
status 0. When disk history or corpus recording is enabled, completed Chat
Completions turns are written before normal shutdown completes.

The first signal waits without a timeout. A second supported shutdown signal
interrupts a stalled drain and exits with status 1; unfinished requests can be
interrupted. Signal listeners are registered before the server announces
readiness, and registration failures exit with a nonzero status.

## Configuration

| Variable | Default | Description |
|---|---|---|
| `CODEX_RELAY_PORT` | `4444` | Port to listen on |
| `CODEX_RELAY_BIND` | `127.0.0.1` | IP address to bind the listener to (e.g. `0.0.0.0` to accept remote connections) |
| `CODEX_RELAY_UPSTREAM` | `https://openrouter.ai/api/v1` | Upstream Chat Completions base URL |
| `CODEX_RELAY_API_KEY` | _(empty)_ | API key forwarded to upstream |
| `CODEX_RELAY_OPENCODE_GO_COMPAT` | `false` | Enable OpenCode Go compatibility normalization and schema-reference stripping |
| `CODEX_RELAY_MAX_REQUEST_BYTES` | `67108864` | Maximum inbound Responses request size |
| `CODEX_RELAY_UPSTREAM_EXTRA_PARAMS` | _(empty)_ | JSON object merged into each upstream request body |
| `CODEX_RELAY_DROP_PARAMS` | _(empty)_ | JSON array of top-level upstream request parameter names to remove before forwarding |
| `CODEX_RELAY_MODEL_MAP` | _(empty)_ | Comma-separated `source:target` model name translations (e.g., `gpt-5.4:deepseek-v4-pro`) |
| `CODEX_RELAY_TOOL_DENYLIST` | _(empty)_ | Comma-separated tool names to remove before forwarding tools to the upstream model |
| `CODEX_RELAY_DISABLE_QUIRKS` | _(empty)_ | Comma-separated [platform quirk](#platform-quirks) names to disable (e.g. `dsml_heal,glm_thinking`) |
| `CODEX_RELAY_SESSION_TTL_HOURS` | `168` | Retain idle session/reasoning state for this many hours |
| `CODEX_RELAY_MAX_SESSIONS` | `256` | Maximum completed response histories retained for `previous_response_id` |
| `CODEX_RELAY_MAX_SESSION_MEMORY_MB` | `512` | Approximate memory budget for retained session/reasoning state |
| `CODEX_RELAY_HISTORY_STORE` | `memory` | Retained history backend: `memory` or `disk` |
| `CODEX_RELAY_HISTORY_DIR` | `.codex-relay-history` | Directory for disk-backed history records |
| `CODEX_RELAY_RECORD_CORPUS` | _(off)_ | Directory to append per-turn conversation records (OpenAI messages JSONL); off unless set |
| `RUST_LOG` | `codex_relay=info` | Log verbosity |

## Platform quirks

### OpenCode Go compatibility

This opt-in mode sends **all** requests to the configured upstream's native
`/responses` endpoint instead of translating them to `/chat/completions`. Use
an upstream and model that support native Responses (for example, Muse on
OpenCode Go); there is no automatic fallback to Chat Completions.

```bash
CODEX_RELAY_OPENCODE_GO_COMPAT=true \
  codex-relay --upstream https://opencode.ai/zen/go/v1 --api-key "$OPENCODE_GO_API_KEY"
```

The bridge normalizes custom tools to structured functions, flattens namespace
tools, and bounds upstream tool names to 64 bytes. It restores the original
client identities on both JSON responses and SSE events, including Codex
`apply_patch` custom calls and namespace routing. History items, forced
`tool_choice`, and `allowed_tools` choices use the same mapping. Ambiguous
names or forced choices targeting unavailable tools return HTTP 422.

Text and reasoning SSE events are forwarded as they arrive, with keepalives
while the provider is idle. Custom tool arguments are buffered **per tool**
until complete JSON is available, then emitted as raw custom-input events.
Malformed/truncated streams produce `response.failed`; unknown or duplicate
tool calls, including undeclared native call types, are rejected. Hosted search
calls require a declared and forwarded `web_search_preview` tool. Response usage
and other provider metadata survive the translation. `apply_patch` descriptions
retain caller-supplied editing constraints when their transport prose is normalized.

Provider-specific workarounds are limited to this mode:

- Orphan function/custom tool outputs without a `call_id` become explicitly
  labeled, untrusted **user** messages, never developer/system instructions.
- JSON Schema `$ref` keywords are removed from tool parameter schemas while
  preserving sibling constraints, literal property names, and example data.
  This can broaden the accepted schema: enable it only for an upstream that
  requires this workaround.
- Unsupported hosted tools are dropped (`web_search_preview` is preserved).
- `reasoning.effort=none` is removed while preserving other reasoning fields.

Model mappings, tool denylists, and upstream extra/drop parameters also apply
in this mode. A tool injected through extra parameters cannot authorize a
call absent from the original client declarations. Overrides that change the
client's streaming mode are rejected.

An incoming `x-opencode-session` header (1–256 ASCII bytes) is forwarded.
Otherwise, each new conversation gets a random identity; a known
`previous_response_id` reuses its identity. Bindings are held in memory and
bounded by `--max-sessions` and `--session-ttl-hours`. Clients replaying full
history without `previous_response_id` should provide the session header to
retain a provider session. Prompt text is never used as a session identifier.
Native conversation history remains upstream; the relay's disk history store
is used only by the Chat Completions path. Corpus recording with
`--record-corpus` is not supported in native mode and is rejected at startup.
Normalization logs contain change counts, not request contents or credentials.

The configurable inbound body limit defaults to 64 MiB **in both modes**;
raise `--max-request-bytes` / `CODEX_RELAY_MAX_REQUEST_BYTES` for larger image
requests. Requests exceeding the limit return HTTP 413.

Some providers need workarounds that are not part of the Responses ⇄ Chat Completions translation itself. These are registered as named quirks (see `src/quirks.rs` for the full registry, triggers, and removal criteria):

| Quirk | Kind | What it does |
|---|---|---|
| `glm_thinking` | request-shaping | Sends `thinking: enabled` for GLM/Zhipu models so they emit `reasoning_content` (issue #26) |
| `dsml_heal` | response-healing | Parses DeepSeek V4's intermittently leaked DSML tool-call markup in text content back into structured tool calls |
| `missing_done` | response-healing | Treats a cleanly closed SSE stream without `[DONE]` as complete when a full turn was received (issue #31) |

Response-healing quirks activate only when the anomaly is detected and log a `quirk <name> fired` warning each time, so you can tell from the logs whether a workaround is still needed. Once the platform fixes the underlying bug, disable a quirk immediately with:

```bash
CODEX_RELAY_DISABLE_QUIRKS=dsml_heal codex-relay
```

## Python API

```python
from codex_relay import start

proc = start(port=4446, upstream="https://api.deepseek.com/v1", api_key="sk-...")
# ... use Codex ...
proc.terminate()
```

## Testing

Two layers — offline tests pin behavior against captured Codex wire-shape;
live tests pin behavior against real provider APIs.

Unix shutdown tests launch the real binary and send process signals. On dynamic
Linux/glibc builds, startup-failure tests also compile a small C fault-injection
fixture with `cc`; the fixture is loaded only into the test child process.

## Debugging tool round-trips

For tool-routing issues, enable debug logs:

```bash
RUST_LOG=codex_relay=debug codex-relay
```

The relay logs tool names only, never tool arguments or message content:

- `response tools=...` — tools received from Codex's Responses API request
- `upstream tools=...` — tools forwarded to the Chat Completions upstream
- `upstream function_calls=...` — function calls returned by a blocking upstream response
- `upstream stream function_calls=...` — function calls returned by a streaming upstream response

These lines are useful for checking whether a tool such as `spawn_agent`
was preserved by the relay, and whether the failure happened before or after
the model selected that tool.

### Disk-backed history

By default, `codex-relay` keeps retained `previous_response_id` histories and
reasoning lookups in memory. For longer-running processes or deeper debugging,
you can opt into an inspectable on-disk store:

```bash
CODEX_RELAY_HISTORY_STORE=disk \
CODEX_RELAY_HISTORY_DIR=.codex-relay-history \
codex-relay
```

The disk backend writes JSON records under:

```text
.codex-relay-history/
  sessions/
  reasoning/
  turns/
```

Session records contain the translated Chat Completions `messages` retained for
a response id. New records retain conversation input (including developer/system
messages) and assistant replies, while excluding the request's top-level
`instructions`/`system`. Send those top-level instructions again on each request
where they should apply. Existing records without this separation preserve
their system content to avoid discarding conversation rules.

Reasoning records keep call-id and turn-fingerprint lookups used
to round-trip provider reasoning content. The relay keeps only an in-memory
index for disk-backed entries and loads payloads on demand.

Treat this directory as sensitive: records may contain prompts, tool outputs,
and other conversation data. The same TTL/count/byte retention knobs apply to
disk-backed records, and evicted entries are removed from disk.

### Corpus recording

For building datasets, `--record-corpus <dir>` continuously appends the
conversation flow to daily-sharded JSONL files in **OpenAI messages format**:

```bash
codex-relay --record-corpus ./corpus \
  --upstream https://api.deepseek.com/v1 --api-key "$DEEPSEEK_API_KEY"
```

This is **off by default** and is a separate subsystem from the retention cache
above: the corpus is an **append-only archive** that is never evicted, whereas
the session store is an evictable continuation cache.

Each line is an *incremental turn event* — only the messages new to that turn
are written, so the same conversation is reconstructed by concatenating the
`messages` of every event that shares a `conversation_id`:

```text
corpus/
  corpus-2026-04-04.jsonl
```

```json
{
  "conversation_id": "resp_abc…",
  "response_id": "resp_def…",
  "parent_response_id": "resp_abc…",
  "timestamp_unix_ms": 1783447750503,
  "model": "deepseek-chat",
  "messages": [ { "role": "user", "content": "…" }, { "role": "assistant", "content": "…" } ]
}
```

The `messages` payload uses the standard OpenAI schema and preserves
`tool_calls`, `role: "tool"` outputs (`tool_call_id`), and assistant
`reasoning_content` (a widely-used non-standard field that training frameworks
ignore if unknown). The first event of a conversation includes the system
prompt; subsequent events omit it. Isolated `spawn_agent` child requests start
their own `conversation_id`.

To fold the turn events back into whole-conversation OpenAI records:

```bash
jq -s 'group_by(.conversation_id)[]
       | {messages: (map(.messages) | add)}' corpus/*.jsonl
```

> ⚠️ Records contain prompts, tool call **arguments**, and tool outputs — more
> than the debug logs ever emit. Treat the directory as sensitive, especially
> when combined with `--bind` on a non-loopback address.

### Subagent tool routing

Codex subagent tools such as `spawn_agent`, `wait_agent`, and `close_agent`
are runtime tools. The relay can preserve them in the tool schema and round-trip
the model's selected function call, but it cannot reliably detect whether the
local Codex app-server daemon is new enough to execute those calls.

If Codex shows `unsupported call: spawn_agent`, first verify that the Codex CLI
and app-server daemon versions match. A stale daemon can expose a newer tool
schema to the model while lacking the handler that executes the returned call.
Also check your Codex config: `[features] subagents = true` is not recognized;
use `[features] multi_agent = true` only if you need to override the default.

As an escape hatch for affected runtimes, remove unsupported tools before they
reach the upstream model:

```bash
CODEX_RELAY_TOOL_DENYLIST=spawn_agent,wait_agent,close_agent codex-relay
```

The denylist matches the tool name forwarded to Chat Completions. Namespaced
MCP tools use their flattened name, for example
`mcp__codex_apps__github-_fetch_issue`.

**Offline (always green, default `cargo test`)**

Replays Codex CLI fixtures through the translation layer and asserts
role/tool/reasoning behavior. Each fixture pins a Codex CLI version under
`tests/fixtures/codex_<major>_<minor>_<patch>/`.

```bash
cargo test
```

**Live (gated on provider API key, `#[ignore]` by default)**

Spawns the relay binary on a random port, points it at the real provider, and
exercises `/v1/models`, blocking + streaming, tool calls, and (for thinking
models) the `reasoning_content` round-trip via an in-process recording proxy.

```bash
DEEPSEEK_API_KEY=sk-... cargo test --test compat_deepseek_live -- --ignored --test-threads=1
```

**Regenerating fixtures after a Codex upgrade**

1. Add a debug dump to the relay (write `body` bytes from `handle_responses`
   to a file before parsing).
2. Run a real `codex exec` against it; copy `inbound_*.json` to a new
   `tests/fixtures/codex_<major>_<minor>_<patch>/` folder.
3. Trim each payload down to the smallest one that exercises the feature you
   want to lock in.
4. Add a row to `tests/fixtures/VERSIONS.md` and a test pointing at the new
   directory.

The old fixture directory stays as a regression net so the relay keeps
working with the previous Codex CLI release.

## Disclaimer

This project is **not affiliated with, endorsed by, or sponsored by OpenAI**. "Codex" refers to [OpenAI Codex CLI](https://github.com/openai/codex), an open-source project licensed under Apache-2.0. codex-relay is an independent, community-built translation proxy.

## Contributors

- [myk5010](https://github.com/myk5010) — system/developer message ordering fix and model name mapping ([\#4](https://github.com/MetaFARS/codex-relay/pull/4))
- [qcnhy](https://github.com/qcnhy) — streaming usage, MCP namespace bug reports, namespace tool-routing analysis, reasoning and apply_patch feature reports, nested-namespace and encrypted-content regression reports, and independent verification ([\#5](https://github.com/MetaFARS/codex-relay/issues/5), [\#6](https://github.com/MetaFARS/codex-relay/issues/6), [\#17](https://github.com/MetaFARS/codex-relay/issues/17), [\#26](https://github.com/MetaFARS/codex-relay/issues/26), [\#37](https://github.com/MetaFARS/codex-relay/issues/37), [\#39](https://github.com/MetaFARS/codex-relay/issues/39), [\#40](https://github.com/MetaFARS/codex-relay/issues/40), [\#60](https://github.com/MetaFARS/codex-relay/issues/60), [\#61](https://github.com/MetaFARS/codex-relay/issues/61))
- [JasonC93](https://github.com/JasonC93) — subagent tool-routing and spawned-agent context isolation reports ([\#10](https://github.com/MetaFARS/codex-relay/issues/10), [\#12](https://github.com/MetaFARS/codex-relay/issues/12), [\#24](https://github.com/MetaFARS/codex-relay/issues/24))
- [ma-buting](https://github.com/ma-buting) — namespace tool-name separator fix ([\#19](https://github.com/MetaFARS/codex-relay/pull/19))
- [SaladDay](https://github.com/SaladDay) — prompt-cache accounting debug logs ([\#22](https://github.com/MetaFARS/codex-relay/pull/22))
- [Cherno76](https://github.com/Cherno76) — prompt-cache hit tokens in Responses API usage ([\#23](https://github.com/MetaFARS/codex-relay/pull/23))
- [the1here2me](https://github.com/the1here2me) — upstream parameter override request behind `--upstream-extra-params` and `--drop-upstream-params` ([\#29](https://github.com/MetaFARS/codex-relay/issues/29))
- [andrewzah](https://github.com/andrewzah) — dropped `[DONE]` recovery, leaked `<think>` markup routing, parallel-test port races, and the outdated config-syntax report ([\#31](https://github.com/MetaFARS/codex-relay/issues/31), [\#32](https://github.com/MetaFARS/codex-relay/issues/32), [\#58](https://github.com/MetaFARS/codex-relay/issues/58), [\#46](https://github.com/MetaFARS/codex-relay/pull/46))
- [0xAndoroid](https://github.com/0xAndoroid) — plaintext collaboration argument negotiation for subagents ([\#43](https://github.com/MetaFARS/codex-relay/pull/43))
- [0x5446](https://github.com/0x5446) — empty-message filtering, `reasoning.effort` forwarding, and doubled-delimiter DSML healing ([\#47](https://github.com/MetaFARS/codex-relay/pull/47), [\#48](https://github.com/MetaFARS/codex-relay/pull/48), [\#49](https://github.com/MetaFARS/codex-relay/pull/49))
- [dennisimoo](https://github.com/dennisimoo) — zero-argument tool-call wedge report ([\#57](https://github.com/MetaFARS/codex-relay/issues/57))
- [dhruv-anand-aintech](https://github.com/dhruv-anand-aintech) — opt-in OpenCode Go compatibility mode ([\#64](https://github.com/MetaFARS/codex-relay/pull/64))
- [365daysdreamer](https://github.com/365daysdreamer) — Codex instruction preservation and `apply_patch` tool-description normalization ([\#65](https://github.com/MetaFARS/codex-relay/pull/65))
- [andrei-21](https://github.com/andrei-21) — graceful shutdown with request draining ([\#66](https://github.com/MetaFARS/codex-relay/pull/66))
- [VikingOwl91](https://github.com/VikingOwl91) — Mistral structured SSE and GLM thinking compatibility, relay URL in the generated config, upstream context limits in the model catalog, and opt-in request lifecycle timing ([\#67](https://github.com/MetaFARS/codex-relay/pull/67), [\#68](https://github.com/MetaFARS/codex-relay/pull/68), [\#69](https://github.com/MetaFARS/codex-relay/pull/69), [\#70](https://github.com/MetaFARS/codex-relay/pull/70))

## License

MIT

