Add CodexHttpClient wrapper with request logging (#5564)

## Summary
- wrap the default reqwest::Client inside a new
CodexHttpClient/CodexRequestBuilder pair and log the HTTP method, URL,
and status for each request
- update the auth/model/provider plumbing to use the new builder helpers
so headers and bearer auth continue to be applied consistently
- add the shared `http` dependency that backs the header conversion
helpers

## Testing
- `CODEX_SANDBOX=seatbelt CODEX_SANDBOX_NETWORK_DISABLED=1 cargo test -p
codex-core`
- `CODEX_SANDBOX=seatbelt CODEX_SANDBOX_NETWORK_DISABLED=1 cargo test -p
codex-chatgpt`
- `CODEX_SANDBOX=seatbelt CODEX_SANDBOX_NETWORK_DISABLED=1 cargo test -p
codex-tui`

------
https://chatgpt.com/codex/tasks/task_i_68fa5038c17483208b1148661c5873be
This commit is contained in:
pakrym-oai
2025-10-24 09:47:52 -07:00
committed by GitHub
parent c72b2ad766
commit 061862a0e2
8 changed files with 148 additions and 20 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1075,6 +1075,7 @@ dependencies = [
"escargot", "escargot",
"eventsource-stream", "eventsource-stream",
"futures", "futures",
"http",
"indexmap 2.10.0", "indexmap 2.10.0",
"landlock", "landlock",
"libc", "libc",

View File

@@ -116,6 +116,7 @@ env_logger = "0.11.5"
escargot = "0.5" escargot = "0.5"
eventsource-stream = "0.2.3" eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false } futures = { version = "0.3", default-features = false }
http = "1.3.1"
icu_decimal = "2.0.0" icu_decimal = "2.0.0"
icu_locale_core = "2.0.0" icu_locale_core = "2.0.0"
ignore = "0.4.23" ignore = "0.4.23"

View File

@@ -34,6 +34,7 @@ dunce = { workspace = true }
env-flags = { workspace = true } env-flags = { workspace = true }
eventsource-stream = { workspace = true } eventsource-stream = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
http = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
mcp-types = { workspace = true } mcp-types = { workspace = true }

View File

@@ -21,6 +21,7 @@ use codex_app_server_protocol::AuthMode;
use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ForcedLoginMethod;
use crate::config::Config; use crate::config::Config;
use crate::default_client::CodexHttpClient;
use crate::token_data::PlanType; use crate::token_data::PlanType;
use crate::token_data::TokenData; use crate::token_data::TokenData;
use crate::token_data::parse_id_token; use crate::token_data::parse_id_token;
@@ -32,7 +33,7 @@ pub struct CodexAuth {
pub(crate) api_key: Option<String>, pub(crate) api_key: Option<String>,
pub(crate) auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>, pub(crate) auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
pub(crate) auth_file: PathBuf, pub(crate) auth_file: PathBuf,
pub(crate) client: reqwest::Client, pub(crate) client: CodexHttpClient,
} }
impl PartialEq for CodexAuth { impl PartialEq for CodexAuth {
@@ -180,7 +181,7 @@ impl CodexAuth {
} }
} }
fn from_api_key_with_client(api_key: &str, client: reqwest::Client) -> Self { fn from_api_key_with_client(api_key: &str, client: CodexHttpClient) -> Self {
Self { Self {
api_key: Some(api_key.to_owned()), api_key: Some(api_key.to_owned()),
mode: AuthMode::ApiKey, mode: AuthMode::ApiKey,
@@ -400,7 +401,7 @@ async fn update_tokens(
async fn try_refresh_token( async fn try_refresh_token(
refresh_token: String, refresh_token: String,
client: &reqwest::Client, client: &CodexHttpClient,
) -> std::io::Result<RefreshResponse> { ) -> std::io::Result<RefreshResponse> {
let refresh_request = RefreshRequest { let refresh_request = RefreshRequest {
client_id: CLIENT_ID, client_id: CLIENT_ID,

View File

@@ -4,6 +4,7 @@ use crate::ModelProviderInfo;
use crate::client_common::Prompt; use crate::client_common::Prompt;
use crate::client_common::ResponseEvent; use crate::client_common::ResponseEvent;
use crate::client_common::ResponseStream; use crate::client_common::ResponseStream;
use crate::default_client::CodexHttpClient;
use crate::error::CodexErr; use crate::error::CodexErr;
use crate::error::ConnectionFailedError; use crate::error::ConnectionFailedError;
use crate::error::ResponseStreamFailed; use crate::error::ResponseStreamFailed;
@@ -36,7 +37,7 @@ use tracing::trace;
pub(crate) async fn stream_chat_completions( pub(crate) async fn stream_chat_completions(
prompt: &Prompt, prompt: &Prompt,
model_family: &ModelFamily, model_family: &ModelFamily,
client: &reqwest::Client, client: &CodexHttpClient,
provider: &ModelProviderInfo, provider: &ModelProviderInfo,
otel_event_manager: &OtelEventManager, otel_event_manager: &OtelEventManager,
) -> Result<ResponseStream> { ) -> Result<ResponseStream> {

View File

@@ -39,6 +39,7 @@ use crate::client_common::ResponsesApiRequest;
use crate::client_common::create_reasoning_param_for_request; use crate::client_common::create_reasoning_param_for_request;
use crate::client_common::create_text_param_for_request; use crate::client_common::create_text_param_for_request;
use crate::config::Config; use crate::config::Config;
use crate::default_client::CodexHttpClient;
use crate::default_client::create_client; use crate::default_client::create_client;
use crate::error::CodexErr; use crate::error::CodexErr;
use crate::error::ConnectionFailedError; use crate::error::ConnectionFailedError;
@@ -81,7 +82,7 @@ pub struct ModelClient {
config: Arc<Config>, config: Arc<Config>,
auth_manager: Option<Arc<AuthManager>>, auth_manager: Option<Arc<AuthManager>>,
otel_event_manager: OtelEventManager, otel_event_manager: OtelEventManager,
client: reqwest::Client, client: CodexHttpClient,
provider: ModelProviderInfo, provider: ModelProviderInfo,
conversation_id: ConversationId, conversation_id: ConversationId,
effort: Option<ReasoningEffortConfig>, effort: Option<ReasoningEffortConfig>,
@@ -335,13 +336,6 @@ impl ModelClient {
.headers() .headers()
.get("cf-ray") .get("cf-ray")
.map(|v| v.to_str().unwrap_or_default().to_string()); .map(|v| v.to_str().unwrap_or_default().to_string());
debug!(
"Response status: {}, cf-ray: {:?}, version: {:?}",
resp.status(),
request_id,
resp.version()
);
} }
match res { match res {

View File

@@ -1,5 +1,12 @@
use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use http::Error as HttpError;
use reqwest::IntoUrl;
use reqwest::Method;
use reqwest::Response;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue; use reqwest::header::HeaderValue;
use serde::Serialize;
use std::fmt::Display;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -22,6 +29,125 @@ use std::sync::OnceLock;
pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None)); pub static USER_AGENT_SUFFIX: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs"; pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
#[derive(Clone, Debug)]
pub struct CodexHttpClient {
inner: reqwest::Client,
}
impl CodexHttpClient {
fn new(inner: reqwest::Client) -> Self {
Self { inner }
}
pub fn get<U>(&self, url: U) -> CodexRequestBuilder
where
U: IntoUrl,
{
self.request(Method::GET, url)
}
pub fn post<U>(&self, url: U) -> CodexRequestBuilder
where
U: IntoUrl,
{
self.request(Method::POST, url)
}
pub fn request<U>(&self, method: Method, url: U) -> CodexRequestBuilder
where
U: IntoUrl,
{
let url_str = url.as_str().to_string();
CodexRequestBuilder::new(self.inner.request(method.clone(), url), method, url_str)
}
}
#[must_use = "requests are not sent unless `send` is awaited"]
#[derive(Debug)]
pub struct CodexRequestBuilder {
builder: reqwest::RequestBuilder,
method: Method,
url: String,
}
impl CodexRequestBuilder {
fn new(builder: reqwest::RequestBuilder, method: Method, url: String) -> Self {
Self {
builder,
method,
url,
}
}
fn map(self, f: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder) -> Self {
Self {
builder: f(self.builder),
method: self.method,
url: self.url,
}
}
pub fn header<K, V>(self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
self.map(|builder| builder.header(key, value))
}
pub fn bearer_auth<T>(self, token: T) -> Self
where
T: Display,
{
self.map(|builder| builder.bearer_auth(token))
}
pub fn json<T>(self, value: &T) -> Self
where
T: ?Sized + Serialize,
{
self.map(|builder| builder.json(value))
}
pub async fn send(self) -> Result<Response, reqwest::Error> {
match self.builder.send().await {
Ok(response) => {
let request_id = response
.headers()
.get("cf-ray")
.map(|v| v.to_str().unwrap_or_default().to_string())
.unwrap_or_default();
let version = format!("{:?}", response.version());
tracing::debug!(
method = %self.method,
url = %self.url,
status = %response.status(),
request_id = %request_id,
version = %version,
"Request completed"
);
Ok(response)
}
Err(error) => {
let status = error.status();
tracing::debug!(
method = %self.method,
url = %self.url,
status = status.map(|s| s.as_u16()),
error = %error,
"Request failed"
);
Err(error)
}
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Originator { pub struct Originator {
pub value: String, pub value: String,
@@ -124,8 +250,8 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String {
} }
} }
/// Create a reqwest client with default `originator` and `User-Agent` headers set. /// Create an HTTP client with default `originator` and `User-Agent` headers set.
pub fn create_client() -> reqwest::Client { pub fn create_client() -> CodexHttpClient {
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@@ -140,7 +266,8 @@ pub fn create_client() -> reqwest::Client {
builder = builder.no_proxy(); builder = builder.no_proxy();
} }
builder.build().unwrap_or_else(|_| reqwest::Client::new()) let inner = builder.build().unwrap_or_else(|_| reqwest::Client::new());
CodexHttpClient::new(inner)
} }
fn is_sandboxed() -> bool { fn is_sandboxed() -> bool {

View File

@@ -6,6 +6,8 @@
//! key. These override or extend the defaults at runtime. //! key. These override or extend the defaults at runtime.
use crate::CodexAuth; use crate::CodexAuth;
use crate::default_client::CodexHttpClient;
use crate::default_client::CodexRequestBuilder;
use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::AuthMode;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
@@ -95,7 +97,7 @@ pub struct ModelProviderInfo {
impl ModelProviderInfo { impl ModelProviderInfo {
/// Construct a `POST` RequestBuilder for the given URL using the provided /// Construct a `POST` RequestBuilder for the given URL using the provided
/// reqwest Client applying: /// [`CodexHttpClient`] applying:
/// • provider-specific headers (static + env based) /// • provider-specific headers (static + env based)
/// • Bearer auth header when an API key is available. /// • Bearer auth header when an API key is available.
/// • Auth token for OAuth. /// • Auth token for OAuth.
@@ -104,9 +106,9 @@ impl ModelProviderInfo {
/// one produced by [`ModelProviderInfo::api_key`]. /// one produced by [`ModelProviderInfo::api_key`].
pub async fn create_request_builder<'a>( pub async fn create_request_builder<'a>(
&'a self, &'a self,
client: &'a reqwest::Client, client: &'a CodexHttpClient,
auth: &Option<CodexAuth>, auth: &Option<CodexAuth>,
) -> crate::error::Result<reqwest::RequestBuilder> { ) -> crate::error::Result<CodexRequestBuilder> {
let effective_auth = if let Some(secret_key) = &self.experimental_bearer_token { let effective_auth = if let Some(secret_key) = &self.experimental_bearer_token {
Some(CodexAuth::from_api_key(secret_key)) Some(CodexAuth::from_api_key(secret_key))
} else { } else {
@@ -187,9 +189,9 @@ impl ModelProviderInfo {
} }
/// Apply provider-specific HTTP headers (both static and environment-based) /// Apply provider-specific HTTP headers (both static and environment-based)
/// onto an existing `reqwest::RequestBuilder` and return the updated /// onto an existing [`CodexRequestBuilder`] and return the updated
/// builder. /// builder.
fn apply_http_headers(&self, mut builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { fn apply_http_headers(&self, mut builder: CodexRequestBuilder) -> CodexRequestBuilder {
if let Some(extra) = &self.http_headers { if let Some(extra) = &self.http_headers {
for (k, v) in extra { for (k, v) in extra {
builder = builder.header(k, v); builder = builder.header(k, v);