Skip to main content

codex/cli/
responses_api_proxy.rs

1use crate::CodexError;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::{collections::BTreeMap, path::PathBuf};
5use tokio::fs;
6
7/// Request for `codex responses-api-proxy`.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct ResponsesApiProxyRequest {
10    /// API key to write to stdin on startup.
11    pub api_key: String,
12    /// Optional port to bind; falls back to an OS-assigned ephemeral port when omitted.
13    pub port: Option<u16>,
14    /// Optional path passed to `--server-info` for `{port,pid}` JSON output.
15    pub server_info_path: Option<PathBuf>,
16    /// Enables the HTTP shutdown endpoint (`GET /shutdown`).
17    pub http_shutdown: bool,
18    /// Optional upstream URL passed to `--upstream-url` (defaults to `https://api.openai.com/v1/responses`).
19    pub upstream_url: Option<String>,
20}
21
22impl ResponsesApiProxyRequest {
23    /// Creates a request with the API key provided via stdin.
24    pub fn new(api_key: impl Into<String>) -> Self {
25        Self {
26            api_key: api_key.into(),
27            port: None,
28            server_info_path: None,
29            http_shutdown: false,
30            upstream_url: None,
31        }
32    }
33
34    /// Sets the listening port (`--port`).
35    pub fn port(mut self, port: u16) -> Self {
36        self.port = Some(port);
37        self
38    }
39
40    /// Writes `{port,pid}` JSON to the provided path via `--server-info`.
41    pub fn server_info(mut self, path: impl Into<PathBuf>) -> Self {
42        self.server_info_path = Some(path.into());
43        self
44    }
45
46    /// Enables the `--http-shutdown` flag (GET /shutdown).
47    pub fn http_shutdown(mut self, enable: bool) -> Self {
48        self.http_shutdown = enable;
49        self
50    }
51
52    /// Overrides the upstream responses endpoint URL.
53    pub fn upstream_url(mut self, url: impl Into<String>) -> Self {
54        let url = url.into();
55        self.upstream_url = (!url.trim().is_empty()).then_some(url);
56        self
57    }
58}
59
60/// Running responses proxy process and metadata.
61#[derive(Debug)]
62pub struct ResponsesApiProxyHandle {
63    /// Spawned `codex responses-api-proxy` child (inherits kill-on-drop).
64    pub child: tokio::process::Child,
65    /// Optional `--server-info` path that may contain `{port,pid}` JSON.
66    pub server_info_path: Option<PathBuf>,
67}
68
69impl ResponsesApiProxyHandle {
70    /// Reads and parses the `{port,pid}` JSON written by `--server-info`.
71    ///
72    /// Returns `Ok(None)` when no server info path was configured.
73    pub async fn read_server_info(&self) -> Result<Option<ResponsesApiProxyInfo>, CodexError> {
74        let Some(path) = &self.server_info_path else {
75            return Ok(None);
76        };
77
78        const MAX_ATTEMPTS: usize = 10;
79        const BACKOFF_MS: u64 = 25;
80
81        for attempt in 0..MAX_ATTEMPTS {
82            match fs::read_to_string(path).await {
83                Ok(contents) => match serde_json::from_str::<ResponsesApiProxyInfo>(&contents) {
84                    Ok(info) => return Ok(Some(info)),
85                    Err(source) => {
86                        if attempt + 1 == MAX_ATTEMPTS {
87                            return Err(CodexError::ResponsesApiProxyInfoParse {
88                                path: path.clone(),
89                                source,
90                            });
91                        }
92                    }
93                },
94                Err(source) => {
95                    let is_missing = source.kind() == std::io::ErrorKind::NotFound;
96                    if !is_missing || attempt + 1 == MAX_ATTEMPTS {
97                        return Err(CodexError::ResponsesApiProxyInfoRead {
98                            path: path.clone(),
99                            source,
100                        });
101                    }
102                }
103            }
104
105            tokio::time::sleep(std::time::Duration::from_millis(BACKOFF_MS)).await;
106        }
107
108        unreachable!("read_server_info loop must return by MAX_ATTEMPTS")
109    }
110}
111
112/// Parsed `{port,pid}` emitted by `codex responses-api-proxy --server-info`.
113#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
114pub struct ResponsesApiProxyInfo {
115    pub port: u16,
116    pub pid: u32,
117    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
118    pub extra: BTreeMap<String, Value>,
119}