Reduce dependency coupling in shared configuration crates (#47713)

## What changed

- Move `Provider` and `RetryConfig` into `codex-client`, retaining re-exports from `codex-api`, and switch `codex-model-provider-info` to depend on `codex-client`.
- Move feature-state metric emission into `codex-core`, preserving its behavior while removing the `codex-features` dependency on `codex-otel`.
- Explicitly enable TLS features for `tonic` in `codex-config` and `reqwest` in `codex-http-client`.
- Enable `png`'s `zlib-rs` feature directly in `codex-utils-image` to keep encoded PNG bytes stable when WebSocket dependencies are absent.

GitOrigin-RevId: 91fd63ab01b55d516b3e80e3644d06af8d5a01f6
This commit is contained in:
Ahmed Ibrahim
2026-09-24 01:50:36 +00:00
committed by copyberry
parent 4891c4e35f
commit f5960fcc22
13 changed files with 140 additions and 129 deletions
+3 -2
View File
@@ -2578,6 +2578,7 @@ dependencies = [
"rand 0.9.3",
"tokio",
"tracing",
"url",
]
[[package]]
@@ -3338,7 +3339,6 @@ name = "codex-features"
version = "0.0.0"
dependencies = [
"codex-network-proxy",
"codex-otel",
"codex-protocol",
"pretty_assertions",
"schemars 0.8.22",
@@ -3989,7 +3989,7 @@ dependencies = [
name = "codex-model-provider-info"
version = "0.0.0"
dependencies = [
"codex-api",
"codex-client",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-redacted-string",
@@ -4956,6 +4956,7 @@ dependencies = [
"divan",
"image",
"mime_guess",
"png",
"thiserror 2.0.18",
"tokio",
]
+2 -101
View File
@@ -1,104 +1,5 @@
use codex_client::Request;
use codex_client::RequestCompression;
use codex_client::RetryOn;
use codex_client::RetryPolicy;
use http::Method;
use http::header::HeaderMap;
use std::collections::HashMap;
use std::time::Duration;
use url::Url;
/// High-level retry configuration for a provider.
///
/// This is converted into a `RetryPolicy` used by `codex-client` to drive
/// transport-level retries for both unary and streaming calls.
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u64,
pub base_delay: Duration,
pub retry_429: bool,
pub retry_5xx: bool,
pub retry_transport: bool,
}
impl RetryConfig {
pub fn to_policy(&self) -> RetryPolicy {
RetryPolicy {
max_attempts: self.max_attempts,
base_delay: self.base_delay,
retry_on: RetryOn {
retry_429: self.retry_429,
retry_5xx: self.retry_5xx,
retry_transport: self.retry_transport,
},
}
}
}
/// HTTP endpoint configuration used to talk to a concrete API deployment.
///
/// Encapsulates base URL, default headers, query params, retry policy, and
/// stream idle timeout, plus helper methods for building requests.
#[derive(Debug, Clone)]
pub struct Provider {
pub name: String,
pub base_url: String,
pub query_params: Option<HashMap<String, String>>,
pub headers: HeaderMap,
pub retry: RetryConfig,
pub stream_idle_timeout: Duration,
}
impl Provider {
pub fn url_for_path(&self, path: &str) -> String {
let base = self.base_url.trim_end_matches('/');
let path = path.trim_start_matches('/');
let mut url = if path.is_empty() {
base.to_string()
} else {
format!("{base}/{path}")
};
if let Some(params) = &self.query_params
&& !params.is_empty()
{
let qs = params
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
url.push('?');
url.push_str(&qs);
}
url
}
pub fn build_request(&self, method: Method, path: &str) -> Request {
Request {
method,
url: self.url_for_path(path),
headers: self.headers.clone(),
body: None,
compression: RequestCompression::None,
timeout: None,
response_body_limit_bytes: None,
}
}
pub fn websocket_url_for_path(&self, path: &str) -> Result<Url, url::ParseError> {
let mut url = Url::parse(&self.url_for_path(path))?;
let scheme = match url.scheme() {
"http" => "ws",
"https" => "wss",
"ws" | "wss" => return Ok(url),
_ => return Ok(url),
};
let _ = url.set_scheme(scheme);
Ok(url)
}
}
pub use codex_client::Provider;
pub use codex_client::RetryConfig;
pub fn is_azure_responses_provider(name: &str, base_url: Option<&str>) -> bool {
if name.eq_ignore_ascii_case("azure") {
+1
View File
@@ -12,6 +12,7 @@ http = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] }
tracing = { workspace = true }
url = { workspace = true }
[lints]
workspace = true
+3
View File
@@ -1,7 +1,10 @@
mod provider;
mod retry;
mod sse;
mod telemetry;
pub use crate::provider::Provider;
pub use crate::provider::RetryConfig;
pub use crate::retry::RetryOn;
pub use crate::retry::RetryOperation;
pub use crate::retry::RetryPolicy;
+103
View File
@@ -0,0 +1,103 @@
//! Endpoint configuration and request construction shared by API clients.
use crate::Request;
use crate::RequestCompression;
use crate::RetryOn;
use crate::RetryPolicy;
use http::Method;
use http::header::HeaderMap;
use std::collections::HashMap;
use std::time::Duration;
use url::Url;
/// High-level retry configuration for a provider.
///
/// This is converted into a `RetryPolicy` used by `codex-client` to drive
/// transport-level retries for both unary and streaming calls.
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u64,
pub base_delay: Duration,
pub retry_429: bool,
pub retry_5xx: bool,
pub retry_transport: bool,
}
impl RetryConfig {
pub fn to_policy(&self) -> RetryPolicy {
RetryPolicy {
max_attempts: self.max_attempts,
base_delay: self.base_delay,
retry_on: RetryOn {
retry_429: self.retry_429,
retry_5xx: self.retry_5xx,
retry_transport: self.retry_transport,
},
}
}
}
/// HTTP endpoint configuration used to talk to a concrete API deployment.
///
/// Encapsulates base URL, default headers, query params, retry policy, and
/// stream idle timeout, plus helper methods for building requests.
#[derive(Debug, Clone)]
pub struct Provider {
pub name: String,
pub base_url: String,
pub query_params: Option<HashMap<String, String>>,
pub headers: HeaderMap,
pub retry: RetryConfig,
pub stream_idle_timeout: Duration,
}
impl Provider {
pub fn url_for_path(&self, path: &str) -> String {
let base = self.base_url.trim_end_matches('/');
let path = path.trim_start_matches('/');
let mut url = if path.is_empty() {
base.to_string()
} else {
format!("{base}/{path}")
};
if let Some(params) = &self.query_params
&& !params.is_empty()
{
let qs = params
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
url.push('?');
url.push_str(&qs);
}
url
}
pub fn build_request(&self, method: Method, path: &str) -> Request {
Request {
method,
url: self.url_for_path(path),
headers: self.headers.clone(),
body: None,
compression: RequestCompression::None,
timeout: None,
response_body_limit_bytes: None,
}
}
pub fn websocket_url_for_path(&self, path: &str) -> Result<Url, url::ParseError> {
let mut url = Url::parse(&self.url_for_path(path))?;
let scheme = match url.scheme() {
"http" => "ws",
"https" => "wss",
"ws" | "wss" => return Ok(url),
_ => return Ok(url),
};
let _ = url.set_scheme(scheme);
Ok(url)
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
toml = { workspace = true, features = ["preserve_order"] }
toml_edit = { workspace = true }
tonic = { workspace = true }
tonic = { workspace = true, features = ["tls-native-roots", "tls-ring"] }
tonic-prost = { workspace = true }
tracing = { workspace = true }
wildmatch = { workspace = true }
+17 -1
View File
@@ -1,10 +1,26 @@
//! Metrics derived from loaded configuration at session start.
use super::Config;
use codex_features::FEATURES;
use codex_features::Stage;
use codex_otel::SessionTelemetry;
pub(crate) fn emit_session_start_metrics(config: &Config, telemetry: &SessionTelemetry) {
config.features.emit_metrics(telemetry);
for feature in FEATURES {
if matches!(feature.stage, Stage::Removed) {
continue;
}
if config.features.enabled(feature.id) != feature.default_enabled {
telemetry.counter(
"codex.feature.state",
/*inc*/ 1,
&[
("feature", feature.key),
("value", &config.features.enabled(feature.id).to_string()),
],
);
}
}
#[cfg(windows)]
crate::windows_system_config::emit_namespace_squatting_probe();
}
-1
View File
@@ -14,7 +14,6 @@ workspace = true
[dependencies]
codex-network-proxy = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
-19
View File
@@ -3,7 +3,6 @@
//! This crate defines the feature registry plus the logic used to resolve an
//! effective feature set from config-like inputs.
use codex_otel::SessionTelemetry;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::WarningEvent;
@@ -562,24 +561,6 @@ impl Features {
self.legacy_usages.iter()
}
pub fn emit_metrics(&self, otel: &SessionTelemetry) {
for feature in FEATURES {
if matches!(feature.stage, Stage::Removed) {
continue;
}
if self.enabled(feature.id) != feature.default_enabled {
otel.counter(
"codex.feature.state",
/*inc*/ 1,
&[
("feature", feature.key),
("value", &self.enabled(feature.id).to_string()),
],
);
}
}
}
/// Apply a table of key -> bool toggles (e.g. from TOML).
pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) {
for (k, v) in m {
+1 -1
View File
@@ -13,7 +13,7 @@ http-body = "1.0.1"
httpdate = { workspace = true }
native-tls = "0.2"
opentelemetry = { workspace = true }
reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots", "stream"] }
reqwest = { workspace = true, features = ["json", "rustls-tls", "rustls-tls-native-roots", "stream"] }
rustls = { workspace = true }
rustls-native-certs = { workspace = true }
rustls-pki-types = { workspace = true }
+1 -1
View File
@@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true
[dependencies]
codex-api = { workspace = true }
codex-client = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-redacted-string = { workspace = true }
http = { workspace = true }
+2 -2
View File
@@ -8,8 +8,8 @@
//! API provider construction applies the process-wide managed residency policy also
//! used by default HTTP headers.
use codex_api::Provider as ApiProvider;
use codex_api::RetryConfig as ApiRetryConfig;
use codex_client::Provider as ApiProvider;
use codex_client::RetryConfig as ApiRetryConfig;
use codex_protocol::auth::AuthMode;
use codex_protocol::config_types::ModelProviderAuthInfo;
use codex_protocol::error::CodexErr;
+6
View File
@@ -7,9 +7,15 @@ license.workspace = true
[lints]
workspace = true
[package.metadata.cargo-shear]
# "image" does not forward png's zlib-rs feature. This direct dependency keeps
# encoded PNG bytes stable when WebSocket dependencies are absent from a build.
ignored = ["png"]
[dependencies]
base64 = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] }
png = { version = "0.18.0", features = ["zlib-rs"] }
codex-utils-cache = { workspace = true }
mime_guess = { workspace = true }
thiserror = { workspace = true }