[codex] add responses proxy JSON dumps (#16753)

This makes Responses API proxy request/response dumping first-class by
adding an optional `--dump-dir` flag that emits paired JSON files with
shared sequence/timestamp prefixes, captures full request and response
headers and records parsed JSON bodies.
This commit is contained in:
Thibault Sottiaux
2026-04-03 16:51:18 -10:00
committed by GitHub
parent 13d828d236
commit 6edb865cc6
5 changed files with 409 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
use std::fs::File;
use std::fs::{self};
use std::io::Read;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpListener;
@@ -27,7 +28,9 @@ use tiny_http::Response;
use tiny_http::Server;
use tiny_http::StatusCode;
mod dump;
mod read_api_key;
use dump::ExchangeDumper;
use read_api_key::read_auth_header_from_stdin;
/// CLI arguments for the proxy.
@@ -49,6 +52,10 @@ pub struct Args {
/// Absolute URL the proxy should forward requests to (defaults to OpenAI).
#[arg(long, default_value = "https://api.openai.com/v1/responses")]
pub upstream_url: String,
/// Directory where request/response dumps should be written as JSON.
#[arg(long, value_name = "DIR")]
pub dump_dir: Option<PathBuf>,
}
#[derive(Serialize)]
@@ -79,6 +86,12 @@ pub fn run_main(args: Args) -> Result<()> {
upstream_url,
host_header,
});
let dump_dir = args
.dump_dir
.map(ExchangeDumper::new)
.transpose()
.context("creating --dump-dir")?
.map(Arc::new);
let (listener, bound_addr) = bind_listener(args.port)?;
if let Some(path) = args.server_info.as_ref() {
@@ -100,13 +113,20 @@ pub fn run_main(args: Args) -> Result<()> {
for request in server.incoming_requests() {
let client = client.clone();
let forward_config = forward_config.clone();
let dump_dir = dump_dir.clone();
std::thread::spawn(move || {
if http_shutdown && request.method() == &Method::Get && request.url() == "/shutdown" {
let _ = request.respond(Response::new_empty(StatusCode(200)));
std::process::exit(0);
}
if let Err(e) = forward_request(&client, auth_header, &forward_config, request) {
if let Err(e) = forward_request(
&client,
auth_header,
&forward_config,
dump_dir.as_deref(),
request,
) {
eprintln!("forwarding error: {e}");
}
});
@@ -144,6 +164,7 @@ fn forward_request(
client: &Client,
auth_header: &'static str,
config: &ForwardConfig,
dump_dir: Option<&ExchangeDumper>,
mut req: Request,
) -> Result<()> {
// Only allow POST /v1/responses exactly, no query string.
@@ -159,8 +180,18 @@ fn forward_request(
// Read request body
let mut body = Vec::new();
let mut reader = req.as_reader();
std::io::Read::read_to_end(&mut reader, &mut body)?;
let reader = req.as_reader();
reader.read_to_end(&mut body)?;
let exchange_dump = dump_dir.and_then(|dump_dir| {
dump_dir
.dump_request(&method, &url_path, req.headers(), &body)
.map_err(|err| {
eprintln!("responses-api-proxy failed to dump request: {err}");
err
})
.ok()
});
// Build headers for upstream, forwarding everything from the incoming
// request except Authorization (we replace it below).
@@ -224,10 +255,17 @@ fn forward_request(
}
});
let response_body: Box<dyn Read + Send> = if let Some(exchange_dump) = exchange_dump {
let headers = upstream_resp.headers().clone();
Box::new(exchange_dump.tee_response_body(status.as_u16(), &headers, upstream_resp))
} else {
Box::new(upstream_resp)
};
let response = Response::new(
StatusCode(status.as_u16()),
response_headers,
upstream_resp,
response_body,
content_length,
None,
);