diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 76188da5b834..ab65fc0a4dd2 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1697,6 +1697,7 @@ dependencies = [ "codex-login", "codex-mcp", "codex-mcp-server", + "codex-model-provider", "codex-protocol", "codex-responses-api-proxy", "codex-rmcp-client", @@ -1933,6 +1934,7 @@ dependencies = [ "codex-instructions", "codex-login", "codex-mcp", + "codex-model-provider", "codex-model-provider-info", "codex-models-manager", "codex-network-proxy", @@ -2369,7 +2371,6 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", - "codex-api", "codex-app-server-protocol", "codex-client", "codex-config", @@ -2465,6 +2466,19 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-model-provider" +version = "0.0.0" +dependencies = [ + "async-trait", + "codex-api", + "codex-login", + "codex-model-provider-info", + "codex-protocol", + "http 1.4.0", + "pretty_assertions", +] + [[package]] name = "codex-model-provider-info" version = "0.0.0" @@ -2494,6 +2508,7 @@ dependencies = [ "codex-config", "codex-feedback", "codex-login", + "codex-model-provider", "codex-model-provider-info", "codex-otel", "codex-protocol", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index c8f6b9e5a7a4..ed6bdc0fd6f7 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -92,6 +92,7 @@ members = [ "thread-store", "codex-experimental-api-macros", "plugin", + "model-provider", ] resolver = "2" @@ -154,6 +155,7 @@ codex-network-proxy = { path = "network-proxy" } codex-ollama = { path = "ollama" } codex-otel = { path = "otel" } codex-plugin = { path = "plugin" } +codex-model-provider = { path = "model-provider" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-realtime-webrtc = { path = "realtime-webrtc" } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 11d3eb3bbd27..ba1c321bc250 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -37,6 +37,7 @@ codex-features = { workspace = true } codex-login = { workspace = true } codex-mcp = { workspace = true } codex-mcp-server = { workspace = true } +codex-model-provider = { workspace = true } codex-protocol = { workspace = true } codex-responses-api-proxy = { workspace = true } codex-rmcp-client = { workspace = true } diff --git a/codex-rs/cli/src/responses_cmd.rs b/codex-rs/cli/src/responses_cmd.rs index 2dc7a671462c..9cd27638d3ea 100644 --- a/codex-rs/cli/src/responses_cmd.rs +++ b/codex-rs/cli/src/responses_cmd.rs @@ -1,5 +1,6 @@ use clap::Parser; use codex_core::config::Config; +use codex_model_provider::create_model_provider; use codex_utils_cli::CliConfigOverrides; use serde_json::json; use tokio::io::AsyncReadExt; @@ -29,16 +30,9 @@ pub(crate) async fn run_responses_command( let base_auth_manager = codex_login::AuthManager::shared_from_config( &config, /*enable_codex_api_key_env*/ true, ); - let auth_manager = - codex_login::auth_manager_for_provider(Some(base_auth_manager), &config.model_provider); - let auth = match auth_manager { - Some(auth_manager) => auth_manager.auth().await, - None => None, - }; - let api_provider = config - .model_provider - .to_api_provider(auth.as_ref().map(codex_login::CodexAuth::auth_mode))?; - let api_auth = codex_login::auth_provider_from_auth(auth, &config.model_provider)?; + let model_provider = create_model_provider(config.model_provider, Some(base_auth_manager)); + let api_provider = model_provider.api_provider().await?; + let api_auth = model_provider.api_auth().await?; let client = codex_api::ResponsesClient::new( codex_api::ReqwestTransport::new(codex_login::default_client::build_reqwest_client()), api_provider, diff --git a/codex-rs/codex-api/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs index 2677ff3e5579..13c263cbdb8c 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -1,4 +1,3 @@ -use crate::AuthProvider as ApiAuthProvider; use crate::TransportError; use crate::error::ApiError; use crate::rate_limits::parse_promo_message; @@ -12,7 +11,6 @@ use codex_protocol::error::RetryLimitReachedError; use codex_protocol::error::UnexpectedResponseError; use codex_protocol::error::UsageLimitReachedError; use http::HeaderMap; -use http::HeaderValue; use serde::Deserialize; use serde_json::Value; @@ -174,48 +172,3 @@ struct UsageErrorBody { plan_type: Option, resets_at: Option, } - -#[derive(Clone, Default)] -pub struct CoreAuthProvider { - pub token: Option, - pub account_id: Option, - pub is_fedramp_account: bool, -} - -impl CoreAuthProvider { - pub fn auth_header_attached(&self) -> bool { - self.token - .as_ref() - .is_some_and(|token| http::HeaderValue::from_str(&format!("Bearer {token}")).is_ok()) - } - - pub fn auth_header_name(&self) -> Option<&'static str> { - self.auth_header_attached().then_some("authorization") - } - - pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self { - Self { - token: token.map(str::to_string), - account_id: account_id.map(str::to_string), - is_fedramp_account: false, - } - } -} - -impl ApiAuthProvider for CoreAuthProvider { - fn add_auth_headers(&self, headers: &mut HeaderMap) { - if let Some(token) = self.token.as_ref() - && let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) - { - let _ = headers.insert(http::header::AUTHORIZATION, header); - } - if let Some(account_id) = self.account_id.as_ref() - && let Ok(header) = HeaderValue::from_str(account_id) - { - let _ = headers.insert("ChatGPT-Account-ID", header); - } - if self.is_fedramp_account { - crate::auth::add_fedramp_routing_header(headers); - } - } -} diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index c7d4bbbdab70..077691c95c9c 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -130,55 +130,3 @@ fn map_api_error_extracts_identity_auth_details_from_headers() { ); assert_eq!(err.identity_error_code.as_deref(), Some("token_expired")); } - -#[test] -fn core_auth_provider_reports_when_auth_header_will_attach() { - let auth = CoreAuthProvider { - token: Some("access-token".to_string()), - account_id: None, - is_fedramp_account: false, - }; - - assert!(auth.auth_header_attached()); - assert_eq!(auth.auth_header_name(), Some("authorization")); -} - -#[test] -fn core_auth_provider_adds_auth_headers() { - let auth = CoreAuthProvider::for_test(Some("access-token"), Some("workspace-123")); - let mut headers = HeaderMap::new(); - - crate::AuthProvider::add_auth_headers(&auth, &mut headers); - - assert_eq!( - headers - .get(http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()), - Some("Bearer access-token") - ); - assert_eq!( - headers - .get("ChatGPT-Account-ID") - .and_then(|value| value.to_str().ok()), - Some("workspace-123") - ); -} - -#[test] -fn core_auth_provider_adds_fedramp_routing_header_for_fedramp_accounts() { - let auth = CoreAuthProvider { - token: Some("access-token".to_string()), - account_id: Some("workspace-123".to_string()), - is_fedramp_account: true, - }; - let mut headers = HeaderMap::new(); - - crate::AuthProvider::add_auth_headers(&auth, &mut headers); - - assert_eq!( - headers - .get("X-OpenAI-Fedramp") - .and_then(|value| value.to_str().ok()), - Some("true") - ); -} diff --git a/codex-rs/codex-api/src/auth.rs b/codex-rs/codex-api/src/auth.rs index efa5fb32889e..d84028a2cec0 100644 --- a/codex-rs/codex-api/src/auth.rs +++ b/codex-rs/codex-api/src/auth.rs @@ -1,5 +1,5 @@ use http::HeaderMap; -use http::HeaderValue; +use std::sync::Arc; /// Adds authentication headers to API requests. /// @@ -10,25 +10,23 @@ pub trait AuthProvider: Send + Sync { fn add_auth_headers(&self, headers: &mut HeaderMap); } -pub(crate) fn add_fedramp_routing_header(headers: &mut HeaderMap) { - headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true")); -} - -#[cfg(test)] -mod tests { - use super::*; +/// Shared auth handle passed through API clients. +pub type SharedAuthProvider = Arc; - #[test] - fn add_fedramp_routing_header_sets_header() { - let mut headers = HeaderMap::new(); - - add_fedramp_routing_header(&mut headers); +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct AuthHeaderTelemetry { + pub attached: bool, + pub name: Option<&'static str>, +} - assert_eq!( - headers - .get("X-OpenAI-Fedramp") - .and_then(|v| v.to_str().ok()), - Some("true") - ); +pub fn auth_header_telemetry(auth: &dyn AuthProvider) -> AuthHeaderTelemetry { + let mut headers = HeaderMap::new(); + auth.add_auth_headers(&mut headers); + let name = headers + .contains_key(http::header::AUTHORIZATION) + .then_some("authorization"); + AuthHeaderTelemetry { + attached: name.is_some(), + name, } } diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs index 748ac3555887..336e958d4e4f 100644 --- a/codex-rs/codex-api/src/endpoint/compact.rs +++ b/codex-rs/codex-api/src/endpoint/compact.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::common::CompactionInput; use crate::endpoint::session::EndpointSession; use crate::error::ApiError; @@ -12,12 +12,12 @@ use serde::Deserialize; use serde_json::to_value; use std::sync::Arc; -pub struct CompactClient { - session: EndpointSession, +pub struct CompactClient { + session: EndpointSession, } -impl CompactClient { - pub fn new(transport: T, provider: Provider, auth: A) -> Self { +impl CompactClient { + pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { session: EndpointSession::new(transport, provider, auth), } @@ -86,18 +86,8 @@ mod tests { } } - #[derive(Clone, Default)] - struct DummyAuth; - - impl AuthProvider for DummyAuth { - fn add_auth_headers(&self, _headers: &mut HeaderMap) {} - } - #[test] fn path_is_responses_compact() { - assert_eq!( - CompactClient::::path(), - "responses/compact" - ); + assert_eq!(CompactClient::::path(), "responses/compact"); } } diff --git a/codex-rs/codex-api/src/endpoint/memories.rs b/codex-rs/codex-api/src/endpoint/memories.rs index 3047c859db0c..a6c25641f25d 100644 --- a/codex-rs/codex-api/src/endpoint/memories.rs +++ b/codex-rs/codex-api/src/endpoint/memories.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::common::MemorySummarizeInput; use crate::common::MemorySummarizeOutput; use crate::endpoint::session::EndpointSession; @@ -12,12 +12,12 @@ use serde::Deserialize; use serde_json::to_value; use std::sync::Arc; -pub struct MemoriesClient { - session: EndpointSession, +pub struct MemoriesClient { + session: EndpointSession, } -impl MemoriesClient { - pub fn new(transport: T, provider: Provider, auth: A) -> Self { +impl MemoriesClient { + pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { session: EndpointSession::new(transport, provider, auth), } @@ -67,6 +67,7 @@ struct SummarizeResponse { #[cfg(test)] mod tests { use super::*; + use crate::auth::AuthProvider; use crate::common::RawMemory; use crate::common::RawMemoryMetadata; use crate::provider::RetryConfig; @@ -157,7 +158,7 @@ mod tests { #[test] fn path_is_memories_trace_summarize_for_wire_compatibility() { assert_eq!( - MemoriesClient::::path(), + MemoriesClient::::path(), "memories/trace_summarize" ); } @@ -178,7 +179,7 @@ mod tests { let client = MemoriesClient::new( transport.clone(), provider("https://example.com/api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let input = MemorySummarizeInput { diff --git a/codex-rs/codex-api/src/endpoint/models.rs b/codex-rs/codex-api/src/endpoint/models.rs index 17342d6f99de..ec9ee7aac6d3 100644 --- a/codex-rs/codex-api/src/endpoint/models.rs +++ b/codex-rs/codex-api/src/endpoint/models.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::endpoint::session::EndpointSession; use crate::error::ApiError; use crate::provider::Provider; @@ -11,12 +11,12 @@ use http::Method; use http::header::ETAG; use std::sync::Arc; -pub struct ModelsClient { - session: EndpointSession, +pub struct ModelsClient { + session: EndpointSession, } -impl ModelsClient { - pub fn new(transport: T, provider: Provider, auth: A) -> Self { +impl ModelsClient { + pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { session: EndpointSession::new(transport, provider, auth), } @@ -76,6 +76,7 @@ impl ModelsClient { #[cfg(test)] mod tests { use super::*; + use crate::auth::AuthProvider; use crate::provider::RetryConfig; use async_trait::async_trait; use codex_client::Request; @@ -165,7 +166,7 @@ mod tests { let client = ModelsClient::new( transport.clone(), provider("https://example.com/api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let (models, _) = client @@ -229,7 +230,7 @@ mod tests { let client = ModelsClient::new( transport, provider("https://example.com/api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let (models, _) = client @@ -256,7 +257,7 @@ mod tests { let client = ModelsClient::new( transport, provider("https://example.com/api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let (models, etag) = client diff --git a/codex-rs/codex-api/src/endpoint/realtime_call.rs b/codex-rs/codex-api/src/endpoint/realtime_call.rs index a9a8b963cfb3..b0342c53498d 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_call.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_call.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::endpoint::realtime_websocket::RealtimeSessionConfig; use crate::endpoint::realtime_websocket::session_update_session_json; use crate::endpoint::session::EndpointSession; @@ -24,8 +24,8 @@ use tracing::trace; const MULTIPART_BOUNDARY: &str = "codex-realtime-call-boundary"; const MULTIPART_CONTENT_TYPE: &str = "multipart/form-data; boundary=codex-realtime-call-boundary"; -pub struct RealtimeCallClient { - session: EndpointSession, +pub struct RealtimeCallClient { + session: EndpointSession, } /// Answer from creating a WebRTC Realtime call. @@ -44,8 +44,8 @@ struct BackendRealtimeCallRequest<'a> { session: &'a Value, } -impl RealtimeCallClient { - pub fn new(transport: T, provider: Provider, auth: A) -> Self { +impl RealtimeCallClient { + pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { session: EndpointSession::new(transport, provider, auth), } @@ -221,6 +221,7 @@ fn decode_call_id_from_location(headers: &HeaderMap) -> Result #[cfg(test)] mod tests { use super::*; + use crate::auth::AuthProvider; use crate::endpoint::realtime_websocket::RealtimeEventParser; use crate::endpoint::realtime_websocket::RealtimeOutputModality; use crate::endpoint::realtime_websocket::RealtimeSessionMode; @@ -327,7 +328,7 @@ mod tests { let client = RealtimeCallClient::new( transport.clone(), provider("https://api.openai.com/v1"), - DummyAuth, + Arc::new(DummyAuth), ); let response = client @@ -370,7 +371,7 @@ mod tests { let client = RealtimeCallClient::new( transport.clone(), provider("https://chatgpt.com/backend-api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let response = client @@ -404,7 +405,7 @@ mod tests { let client = RealtimeCallClient::new( transport.clone(), provider("https://api.openai.com/v1"), - DummyAuth, + Arc::new(DummyAuth), ); let response = client @@ -466,7 +467,7 @@ mod tests { let client = RealtimeCallClient::new( transport.clone(), provider("https://chatgpt.com/backend-api/codex"), - DummyAuth, + Arc::new(DummyAuth), ); let response = client @@ -512,8 +513,11 @@ mod tests { #[tokio::test] async fn errors_when_location_is_missing() { let transport = CapturingTransport::without_location(); - let client = - RealtimeCallClient::new(transport, provider("https://api.openai.com/v1"), DummyAuth); + let client = RealtimeCallClient::new( + transport, + provider("https://api.openai.com/v1"), + Arc::new(DummyAuth), + ); let err = client .create("v=offer\r\n".to_string()) diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index 8e0e7384fda0..17b478d1fd77 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::common::ResponseStream; use crate::common::ResponsesApiRequest; use crate::endpoint::session::EndpointSession; @@ -23,8 +23,8 @@ use std::sync::Arc; use std::sync::OnceLock; use tracing::instrument; -pub struct ResponsesClient { - session: EndpointSession, +pub struct ResponsesClient { + session: EndpointSession, sse_telemetry: Option>, } @@ -37,8 +37,8 @@ pub struct ResponsesOptions { pub turn_state: Option>>, } -impl ResponsesClient { - pub fn new(transport: T, provider: Provider, auth: A) -> Self { +impl ResponsesClient { + pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { session: EndpointSession::new(transport, provider, auth), sse_telemetry: None, diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index d2b775cdd5a4..bc72aee4119d 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::common::ResponseEvent; use crate::common::ResponseStream; use crate::common::ResponsesWsRequest; @@ -279,13 +279,13 @@ impl ResponsesWebsocketConnection { } } -pub struct ResponsesWebsocketClient { +pub struct ResponsesWebsocketClient { provider: Provider, - auth: A, + auth: SharedAuthProvider, } -impl ResponsesWebsocketClient { - pub fn new(provider: Provider, auth: A) -> Self { +impl ResponsesWebsocketClient { + pub fn new(provider: Provider, auth: SharedAuthProvider) -> Self { Self { provider, auth } } diff --git a/codex-rs/codex-api/src/endpoint/session.rs b/codex-rs/codex-api/src/endpoint/session.rs index e4a470ceeead..5c9ec315935d 100644 --- a/codex-rs/codex-api/src/endpoint/session.rs +++ b/codex-rs/codex-api/src/endpoint/session.rs @@ -1,4 +1,4 @@ -use crate::auth::AuthProvider; +use crate::auth::SharedAuthProvider; use crate::error::ApiError; use crate::provider::Provider; use crate::telemetry::run_with_request_telemetry; @@ -14,15 +14,15 @@ use serde_json::Value; use std::sync::Arc; use tracing::instrument; -pub(crate) struct EndpointSession { +pub(crate) struct EndpointSession { transport: T, provider: Provider, - auth: A, + auth: SharedAuthProvider, request_telemetry: Option>, } -impl EndpointSession { - pub(crate) fn new(transport: T, provider: Provider, auth: A) -> Self { +impl EndpointSession { + pub(crate) fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self { Self { transport, provider, diff --git a/codex-rs/codex-api/src/files.rs b/codex-rs/codex-api/src/files.rs index ebe35af5ba94..d1e2840066d8 100644 --- a/codex-rs/codex-api/src/files.rs +++ b/codex-rs/codex-api/src/files.rs @@ -96,7 +96,7 @@ pub fn openai_file_uri(file_id: &str) -> String { pub async fn upload_local_file( base_url: &str, - auth: &impl AuthProvider, + auth: &dyn AuthProvider, path: &Path, ) -> Result { let metadata = tokio::fs::metadata(path) @@ -252,7 +252,7 @@ pub async fn upload_local_file( } fn authorized_request( - auth: &impl AuthProvider, + auth: &dyn AuthProvider, method: reqwest::Method, url: &str, ) -> reqwest::RequestBuilder { @@ -276,8 +276,8 @@ fn build_reqwest_client() -> reqwest::Client { #[cfg(test)] mod tests { use super::*; - use crate::CoreAuthProvider; use pretty_assertions::assert_eq; + use reqwest::header::HeaderValue; use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; @@ -291,8 +291,21 @@ mod tests { use wiremock::matchers::method; use wiremock::matchers::path; - fn chatgpt_auth() -> CoreAuthProvider { - CoreAuthProvider::for_test(Some("token"), Some("account_id")) + #[derive(Clone, Copy)] + struct ChatGptTestAuth; + + impl AuthProvider for ChatGptTestAuth { + fn add_auth_headers(&self, headers: &mut reqwest::header::HeaderMap) { + headers.insert( + reqwest::header::AUTHORIZATION, + HeaderValue::from_static("Bearer token"), + ); + headers.insert("ChatGPT-Account-ID", HeaderValue::from_static("account_id")); + } + } + + fn chatgpt_auth() -> ChatGptTestAuth { + ChatGptTestAuth } fn base_url_for(server: &MockServer) -> String { diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index 82a10048498a..d92bb5b35510 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -15,9 +15,11 @@ pub use codex_client::RequestTelemetry; pub use codex_client::ReqwestTransport; pub use codex_client::TransportError; -pub use crate::api_bridge::CoreAuthProvider; pub use crate::api_bridge::map_api_error; +pub use crate::auth::AuthHeaderTelemetry; pub use crate::auth::AuthProvider; +pub use crate::auth::SharedAuthProvider; +pub use crate::auth::auth_header_telemetry; pub use crate::common::CompactionInput; pub use crate::common::MemorySummarizeInput; pub use crate::common::MemorySummarizeOutput; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index d82fcc14cead..3184544aa7b2 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -200,7 +200,7 @@ data: {"id":"resp-1","output":[{"type":"message","role":"assistant","content":[{ async fn responses_client_uses_responses_path() -> Result<()> { let state = RecordingState::default(); let transport = RecordingTransport::new(state.clone()); - let client = ResponsesClient::new(transport, provider("openai"), NoAuth); + let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); let body = serde_json::json!({ "echo": true }); let _stream = client @@ -221,7 +221,7 @@ async fn responses_client_uses_responses_path() -> Result<()> { async fn streaming_client_adds_auth_headers() -> Result<()> { let state = RecordingState::default(); let transport = RecordingTransport::new(state.clone()); - let auth = StaticAuth::new("secret-token", "acct-1"); + let auth = Arc::new(StaticAuth::new("secret-token", "acct-1")); let client = ResponsesClient::new(transport, provider("openai"), auth); let body = serde_json::json!({ "model": "gpt-test" }); @@ -281,7 +281,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { text: None, client_metadata: None, }; - let client = ResponsesClient::new(transport.clone(), provider, NoAuth); + let client = ResponsesClient::new(transport.clone(), provider, Arc::new(NoAuth)); let _stream = client .stream_request( @@ -300,7 +300,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { let state = RecordingState::default(); let transport = RecordingTransport::new(state.clone()); - let client = ResponsesClient::new(transport, provider("azure"), NoAuth); + let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth)); let request = ResponsesApiRequest { model: "gpt-test".into(), diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index fab135e0ab30..96de0b2cec85 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -14,6 +14,7 @@ use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::default_input_modalities; use http::HeaderMap; use http::Method; +use std::sync::Arc; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; @@ -108,7 +109,7 @@ async fn models_client_hits_models_endpoint() { .await; let transport = ReqwestTransport::new(reqwest::Client::new()); - let client = ModelsClient::new(transport, provider(&base_url), DummyAuth); + let client = ModelsClient::new(transport, provider(&base_url), Arc::new(DummyAuth)); let (models, _) = client .list_models("0.1.0", HeaderMap::new()) diff --git a/codex-rs/codex-api/tests/sse_end_to_end.rs b/codex-rs/codex-api/tests/sse_end_to_end.rs index 4d32e8224260..107c10172446 100644 --- a/codex-rs/codex-api/tests/sse_end_to_end.rs +++ b/codex-rs/codex-api/tests/sse_end_to_end.rs @@ -1,3 +1,4 @@ +use std::sync::Arc; use std::time::Duration; use anyhow::Result; @@ -116,7 +117,7 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()> let body = build_responses_body(vec![item1, item2, completed]); let transport = FixtureSseTransport::new(body); - let client = ResponsesClient::new(transport, provider("openai"), NoAuth); + let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); let mut stream = client .stream( diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a0d42598105b..8770aaf48717 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -52,6 +52,7 @@ codex-instructions = { workspace = true } codex-network-proxy = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } +codex-model-provider = { workspace = true } codex-protocol = { workspace = true } codex-response-debug-context = { workspace = true } codex-rollout = { workspace = true } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index bd83c81f0de4..c5b7c46122c8 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -39,6 +39,7 @@ use codex_api::Compression; use codex_api::MemoriesClient as ApiMemoriesClient; use codex_api::MemorySummarizeInput as ApiMemorySummarizeInput; use codex_api::MemorySummarizeOutput as ApiMemorySummarizeOutput; +use codex_api::Provider as ApiProvider; use codex_api::RawMemory as ApiRawMemory; use codex_api::RealtimeCallClient as ApiRealtimeCallClient; use codex_api::RealtimeSessionConfig as ApiRealtimeSessionConfig; @@ -52,9 +53,11 @@ use codex_api::ResponsesOptions as ApiResponsesOptions; use codex_api::ResponsesWebsocketClient as ApiWebSocketResponsesClient; use codex_api::ResponsesWebsocketConnection as ApiWebSocketConnection; use codex_api::ResponsesWsRequest; +use codex_api::SharedAuthProvider; use codex_api::SseTelemetry; use codex_api::TransportError; use codex_api::WebsocketTelemetry; +use codex_api::auth_header_telemetry; use codex_api::build_conversation_headers; use codex_api::create_text_param_for_request; use codex_api::response_create_client_metadata; @@ -101,14 +104,13 @@ use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::util::emit_feedback_auth_recovery_tags; -use codex_api::CoreAuthProvider; use codex_api::map_api_error; use codex_feedback::FeedbackRequestTags; use codex_feedback::emit_feedback_request_tags_with_auth_env; -use codex_login::api_bridge::auth_provider_from_auth; use codex_login::auth_env_telemetry::AuthEnvTelemetry; use codex_login::auth_env_telemetry::collect_auth_env_telemetry; -use codex_login::provider_auth::auth_manager_for_provider; +use codex_model_provider::SharedModelProvider; +use codex_model_provider::create_model_provider; #[cfg(test)] use codex_model_provider_info::DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS; use codex_model_provider_info::ModelProviderInfo; @@ -143,11 +145,10 @@ pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = /// configuration is per turn and is passed explicitly to streaming/unary methods. #[derive(Debug)] struct ModelClientState { - auth_manager: Option>, conversation_id: ThreadId, window_generation: AtomicU64, installation_id: String, - provider: ModelProviderInfo, + provider: SharedModelProvider, auth_env_telemetry: AuthEnvTelemetry, session_source: SessionSource, model_verbosity: Option, @@ -164,8 +165,8 @@ struct ModelClientState { /// share the same auth/provider setup flow. struct CurrentClientSetup { auth: Option, - api_provider: codex_api::Provider, - api_auth: CoreAuthProvider, + api_provider: ApiProvider, + api_auth: SharedAuthProvider, } #[derive(Clone, Copy)] @@ -275,7 +276,7 @@ pub(crate) struct RealtimeWebrtcCallStart { /// API-key sessions send that API bearer. ChatGPT-auth sessions send their bearer plus account id; /// transceiver is responsible for accepting that same call-create identity on the direct /// `api.openai.com` sideband path. -fn sideband_websocket_auth_headers(api_auth: &CoreAuthProvider) -> ApiHeaderMap { +fn sideband_websocket_auth_headers(api_auth: &dyn AuthProvider) -> ApiHeaderMap { let mut headers = ApiHeaderMap::new(); api_auth.add_auth_headers(&mut headers); headers @@ -291,25 +292,26 @@ impl ModelClient { auth_manager: Option>, conversation_id: ThreadId, installation_id: String, - provider: ModelProviderInfo, + provider_info: ModelProviderInfo, session_source: SessionSource, model_verbosity: Option, enable_request_compression: bool, include_timing_metrics: bool, beta_features_header: Option, ) -> Self { - let auth_manager = auth_manager_for_provider(auth_manager, &provider); - let codex_api_key_env_enabled = auth_manager + let model_provider = create_model_provider(provider_info, auth_manager); + let codex_api_key_env_enabled = model_provider + .auth_manager() .as_ref() .is_some_and(|manager| manager.codex_api_key_env_enabled()); - let auth_env_telemetry = collect_auth_env_telemetry(&provider, codex_api_key_env_enabled); + let auth_env_telemetry = + collect_auth_env_telemetry(model_provider.info(), codex_api_key_env_enabled); Self { state: Arc::new(ModelClientState { - auth_manager, conversation_id, window_generation: AtomicU64::new(0), installation_id, - provider, + provider: model_provider, auth_env_telemetry, session_source, model_verbosity, @@ -335,7 +337,7 @@ impl ModelClient { } pub(crate) fn auth_manager(&self) -> Option> { - self.state.auth_manager.clone() + self.state.provider.auth_manager() } pub(crate) fn set_window_generation(&self, window_generation: u64) { @@ -418,7 +420,7 @@ impl ModelClient { session_telemetry, AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), - &client_setup.api_auth, + client_setup.api_auth.as_ref(), PendingUnauthorizedRetry::default(), ), RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT), @@ -478,7 +480,9 @@ impl ModelClient { // the server-side control WebSocket to the call id from that HTTP response. let client_setup = self.current_client_setup().await?; let mut sideband_headers = extra_headers.clone(); - sideband_headers.extend(sideband_websocket_auth_headers(&client_setup.api_auth)); + sideband_headers.extend(sideband_websocket_auth_headers( + client_setup.api_auth.as_ref(), + )); let transport = ReqwestTransport::new(build_reqwest_client()); let response = ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth) @@ -515,7 +519,7 @@ impl ModelClient { session_telemetry, AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), - &client_setup.api_auth, + client_setup.api_auth.as_ref(), PendingUnauthorizedRetry::default(), ), RequestRouteTelemetry::for_endpoint(MEMORIES_SUMMARIZE_ENDPOINT), @@ -636,7 +640,7 @@ impl ModelClient { /// /// WebSocket use is controlled by provider capability and session-scoped fallback state. pub fn responses_websocket_enabled(&self) -> bool { - if !self.state.provider.supports_websockets + if !self.state.provider.info().supports_websockets || self.state.disable_websockets.load(Ordering::Relaxed) || (*CODEX_RS_SSE_FIXTURE).is_some() { @@ -651,15 +655,9 @@ impl ModelClient { /// This centralizes setup used by both prewarm and normal request paths so they stay in /// lockstep when auth/provider resolution changes. async fn current_client_setup(&self) -> Result { - let auth = match self.state.auth_manager.as_ref() { - Some(manager) => manager.auth().await, - None => None, - }; - let api_provider = self - .state - .provider - .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?; - let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; + let auth = self.state.provider.auth().await; + let api_provider = self.state.provider.api_provider().await?; + let api_auth = self.state.provider.api_auth().await?; Ok(CurrentClientSetup { auth, api_provider, @@ -676,7 +674,7 @@ impl ModelClient { &self, session_telemetry: &SessionTelemetry, api_provider: codex_api::Provider, - api_auth: CoreAuthProvider, + api_auth: SharedAuthProvider, turn_state: Option>>, turn_metadata_header: Option<&str>, auth_context: AuthRequestTelemetryContext, @@ -689,7 +687,7 @@ impl ModelClient { request_route_telemetry, self.state.auth_env_telemetry.clone(), ); - let websocket_connect_timeout = self.state.provider.websocket_connect_timeout(); + let websocket_connect_timeout = self.state.provider.info().websocket_connect_timeout(); let start = Instant::now(); let result = match tokio::time::timeout( websocket_connect_timeout, @@ -1007,7 +1005,7 @@ impl ModelClientSession { })?; let auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), - &client_setup.api_auth, + client_setup.api_auth.as_ref(), PendingUnauthorizedRetry::default(), ); let connection = self @@ -1033,8 +1031,8 @@ impl ModelClientSession { level = "info", skip_all, fields( - provider = %self.client.state.provider.name, - wire_api = %self.client.state.provider.wire_api, + provider = %self.client.state.provider.info().name, + wire_api = %self.client.state.provider.info().wire_api, transport = "responses_websocket", api.path = "responses", turn.has_metadata_header = params.turn_metadata_header.is_some() @@ -1105,7 +1103,7 @@ impl ModelClientSession { fn responses_request_compression(&self, auth: Option<&CodexAuth>) -> Compression { if self.client.state.enable_request_compression && auth.is_some_and(CodexAuth::is_chatgpt_auth) - && self.client.state.provider.is_openai() + && self.client.state.provider.info().is_openai() { Compression::Zstd } else { @@ -1124,7 +1122,7 @@ impl ModelClientSession { skip_all, fields( model = %model_info.slug, - wire_api = %self.client.state.provider.wire_api, + wire_api = %self.client.state.provider.info().wire_api, transport = "responses_http", http.method = "POST", api.path = "responses", @@ -1145,14 +1143,14 @@ impl ModelClientSession { warn!(path, "Streaming from fixture"); let stream = codex_api::stream_from_fixture( path, - self.client.state.provider.stream_idle_timeout(), + self.client.state.provider.info().stream_idle_timeout(), ) .map_err(map_api_error)?; let (stream, _last_request_rx) = map_response_stream(stream, session_telemetry.clone()); return Ok(stream); } - let auth_manager = self.client.state.auth_manager.clone(); + let auth_manager = self.client.state.provider.auth_manager(); let mut auth_recovery = auth_manager .as_ref() .map(AuthManager::unauthorized_recovery); @@ -1162,7 +1160,7 @@ impl ModelClientSession { let transport = ReqwestTransport::new(build_reqwest_client()); let request_auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), - &client_setup.api_auth, + client_setup.api_auth.as_ref(), pending_retry, ); let (request_telemetry, sse_telemetry) = Self::build_streaming_telemetry( @@ -1221,7 +1219,7 @@ impl ModelClientSession { skip_all, fields( model = %model_info.slug, - wire_api = %self.client.state.provider.wire_api, + wire_api = %self.client.state.provider.info().wire_api, transport = "responses_websocket", api.path = "responses", turn.has_metadata_header = turn_metadata_header.is_some(), @@ -1240,7 +1238,7 @@ impl ModelClientSession { warmup: bool, request_trace: Option, ) -> Result { - let auth_manager = self.client.state.auth_manager.clone(); + let auth_manager = self.client.state.provider.auth_manager(); let mut auth_recovery = auth_manager .as_ref() @@ -1250,7 +1248,7 @@ impl ModelClientSession { let client_setup = self.client.current_client_setup().await?; let request_auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), - &client_setup.api_auth, + client_setup.api_auth.as_ref(), pending_retry, ); let compression = self.responses_request_compression(client_setup.auth.as_ref()); @@ -1432,7 +1430,7 @@ impl ModelClientSession { service_tier: Option, turn_metadata_header: Option<&str>, ) -> Result { - let wire_api = self.client.state.provider.wire_api; + let wire_api = self.client.state.provider.info().wire_api; match wire_api { WireApi::Responses => { if self.client.responses_websocket_enabled() { @@ -1680,16 +1678,17 @@ struct AuthRequestTelemetryContext { impl AuthRequestTelemetryContext { fn new( auth_mode: Option, - api_auth: &CoreAuthProvider, + api_auth: &dyn AuthProvider, retry: PendingUnauthorizedRetry, ) -> Self { + let auth_telemetry = auth_header_telemetry(api_auth); Self { auth_mode: auth_mode.map(|mode| match mode { AuthMode::ApiKey => "ApiKey", AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => "Chatgpt", }), - auth_header_attached: api_auth.auth_header_attached(), - auth_header_name: api_auth.auth_header_name(), + auth_header_attached: auth_telemetry.attached, + auth_header_name: auth_telemetry.name, retry_after_unauthorized: retry.retry_after_unauthorized, recovery_mode: retry.recovery_mode, recovery_phase: retry.recovery_phase, @@ -1700,7 +1699,7 @@ impl AuthRequestTelemetryContext { struct WebsocketConnectParams<'a> { session_telemetry: &'a SessionTelemetry, api_provider: codex_api::Provider, - api_auth: CoreAuthProvider, + api_auth: SharedAuthProvider, turn_metadata_header: Option<&'a str>, options: &'a ApiResponsesOptions, auth_context: AuthRequestTelemetryContext, diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 2fd5f04f9510..f4575b26a0b2 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -7,8 +7,8 @@ use super::X_CODEX_PARENT_THREAD_ID_HEADER; use super::X_CODEX_TURN_METADATA_HEADER; use super::X_CODEX_WINDOW_ID_HEADER; use super::X_OPENAI_SUBAGENT_HEADER; -use codex_api::CoreAuthProvider; use codex_app_server_protocol::AuthMode; +use codex_model_provider::BearerAuthProvider; use codex_model_provider_info::WireApi; use codex_model_provider_info::create_oss_provider_with_base_url; use codex_otel::SessionTelemetry; @@ -155,7 +155,7 @@ async fn summarize_memories_returns_empty_for_empty_input() { fn auth_request_telemetry_context_tracks_attached_auth_and_retry_phase() { let auth_context = AuthRequestTelemetryContext::new( Some(AuthMode::Chatgpt), - &CoreAuthProvider::for_test(Some("access-token"), Some("workspace-123")), + &BearerAuthProvider::for_test(Some("access-token"), Some("workspace-123")), PendingUnauthorizedRetry::from_recovery(UnauthorizedRecoveryExecution { mode: "managed", phase: "refresh_token", diff --git a/codex-rs/core/src/codex/turn.rs b/codex-rs/core/src/codex/turn.rs index 84610804a0c9..5749bda84fa9 100644 --- a/codex-rs/core/src/codex/turn.rs +++ b/codex-rs/core/src/codex/turn.rs @@ -793,7 +793,7 @@ async fn run_auto_compact( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { - if should_use_remote_compact_task(&turn_context.provider) { + if should_use_remote_compact_task(turn_context.provider.info()) { run_inline_remote_auto_compact_task( Arc::clone(sess), Arc::clone(turn_context), @@ -1074,7 +1074,7 @@ async fn run_sampling_request( } // Use the configured provider-specific stream retry budget. - let max_retries = turn_context.provider.stream_max_retries(); + let max_retries = turn_context.provider.info().stream_max_retries(); if retries >= max_retries && client_session.try_switch_fallback_transport( &turn_context.session_telemetry, diff --git a/codex-rs/core/src/codex/turn_context.rs b/codex-rs/core/src/codex/turn_context.rs index 6e5bf22c4bd4..2969d7efad44 100644 --- a/codex-rs/core/src/codex/turn_context.rs +++ b/codex-rs/core/src/codex/turn_context.rs @@ -1,4 +1,6 @@ use super::*; +use codex_model_provider::SharedModelProvider; +use codex_model_provider::create_model_provider; pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool { matches!( @@ -32,7 +34,7 @@ pub(crate) struct TurnContext { pub(crate) auth_manager: Option>, pub(crate) model_info: ModelInfo, pub(crate) session_telemetry: SessionTelemetry, - pub(crate) provider: ModelProviderInfo, + pub(crate) provider: SharedModelProvider, pub(crate) reasoning_effort: Option, pub(crate) reasoning_summary: ReasoningSummaryConfig, pub(crate) session_source: SessionSource, @@ -354,8 +356,8 @@ impl Session { let session_source = session_configuration.session_source.clone(); let image_generation_tool_auth_allowed = image_generation_tool_auth_allowed(auth_manager.as_deref()); - let auth_manager_for_context = auth_manager; - let provider_for_context = provider; + let auth_manager_for_context = auth_manager.clone(); + let provider_for_context = create_model_provider(provider, auth_manager); let session_telemetry_for_context = session_telemetry; let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index cad67dcc8fdc..9ccd6eb4e060 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -16,6 +16,7 @@ use codex_execpolicy::Decision; use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_features::Feature; +use codex_model_provider::create_model_provider; use codex_protocol::models::ContentItem; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; @@ -101,7 +102,10 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid )); session.services.models_manager = models_manager; turn_context_raw.config = Arc::clone(&config); - turn_context_raw.provider = config.model_provider.clone(); + turn_context_raw.provider = create_model_provider( + config.model_provider.clone(), + turn_context_raw.auth_manager.clone(), + ); let session = Arc::new(session); let turn_context = Arc::new(turn_context_raw); let expiration_ms: u64 = if cfg!(windows) { 2_500 } else { 1_000 }; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index eaf1d921098c..ec3798efb2b3 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -19,7 +19,6 @@ use codex_analytics::CompactionStrategy; use codex_analytics::CompactionTrigger; use codex_analytics::now_unix_seconds; use codex_features::Feature; -use codex_model_provider_info::ModelProviderInfo; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::ContextCompactionItem; @@ -38,6 +37,8 @@ use codex_utils_output_truncation::truncate_text; use futures::prelude::*; use tracing::error; +use codex_model_provider_info::ModelProviderInfo; + pub const SUMMARIZATION_PROMPT: &str = include_str!("../templates/compact/prompt.md"); pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_prefix.md"); const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000; @@ -166,7 +167,7 @@ async fn run_compact_task_inner_impl( let mut truncated_count = 0usize; - let max_retries = turn_context.provider.stream_max_retries(); + let max_retries = turn_context.provider.info().stream_max_retries(); let mut retries = 0; let mut client_session = sess.services.model_client.new_session(); // Reuse one client session so turn-scoped state (sticky routing, websocket incremental diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 2712a9403932..58ca0bd04994 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -17,6 +17,7 @@ use crate::config_loader::Sourced; use crate::test_support; use codex_config::config_toml::ConfigToml; use codex_exec_server::LOCAL_FS; +use codex_model_provider::create_model_provider; use codex_network_proxy::NetworkProxyConfig; use codex_protocol::ThreadId; use codex_protocol::approvals::NetworkApprovalProtocol; @@ -83,7 +84,7 @@ async fn guardian_test_session_and_turn_with_base_url( )); session.services.models_manager = models_manager; turn.config = Arc::clone(&config); - turn.provider = config.model_provider.clone(); + turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone()); turn.user_instructions = None; (Arc::new(session), Arc::new(turn)) @@ -889,7 +890,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() )); session.services.models_manager = models_manager; turn.config = Arc::clone(&config); - turn.provider = config.model_provider.clone(); + turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone()); let session = Arc::new(session); let turn = Arc::new(turn); seed_guardian_parent_history(&session, &turn).await; @@ -1261,7 +1262,8 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> .models_manager = models_manager; let turn_mut = Arc::get_mut(&mut turn).expect("turn should be uniquely owned"); turn_mut.config = Arc::clone(&config); - turn_mut.provider = config.model_provider.clone(); + turn_mut.provider = + create_model_provider(config.model_provider.clone(), turn_mut.auth_manager.clone()); turn_mut.user_instructions = None; seed_guardian_parent_history(&session, &turn).await; diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index 33d0a3f1f08d..720576747d6e 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -12,9 +12,9 @@ use crate::codex::Session; use crate::codex::TurnContext; -use codex_api::CoreAuthProvider; use codex_api::upload_local_file; use codex_login::CodexAuth; +use codex_model_provider::BearerAuthProvider; use serde_json::Value as JsonValue; pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( @@ -112,7 +112,7 @@ async fn build_uploaded_local_argument_value( let token_data = auth .get_token_data() .map_err(|error| format!("failed to read ChatGPT auth for file upload: {error}"))?; - let upload_auth = CoreAuthProvider { + let upload_auth = BearerAuthProvider { token: Some(token_data.access_token), account_id: token_data.account_id, is_fedramp_account: auth.is_fedramp_account(), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index a85233c77387..68d2cbcb4bb2 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -12,6 +12,7 @@ use codex_config::types::ApprovalsReviewer; use codex_config::types::AppsConfigToml; use codex_config::types::McpServerConfig; use codex_config::types::McpServerToolConfig; +use codex_model_provider::create_model_provider; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use core_test_support::PathExt; @@ -1390,7 +1391,10 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() { )); session.services.models_manager = models_manager; turn_context.config = Arc::clone(&config); - turn_context.provider = config.model_provider.clone(); + turn_context.provider = create_model_provider( + config.model_provider.clone(), + turn_context.auth_manager.clone(), + ); let session = Arc::new(session); let turn_context = Arc::new(turn_context); @@ -1466,7 +1470,10 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() { )); session.services.models_manager = models_manager; turn_context.config = Arc::clone(&config); - turn_context.provider = config.model_provider.clone(); + turn_context.provider = create_model_provider( + config.model_provider.clone(), + turn_context.auth_manager.clone(), + ); let session = Arc::new(session); let turn_context = Arc::new(turn_context); @@ -1920,7 +1927,10 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_ )); session.services.models_manager = models_manager; turn_context.config = Arc::clone(&config); - turn_context.provider = config.model_provider.clone(); + turn_context.provider = create_model_provider( + config.model_provider.clone(), + turn_context.auth_manager.clone(), + ); let session = Arc::new(session); let turn_context = Arc::new(turn_context); diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs index 8c7998d8537a..aac6d2cb802a 100644 --- a/codex-rs/core/src/tasks/compact.rs +++ b/codex-rs/core/src/tasks/compact.rs @@ -27,7 +27,7 @@ impl SessionTask for CompactTask { _cancellation_token: CancellationToken, ) -> Option { let session = session.clone_session(); - let _ = if crate::compact::should_use_remote_compact_task(&ctx.provider) { + let _ = if crate::compact::should_use_remote_compact_task(ctx.provider.info()) { session.services.session_telemetry.counter( "codex.task.compact", /*inc*/ 1, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 9c2740d48aa1..fa9d10b7eb27 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -224,7 +224,7 @@ fn build_agent_shared_config(turn: &TurnContext) -> Resultnul ModelProviderAuthInfo { command: self.command.clone(), args: self.args.clone(), - // Match the provider-auth default to avoid brittle shell-startup timing in CI. + // Match the model-provider default to avoid brittle shell-startup timing in CI. timeout_ms: non_zero_u64(/*value*/ 5_000), refresh_interval_ms: 60_000, cwd: match codex_utils_absolute_path::AbsolutePathBuf::try_from(self.tempdir.path()) { diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 9c2d021c76e5..d5303ea54cf3 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -12,7 +12,6 @@ async-trait = { workspace = true } base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-app-server-protocol = { workspace = true } -codex-api = { workspace = true } codex-client = { workspace = true } codex-config = { workspace = true } codex-keyring-store = { workspace = true } diff --git a/codex-rs/login/src/api_bridge.rs b/codex-rs/login/src/api_bridge.rs deleted file mode 100644 index 684c890bde9b..000000000000 --- a/codex-rs/login/src/api_bridge.rs +++ /dev/null @@ -1,40 +0,0 @@ -use codex_api::CoreAuthProvider; -use codex_model_provider_info::ModelProviderInfo; - -use crate::CodexAuth; - -pub fn auth_provider_from_auth( - auth: Option, - provider: &ModelProviderInfo, -) -> codex_protocol::error::Result { - if let Some(api_key) = provider.api_key()? { - return Ok(CoreAuthProvider { - token: Some(api_key), - account_id: None, - is_fedramp_account: false, - }); - } - - if let Some(token) = provider.experimental_bearer_token.clone() { - return Ok(CoreAuthProvider { - token: Some(token), - account_id: None, - is_fedramp_account: false, - }); - } - - if let Some(auth) = auth { - let token = auth.get_token()?; - Ok(CoreAuthProvider { - token: Some(token), - account_id: auth.get_account_id(), - is_fedramp_account: auth.is_fedramp_account(), - }) - } else { - Ok(CoreAuthProvider { - token: None, - account_id: None, - is_fedramp_account: false, - }) - } -} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 4880e431061f..2aa967d152ef 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -1,7 +1,5 @@ -pub mod api_bridge; pub mod auth; pub mod auth_env_telemetry; -pub mod provider_auth; pub mod token_data; mod device_code_auth; @@ -19,7 +17,6 @@ pub use server::ServerOptions; pub use server::ShutdownHandle; pub use server::run_login_server; -pub use api_bridge::auth_provider_from_auth; pub use auth::AgentIdentityAuthRecord; pub use auth::AuthConfig; pub use auth::AuthDotJson; @@ -46,6 +43,4 @@ pub use auth::read_openai_api_key_from_env; pub use auth::save_auth; pub use auth_env_telemetry::AuthEnvTelemetry; pub use auth_env_telemetry::collect_auth_env_telemetry; -pub use provider_auth::auth_manager_for_provider; -pub use provider_auth::required_auth_manager_for_provider; pub use token_data::TokenData; diff --git a/codex-rs/login/src/provider_auth.rs b/codex-rs/login/src/provider_auth.rs deleted file mode 100644 index 5a4bb6d82bb0..000000000000 --- a/codex-rs/login/src/provider_auth.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::sync::Arc; - -use codex_model_provider_info::ModelProviderInfo; - -use crate::AuthManager; - -/// Returns the provider-scoped auth manager when this provider uses command-backed auth. -/// -/// Providers without custom auth continue using the caller-supplied base manager. -pub fn auth_manager_for_provider( - auth_manager: Option>, - provider: &ModelProviderInfo, -) -> Option> { - match provider.auth.clone() { - Some(config) => Some(AuthManager::external_bearer_only(config)), - None => auth_manager, - } -} - -/// Returns an auth manager for request paths that always require authentication. -/// -/// Providers with command-backed auth get a bearer-only manager; otherwise the caller's manager -/// is reused unchanged. -pub fn required_auth_manager_for_provider( - auth_manager: Arc, - provider: &ModelProviderInfo, -) -> Arc { - match provider.auth.clone() { - Some(config) => AuthManager::external_bearer_only(config), - None => auth_manager, - } -} diff --git a/codex-rs/model-provider/BUILD.bazel b/codex-rs/model-provider/BUILD.bazel new file mode 100644 index 000000000000..7fd0b25b52a6 --- /dev/null +++ b/codex-rs/model-provider/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "model-provider", + crate_name = "codex_model_provider", +) diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml new file mode 100644 index 000000000000..c904d1bbfa51 --- /dev/null +++ b/codex-rs/model-provider/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-model-provider" +version.workspace = true + +[lib] +doctest = false +name = "codex_model_provider" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +async-trait = { workspace = true } +codex-api = { workspace = true } +codex-login = { workspace = true } +codex-model-provider-info = { workspace = true } +codex-protocol = { workspace = true } +http = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/model-provider/src/auth.rs b/codex-rs/model-provider/src/auth.rs new file mode 100644 index 000000000000..64640dcc960e --- /dev/null +++ b/codex-rs/model-provider/src/auth.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use codex_api::SharedAuthProvider; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderInfo; + +use crate::bearer_auth_provider::BearerAuthProvider; + +/// Returns the provider-scoped auth manager when this provider uses command-backed auth. +/// +/// Providers without custom auth continue using the caller-supplied base manager, when present. +pub(crate) fn auth_manager_for_provider( + auth_manager: Option>, + provider: &ModelProviderInfo, +) -> Option> { + match provider.auth.clone() { + Some(config) => Some(AuthManager::external_bearer_only(config)), + None => auth_manager, + } +} + +fn bearer_auth_provider_from_auth( + auth: Option<&CodexAuth>, + provider: &ModelProviderInfo, +) -> codex_protocol::error::Result { + if let Some(api_key) = provider.api_key()? { + return Ok(BearerAuthProvider { + token: Some(api_key), + account_id: None, + is_fedramp_account: false, + }); + } + + if let Some(token) = provider.experimental_bearer_token.clone() { + return Ok(BearerAuthProvider { + token: Some(token), + account_id: None, + is_fedramp_account: false, + }); + } + + if let Some(auth) = auth { + let token = auth.get_token()?; + Ok(BearerAuthProvider { + token: Some(token), + account_id: auth.get_account_id(), + is_fedramp_account: auth.is_fedramp_account(), + }) + } else { + Ok(BearerAuthProvider { + token: None, + account_id: None, + is_fedramp_account: false, + }) + } +} + +pub(crate) fn resolve_provider_auth( + auth: Option<&CodexAuth>, + provider: &ModelProviderInfo, +) -> codex_protocol::error::Result { + Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?)) +} diff --git a/codex-rs/model-provider/src/bearer_auth_provider.rs b/codex-rs/model-provider/src/bearer_auth_provider.rs new file mode 100644 index 000000000000..5a24ca6f78da --- /dev/null +++ b/codex-rs/model-provider/src/bearer_auth_provider.rs @@ -0,0 +1,102 @@ +use codex_api::AuthProvider; +use http::HeaderMap; +use http::HeaderValue; + +/// Bearer-token auth provider for OpenAI-compatible model-provider requests. +#[derive(Clone, Default)] +pub struct BearerAuthProvider { + pub token: Option, + pub account_id: Option, + pub is_fedramp_account: bool, +} + +impl BearerAuthProvider { + pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self { + Self { + token: token.map(str::to_string), + account_id: account_id.map(str::to_string), + is_fedramp_account: false, + } + } +} + +impl AuthProvider for BearerAuthProvider { + fn add_auth_headers(&self, headers: &mut HeaderMap) { + if let Some(token) = self.token.as_ref() + && let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}")) + { + let _ = headers.insert(http::header::AUTHORIZATION, header); + } + if let Some(account_id) = self.account_id.as_ref() + && let Ok(header) = HeaderValue::from_str(account_id) + { + let _ = headers.insert("ChatGPT-Account-ID", header); + } + if self.is_fedramp_account { + let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true")); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn bearer_auth_provider_reports_when_auth_header_will_attach() { + let auth = BearerAuthProvider { + token: Some("access-token".to_string()), + account_id: None, + is_fedramp_account: false, + }; + + assert_eq!( + codex_api::auth_header_telemetry(&auth), + codex_api::AuthHeaderTelemetry { + attached: true, + name: Some("authorization"), + } + ); + } + + #[test] + fn bearer_auth_provider_adds_auth_headers() { + let auth = BearerAuthProvider::for_test(Some("access-token"), Some("workspace-123")); + let mut headers = HeaderMap::new(); + + auth.add_auth_headers(&mut headers); + + assert_eq!( + headers + .get(http::header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + Some("Bearer access-token") + ); + assert_eq!( + headers + .get("ChatGPT-Account-ID") + .and_then(|value| value.to_str().ok()), + Some("workspace-123") + ); + } + + #[test] + fn bearer_auth_provider_adds_fedramp_routing_header_for_fedramp_accounts() { + let auth = BearerAuthProvider { + token: Some("access-token".to_string()), + account_id: Some("workspace-123".to_string()), + is_fedramp_account: true, + }; + let mut headers = HeaderMap::new(); + + auth.add_auth_headers(&mut headers); + + assert_eq!( + headers + .get("X-OpenAI-Fedramp") + .and_then(|value| value.to_str().ok()), + Some("true") + ); + } +} diff --git a/codex-rs/model-provider/src/lib.rs b/codex-rs/model-provider/src/lib.rs new file mode 100644 index 000000000000..f240c47db0b4 --- /dev/null +++ b/codex-rs/model-provider/src/lib.rs @@ -0,0 +1,9 @@ +mod auth; +mod bearer_auth_provider; +mod provider; + +pub use bearer_auth_provider::BearerAuthProvider; +pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider; +pub use provider::ModelProvider; +pub use provider::SharedModelProvider; +pub use provider::create_model_provider; diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs new file mode 100644 index 000000000000..b0a5a30f4e01 --- /dev/null +++ b/codex-rs/model-provider/src/provider.rs @@ -0,0 +1,126 @@ +use std::fmt; +use std::sync::Arc; + +use codex_api::Provider; +use codex_api::SharedAuthProvider; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_model_provider_info::ModelProviderInfo; + +use crate::auth::auth_manager_for_provider; +use crate::auth::resolve_provider_auth; + +/// Runtime provider abstraction used by model execution. +/// +/// Implementations own provider-specific behavior for a model backend. The +/// `ModelProviderInfo` returned by `info` is the serialized/configured provider +/// metadata used by the default OpenAI-compatible implementation. +#[async_trait::async_trait] +pub trait ModelProvider: fmt::Debug + Send + Sync { + /// Returns the configured provider metadata. + fn info(&self) -> &ModelProviderInfo; + + /// Returns the provider-scoped auth manager, when this provider uses one. + /// + /// TODO(celia-oai): Make auth manager access internal to this crate so callers + /// resolve provider-specific auth only through `ModelProvider`. We first need + /// to think through whether Codex should have a unified provider-specific auth + /// manager throughout the codebase; that is a larger refactor than this change. + fn auth_manager(&self) -> Option>; + + /// Returns the current provider-scoped auth value, if one is configured. + async fn auth(&self) -> Option; + + /// Returns provider configuration adapted for the API client. + async fn api_provider(&self) -> codex_protocol::error::Result { + let auth = self.auth().await; + self.info() + .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)) + } + + /// Returns the auth provider used to attach request credentials. + async fn api_auth(&self) -> codex_protocol::error::Result { + let auth = self.auth().await; + resolve_provider_auth(auth.as_ref(), self.info()) + } +} + +/// Shared runtime model provider handle. +pub type SharedModelProvider = Arc; + +/// Creates the default runtime model provider for configured provider metadata. +pub fn create_model_provider( + provider_info: ModelProviderInfo, + auth_manager: Option>, +) -> SharedModelProvider { + let auth_manager = auth_manager_for_provider(auth_manager, &provider_info); + Arc::new(ConfiguredModelProvider { + info: provider_info, + auth_manager, + }) +} + +/// Runtime model provider backed by configured `ModelProviderInfo`. +#[derive(Clone, Debug)] +struct ConfiguredModelProvider { + info: ModelProviderInfo, + auth_manager: Option>, +} + +#[async_trait::async_trait] +impl ModelProvider for ConfiguredModelProvider { + fn info(&self) -> &ModelProviderInfo { + &self.info + } + + fn auth_manager(&self) -> Option> { + self.auth_manager.clone() + } + + async fn auth(&self) -> Option { + match self.auth_manager.as_ref() { + Some(auth_manager) => auth_manager.auth().await, + None => None, + } + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU64; + + use codex_protocol::config_types::ModelProviderAuthInfo; + + use super::*; + + fn provider_info_with_command_auth() -> ModelProviderInfo { + ModelProviderInfo { + auth: Some(ModelProviderAuthInfo { + command: "print-token".to_string(), + args: Vec::new(), + timeout_ms: NonZeroU64::new(5_000).expect("timeout should be non-zero"), + refresh_interval_ms: 300_000, + cwd: std::env::current_dir() + .expect("current dir should be available") + .try_into() + .expect("current dir should be absolute"), + }), + requires_openai_auth: false, + ..ModelProviderInfo::create_openai_provider(/*base_url*/ None) + } + } + + #[test] + fn create_model_provider_builds_command_auth_manager_without_base_manager() { + let provider = create_model_provider( + provider_info_with_command_auth(), + /*auth_manager*/ None, + ); + + let auth_manager = provider + .auth_manager() + .expect("command auth provider should have an auth manager"); + + assert!(auth_manager.has_external_auth()); + } +} diff --git a/codex-rs/models-manager/Cargo.toml b/codex-rs/models-manager/Cargo.toml index 58eff2437a59..59a2bff101c0 100644 --- a/codex-rs/models-manager/Cargo.toml +++ b/codex-rs/models-manager/Cargo.toml @@ -22,6 +22,7 @@ codex-feedback = { workspace = true } codex-login = { workspace = true } codex-model-provider-info = { workspace = true } codex-otel = { workspace = true } +codex-model-provider = { workspace = true } codex-protocol = { workspace = true } codex-response-debug-context = { workspace = true } codex-utils-output-truncation = { workspace = true } diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index a7d4aa12ab4c..4b0e2ca777bd 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -7,6 +7,7 @@ use codex_api::ModelsClient; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; use codex_api::TransportError; +use codex_api::auth_header_telemetry; use codex_api::map_api_error; use codex_app_server_protocol::AuthMode; use codex_feedback::FeedbackRequestTags; @@ -14,10 +15,10 @@ use codex_feedback::emit_feedback_request_tags_with_auth_env; use codex_login::AuthEnvTelemetry; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_login::auth_provider_from_auth; use codex_login::collect_auth_env_telemetry; use codex_login::default_client::build_reqwest_client; -use codex_login::required_auth_manager_for_provider; +use codex_model_provider::SharedModelProvider; +use codex_model_provider::create_model_provider; use codex_model_provider_info::ModelProviderInfo; use codex_otel::TelemetryAuthMode; use codex_protocol::config_types::CollaborationModeMask; @@ -178,10 +179,9 @@ pub struct ModelsManager { remote_models: RwLock>, catalog_mode: CatalogMode, collaboration_modes_config: CollaborationModesConfig, - auth_manager: Arc, etag: RwLock>, cache_manager: ModelsCacheManager, - provider: ModelProviderInfo, + provider: SharedModelProvider, } impl ModelsManager { @@ -206,14 +206,17 @@ impl ModelsManager { } /// Construct a manager with an explicit provider used for remote model refreshes. + // TODO(celia-oai): Revisit this ownership direction: the model provider should likely + // own or return the models manager instead of requiring the manager to construct and use + // a provider from provider info. pub fn new_with_provider( codex_home: PathBuf, auth_manager: Arc, model_catalog: Option, collaboration_modes_config: CollaborationModesConfig, - provider: ModelProviderInfo, + provider_info: ModelProviderInfo, ) -> Self { - let auth_manager = required_auth_manager_for_provider(auth_manager, &provider); + let model_provider = create_model_provider(provider_info, Some(auth_manager)); let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); let catalog_mode = if model_catalog.is_some() { @@ -228,10 +231,9 @@ impl ModelsManager { remote_models: RwLock::new(remote_models), catalog_mode, collaboration_modes_config, - auth_manager, etag: RwLock::new(None), cache_manager, - provider, + provider: model_provider, } } @@ -395,9 +397,11 @@ impl ModelsManager { return Ok(()); } - if self.auth_manager.auth_mode() != Some(AuthMode::Chatgpt) - && !self.provider.has_command_auth() - { + let auth_mode = self + .provider + .auth_manager() + .and_then(|auth_manager| auth_manager.auth_mode()); + if auth_mode != Some(AuthMode::Chatgpt) && !self.provider.info().has_command_auth() { if matches!( refresh_strategy, RefreshStrategy::Offline | RefreshStrategy::OnlineIfUncached @@ -432,19 +436,21 @@ impl ModelsManager { async fn fetch_and_update_models(&self) -> CoreResult<()> { let _timer = codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]); - let auth = self.auth_manager.auth().await; + let auth_manager = self.provider.auth_manager(); + let codex_api_key_env_enabled = auth_manager + .as_ref() + .is_some_and(|auth_manager| auth_manager.codex_api_key_env_enabled()); + let auth = self.provider.auth().await; let auth_mode = auth.as_ref().map(CodexAuth::auth_mode); - let api_provider = self.provider.to_api_provider(auth_mode)?; - let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?; - let auth_env = collect_auth_env_telemetry( - &self.provider, - self.auth_manager.codex_api_key_env_enabled(), - ); + let api_provider = self.provider.api_provider().await?; + let api_auth = self.provider.api_auth().await?; + let auth_env = collect_auth_env_telemetry(self.provider.info(), codex_api_key_env_enabled); let transport = ReqwestTransport::new(build_reqwest_client()); + let auth_telemetry = auth_header_telemetry(api_auth.as_ref()); let request_telemetry: Arc = Arc::new(ModelsRequestTelemetry { auth_mode: auth_mode.map(|mode| TelemetryAuthMode::from(mode).to_string()), - auth_header_attached: api_auth.auth_header_attached(), - auth_header_name: api_auth.auth_header_name(), + auth_header_attached: auth_telemetry.attached, + auth_header_name: auth_telemetry.name, auth_env, }); let client = ModelsClient::new(transport, api_provider, api_auth) @@ -520,7 +526,11 @@ impl ModelsManager { remote_models.sort_by(|a, b| a.priority.cmp(&b.priority)); let mut presets: Vec = remote_models.into_iter().map(Into::into).collect(); - let chatgpt_mode = matches!(self.auth_manager.auth_mode(), Some(AuthMode::Chatgpt)); + let auth_mode = self + .provider + .auth_manager() + .and_then(|auth_manager| auth_manager.auth_mode()); + let chatgpt_mode = matches!(auth_mode, Some(AuthMode::Chatgpt)); presets = ModelPreset::filter_by_auth(presets, chatgpt_mode); ModelPreset::mark_default_by_picker_visibility(&mut presets);