codex-responses-adapter is a local Rust proxy that lets Codex talk to providers that expose an OpenAI-compatible Chat Completions API, even when Codex itself only speaks the OpenAI Responses API.
It accepts POST /v1/responses from Codex, translates requests into /chat/completions for downstream providers such as GLM, MiniMax, vLLM, or custom OpenAI-compatible endpoints, and converts the result back into a Responses-shaped response.
Codex CLI expects a Responses API endpoint. Many third-party model providers still expose Chat Completions instead. This adapter bridges that mismatch so you can keep wire_api = "responses" in Codex while routing inference to non-OpenAI backends.
- Responses API to Chat Completions translation
- Streaming and non-streaming response conversion
- Function tool round-trip support
- Model name mapping from Codex model IDs to upstream model IDs
- Multi-provider routing with ordered fallback
- Provider-specific path override via
POST /v1/{provider}/responses - Optional bearer-token passthrough with
use_incoming_auth = true - Provider capability handling, including downgrade behavior for unsupported features
<think>...</think>filtering in both normal and streaming responses- Adapter-managed
web_searchvia Tavily, Brave, or a custom backend - Native
/responsespassthrough for providers that already support the Responses API
This is a working adapter for the current MVP and early extension scope. The repository currently includes unit coverage for request conversion, response conversion, search routing, and config parsing.
As of March 11, 2026, cargo test passes with 37 tests.
cargo build --releasecp codex-responses-adapter.example.toml ~/.codex-responses-adapter.tomlEdit ~/.codex-responses-adapter.toml to point at your upstream providers and model mappings.
Example:
export GLM_API_KEY="your-glm-key"
export MINIMAX_API_KEY="your-minimax-key"
export TAVILY_API_KEY="your-tavily-key"./target/release/codex-responses-adapterThe server listens on 127.0.0.1:3000 by default.
curl http://127.0.0.1:3000/healthExpected response:
{"status":"ok"}Point Codex at the adapter instead of a provider directly:
# ~/.codex/config.toml
model_provider = "glm-adapter"
[model_providers.glm-adapter]
name = "GLM Adapter"
base_url = "http://127.0.0.1:3000/v1"
wire_api = "responses"
env_key = "GLM_API_KEY"The adapter then maps Codex model names to your configured upstream provider/model pairs.
- Start
codex-responses-adapter - Confirm
GET /healthreturns{"status":"ok"} - Start Codex
If the adapter is not running first, Codex will fail on the initial request to http://127.0.0.1:3000/v1.
If you keep multiple providers in Codex, create a dedicated profile for the adapter:
# ~/.codex/config.toml
model_provider = "responses-adapter"
[model_providers.responses-adapter]
name = "Responses Adapter"
base_url = "http://127.0.0.1:3000/v1"
wire_api = "responses"
env_key = "GLM_API_KEY"env_key is still required by Codex config, but in practice upstream authentication is handled by the adapter config:
api_key_envorapi_keyfor provider-owned credentialsuse_incoming_auth = truewhen you want to pass through the bearer token from Codex
Codex sends a model name such as o4-mini. The adapter uses [[models]] in ~/.codex-responses-adapter.toml to choose the actual upstream route.
Example:
[[models]]
name = "o4-mini"
routes = [
{ provider = "glm", model = "glm-4-flash" },
{ provider = "minimax", model = "MiniMax-Text-01" }
]That means:
- Codex asks for
o4-mini - the adapter tries
glm/glm-4-flashfirst - if that route fails, it falls back to
minimax/MiniMax-Text-01
Once the adapter is running, normal Codex usage stays the same.
Interactive session:
codexOne-shot execution:
codex exec "Say hello in one sentence"If your local Codex setup uses profiles, call the adapter profile explicitly:
codex --profile responses-adapter
codex exec --profile responses-adapter "Summarize the purpose of this repository"The default Codex path is POST /v1/responses, which follows the adapter's routing table.
If you want to pin requests to one provider for debugging, create a separate Codex provider entry that points directly at:
http://127.0.0.1:3000/v1/glm/responseshttp://127.0.0.1:3000/v1/minimax/responses
Example:
[model_providers.responses-adapter-glm]
name = "Responses Adapter GLM"
base_url = "http://127.0.0.1:3000/v1/glm"
wire_api = "responses"
env_key = "GLM_API_KEY"This bypasses model-level fallback and forces the named adapter provider.
When Codex triggers web_search, the adapter chooses one of two behaviors:
- passthrough to upstream
/responsesif the selected provider supports the Responses API - adapter-managed search if
[web_search]is enabled and a backend is configured
If neither path is available, the request fails clearly instead of silently dropping search.
[server]
port = 3000
allow_downgrade = true
[providers.glm]
provider_type = "glm"
upstream_url = "https://open.bigmodel.cn/api/paas/v4"
api_key_env = "GLM_API_KEY"
[providers.minimax]
provider_type = "minimax"
upstream_url = "https://api.minimaxi.com/v1"
api_key_env = "MINIMAX_API_KEY"
[[models]]
name = "o4-mini"
routes = [
{ provider = "glm", model = "glm-4-flash" },
{ provider = "minimax", model = "MiniMax-Text-01" }
]
[default_route]
provider = "glm"
model = "glm-4-flash"For a fuller example, see codex-responses-adapter.example.toml.
If you do not want a config file, the adapter can run in single-provider mode:
codex-responses-adapter \
--upstream-url https://open.bigmodel.cn/api/paas/v4 \
--provider glm \
--api-key-env GLM_API_KEY \
--default-model glm-4-flash \
--model-map "o4-mini=glm-4-flash,o3=glm-4"| Method | Path | Purpose |
|---|---|---|
POST |
/v1/responses |
Default adapter entrypoint |
POST |
/v1/{provider}/responses |
Force a specific configured provider |
GET |
/health |
Health check |
The adapter supports two search paths:
- Native passthrough to upstream
POST /responseswhen the selected provider supports the Responses API. - Adapter-managed search when
web_searchis enabled and a backend is configured.
Supported adapter-managed backends:
- Tavily
- Brave
- Custom JSON API
Example:
[web_search]
enabled = true
strategy = "prefer_passthrough"
backend = "tavily"
max_results = 5
timeout_seconds = 10
allow_backend_fallback = true
[web_search.tavily]
api_key_env = "TAVILY_API_KEY"glm: chat translation path by defaultminimax: merges developer/system text into one leading system message for compatibilityopenai: can be configured for direct Responses passthrough withsupports_responses_api = truevllm: supported as a generic OpenAI-compatible downstream, but some capabilities may require downgradecustom: use when the upstream is OpenAI-compatible but not covered by a preset
previous_response_idis not supported- Server-side conversation state is not implemented
- Hosted tools are not generically emulated; only the current web-search path has adapter support
- Some Responses features may be dropped or downgraded depending on downstream provider capabilities
- The adapter binds to
127.0.0.1by default rather than all interfaces
Check:
- your upstream
upstream_url - your API key environment variables
- your model mapping names
- whether the selected provider supports the requested feature set
Check:
[web_search].enabled = true- a valid
backendis configured - the corresponding backend API key is present
- whether your route should use passthrough or adapter-managed search
Set:
[providers.openai]
provider_type = "openai"
upstream_url = "https://api.openai.com/v1"
use_incoming_auth = true
supports_responses_api = truecargo testKey files:
- src/main.rs
- src/handler.rs
- src/request_converter.rs
- src/response_converter.rs
- src/web_search.rs
- docs/specs/2026-03-11-responses-adapter-design.md
- docs/specs/2026-03-11-search-routing.md
This repository is licensed under the MIT License.