codex/cli/
responses_api_proxy.rs1use crate::CodexError;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::{collections::BTreeMap, path::PathBuf};
5use tokio::fs;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct ResponsesApiProxyRequest {
10 pub api_key: String,
12 pub port: Option<u16>,
14 pub server_info_path: Option<PathBuf>,
16 pub http_shutdown: bool,
18 pub upstream_url: Option<String>,
20}
21
22impl ResponsesApiProxyRequest {
23 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 pub fn port(mut self, port: u16) -> Self {
36 self.port = Some(port);
37 self
38 }
39
40 pub fn server_info(mut self, path: impl Into<PathBuf>) -> Self {
42 self.server_info_path = Some(path.into());
43 self
44 }
45
46 pub fn http_shutdown(mut self, enable: bool) -> Self {
48 self.http_shutdown = enable;
49 self
50 }
51
52 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#[derive(Debug)]
62pub struct ResponsesApiProxyHandle {
63 pub child: tokio::process::Child,
65 pub server_info_path: Option<PathBuf>,
67}
68
69impl ResponsesApiProxyHandle {
70 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#[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}