Skip to content

Commit 5c39f91

Browse files
committed
feat: introduce responses-api-proxy
1 parent ded6672 commit 5c39f91

File tree

8 files changed

+396
-2
lines changed

8 files changed

+396
-2
lines changed

codex-rs/Cargo.lock

Lines changed: 134 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"ollama",
1919
"protocol",
2020
"protocol-ts",
21+
"responses-api-proxy",
2122
"tui",
2223
"utils/readiness",
2324
]
@@ -49,6 +50,7 @@ codex-mcp-server = { path = "mcp-server" }
4950
codex-ollama = { path = "ollama" }
5051
codex-protocol = { path = "protocol" }
5152
codex-protocol-ts = { path = "protocol-ts" }
53+
codex-responses-api-proxy = { path = "responses-api-proxy" }
5254
codex-tui = { path = "tui" }
5355
codex-utils-readiness = { path = "utils/readiness" }
5456
core_test_support = { path = "core/tests/common" }

codex-rs/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ codex-mcp-server = { workspace = true }
2828
codex-protocol = { workspace = true }
2929
codex-protocol-ts = { workspace = true }
3030
codex-tui = { workspace = true }
31+
codex-responses-api-proxy = { workspace = true }
3132
ctor = { workspace = true }
3233
owo-colors = { workspace = true }
3334
serde_json = { workspace = true }

codex-rs/cli/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use codex_cli::login::run_logout;
1515
use codex_cli::proto;
1616
use codex_common::CliConfigOverrides;
1717
use codex_exec::Cli as ExecCli;
18+
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
1819
use codex_tui::AppExitInfo;
1920
use codex_tui::Cli as TuiCli;
2021
use owo_colors::OwoColorize;
@@ -87,6 +88,10 @@ enum Subcommand {
8788
/// Internal: generate TypeScript protocol bindings.
8889
#[clap(hide = true)]
8990
GenerateTs(GenerateTsCommand),
91+
92+
/// Internal: run the responses API proxy.
93+
#[clap(hide = true)]
94+
ResponsesApiProxy(ResponsesApiProxyArgs),
9095
}
9196

9297
#[derive(Debug, Parser)]
@@ -234,7 +239,7 @@ fn main() -> anyhow::Result<()> {
234239
async fn cli_main(pre_main_args: PreMainArgs) -> anyhow::Result<()> {
235240
let PreMainArgs {
236241
codex_linux_sandbox_exe,
237-
openai_api_key: _,
242+
openai_api_key,
238243
} = pre_main_args;
239244

240245
let MultitoolCli {
@@ -347,6 +352,11 @@ async fn cli_main(pre_main_args: PreMainArgs) -> anyhow::Result<()> {
347352
Some(Subcommand::GenerateTs(gen_cli)) => {
348353
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
349354
}
355+
Some(Subcommand::ResponsesApiProxy(args)) => {
356+
let api_key =
357+
openai_api_key.ok_or_else(|| anyhow::anyhow!("OPENAI_API_KEY must be set"))?;
358+
codex_responses_api_proxy::run_main(api_key, args)?;
359+
}
350360
}
351361

352362
Ok(())
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
edition = "2024"
3+
name = "codex-responses-api-proxy"
4+
version = { workspace = true }
5+
6+
[lib]
7+
name = "codex_responses_api_proxy"
8+
path = "src/lib.rs"
9+
10+
[[bin]]
11+
name = "responses-api-proxy"
12+
path = "src/main.rs"
13+
14+
[lints]
15+
workspace = true
16+
17+
[dependencies]
18+
anyhow = { workspace = true }
19+
clap = { workspace = true, features = ["derive"] }
20+
codex-arg0 = { workspace = true }
21+
libc = { workspace = true }
22+
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }
23+
serde = { workspace = true, features = ["derive"] }
24+
serde_json = { workspace = true }
25+
tiny_http = { workspace = true }
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# codex-responses-api-proxy
2+
3+
A minimal HTTP proxy that only forwards POST requests to `/v1/responses` to the OpenAI API, injecting the `Authorization: Bearer $OPENAI_API_KEY` header. Everything else is rejected with `403 Forbidden`.
4+
5+
**IMPORTANT:** This is designed to be used with `CODEX_SECURE_MODE=1` so that an unprivileged user cannot inspect or tamper with this process. Though if `--http-shutdown` is specified, an unprivileged user _can_ shutdown the server.
6+
7+
## Behavior
8+
9+
- Reads `OPENAI_API_KEY` from the environment at startup. On Unix platforms, it attempts to `mlock(2)` the memory holding the key so it is not swapped to disk.
10+
- Immediately removes `OPENAI_API_KEY` from the process environment after reading it to avoid leaving the key in unprotected env storage. The shared `arg0_dispatch_or_else()` helper handles this before Tokio spins up.
11+
- Listens on the provided port or an ephemeral port if `--port` is not specified.
12+
- Accepts exactly `POST /v1/responses` (no query string). The request body is forwarded to `https://api.openai.com/v1/responses` with `Authorization: Bearer <key>` set. All original request headers (except any incoming `Authorization`) are forwarded upstream. For other requests, it responds with `403`.
13+
- Optionally writes a single-line JSON file with startup info, currently `{ "port": <u16> }`.
14+
- Optional `--http-shutdown` enables `GET /shutdown` to terminate the process with exit code 0. This allows one user (e.g., root) to start the proxy and another unprivileged user on the host to shut it down.
15+
16+
## CLI
17+
18+
```
19+
responses-api-proxy [--port <PORT>] [--startup-info <FILE>] [--http-shutdown]
20+
```
21+
22+
- `--port <PORT>`: Port to bind on `127.0.0.1`. If omitted, an ephemeral port is chosen.
23+
- `--startup-info <FILE>`: If set, the proxy writes a single line of JSON with `{ "port": <PORT> }` once listening.
24+
- `--http-shutdown`: If set, enables `GET /shutdown` to exit the process with code `0`.
25+
26+
## Notes
27+
28+
- Only `POST /v1/responses` is permitted. No query strings are allowed.
29+
- All request headers are forwarded to the upstream call (aside from overriding `Authorization`). Response status and content-type are mirrored from upstream.

0 commit comments

Comments
 (0)