Skip to content

Commit a803790

Browse files
feat: add opt-in provider runtime abstraction (#17713)
## Summary - Add `codex-model-provider` as the runtime home for model-provider behavior that does not belong in `codex-core`, `codex-login`, or `codex-api`. - The new crate wraps configured `ModelProviderInfo` in a `ModelProvider` trait object that can resolve the API provider config, provider-scoped auth manager, and request auth provider for each call. - This centralizes provider auth behavior in one place today, and gives us an extension point for future provider-specific auth, model listing, request setup, and related runtime behavior. ## Tests Ran tests manually to make sure that provider auth under different configs still work as expected. --------- Co-authored-by: pakrym-oai <pakrym@openai.com>
1 parent 91e8eeb commit a803790

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+577
-369
lines changed

codex-rs/Cargo.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ members = [
9292
"thread-store",
9393
"codex-experimental-api-macros",
9494
"plugin",
95+
"model-provider",
9596
]
9697
resolver = "2"
9798

@@ -154,6 +155,7 @@ codex-network-proxy = { path = "network-proxy" }
154155
codex-ollama = { path = "ollama" }
155156
codex-otel = { path = "otel" }
156157
codex-plugin = { path = "plugin" }
158+
codex-model-provider = { path = "model-provider" }
157159
codex-process-hardening = { path = "process-hardening" }
158160
codex-protocol = { path = "protocol" }
159161
codex-realtime-webrtc = { path = "realtime-webrtc" }

codex-rs/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ codex-features = { workspace = true }
3737
codex-login = { workspace = true }
3838
codex-mcp = { workspace = true }
3939
codex-mcp-server = { workspace = true }
40+
codex-model-provider = { workspace = true }
4041
codex-protocol = { workspace = true }
4142
codex-responses-api-proxy = { workspace = true }
4243
codex-rmcp-client = { workspace = true }

codex-rs/cli/src/responses_cmd.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use clap::Parser;
22
use codex_core::config::Config;
3+
use codex_model_provider::create_model_provider;
34
use codex_utils_cli::CliConfigOverrides;
45
use serde_json::json;
56
use tokio::io::AsyncReadExt;
@@ -29,16 +30,9 @@ pub(crate) async fn run_responses_command(
2930
let base_auth_manager = codex_login::AuthManager::shared_from_config(
3031
&config, /*enable_codex_api_key_env*/ true,
3132
);
32-
let auth_manager =
33-
codex_login::auth_manager_for_provider(Some(base_auth_manager), &config.model_provider);
34-
let auth = match auth_manager {
35-
Some(auth_manager) => auth_manager.auth().await,
36-
None => None,
37-
};
38-
let api_provider = config
39-
.model_provider
40-
.to_api_provider(auth.as_ref().map(codex_login::CodexAuth::auth_mode))?;
41-
let api_auth = codex_login::auth_provider_from_auth(auth, &config.model_provider)?;
33+
let model_provider = create_model_provider(config.model_provider, Some(base_auth_manager));
34+
let api_provider = model_provider.api_provider().await?;
35+
let api_auth = model_provider.api_auth().await?;
4236
let client = codex_api::ResponsesClient::new(
4337
codex_api::ReqwestTransport::new(codex_login::default_client::build_reqwest_client()),
4438
api_provider,

codex-rs/codex-api/src/api_bridge.rs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::AuthProvider as ApiAuthProvider;
21
use crate::TransportError;
32
use crate::error::ApiError;
43
use crate::rate_limits::parse_promo_message;
@@ -12,7 +11,6 @@ use codex_protocol::error::RetryLimitReachedError;
1211
use codex_protocol::error::UnexpectedResponseError;
1312
use codex_protocol::error::UsageLimitReachedError;
1413
use http::HeaderMap;
15-
use http::HeaderValue;
1614
use serde::Deserialize;
1715
use serde_json::Value;
1816

@@ -174,48 +172,3 @@ struct UsageErrorBody {
174172
plan_type: Option<PlanType>,
175173
resets_at: Option<i64>,
176174
}
177-
178-
#[derive(Clone, Default)]
179-
pub struct CoreAuthProvider {
180-
pub token: Option<String>,
181-
pub account_id: Option<String>,
182-
pub is_fedramp_account: bool,
183-
}
184-
185-
impl CoreAuthProvider {
186-
pub fn auth_header_attached(&self) -> bool {
187-
self.token
188-
.as_ref()
189-
.is_some_and(|token| http::HeaderValue::from_str(&format!("Bearer {token}")).is_ok())
190-
}
191-
192-
pub fn auth_header_name(&self) -> Option<&'static str> {
193-
self.auth_header_attached().then_some("authorization")
194-
}
195-
196-
pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self {
197-
Self {
198-
token: token.map(str::to_string),
199-
account_id: account_id.map(str::to_string),
200-
is_fedramp_account: false,
201-
}
202-
}
203-
}
204-
205-
impl ApiAuthProvider for CoreAuthProvider {
206-
fn add_auth_headers(&self, headers: &mut HeaderMap) {
207-
if let Some(token) = self.token.as_ref()
208-
&& let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}"))
209-
{
210-
let _ = headers.insert(http::header::AUTHORIZATION, header);
211-
}
212-
if let Some(account_id) = self.account_id.as_ref()
213-
&& let Ok(header) = HeaderValue::from_str(account_id)
214-
{
215-
let _ = headers.insert("ChatGPT-Account-ID", header);
216-
}
217-
if self.is_fedramp_account {
218-
crate::auth::add_fedramp_routing_header(headers);
219-
}
220-
}
221-
}

codex-rs/codex-api/src/api_bridge_tests.rs

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -130,55 +130,3 @@ fn map_api_error_extracts_identity_auth_details_from_headers() {
130130
);
131131
assert_eq!(err.identity_error_code.as_deref(), Some("token_expired"));
132132
}
133-
134-
#[test]
135-
fn core_auth_provider_reports_when_auth_header_will_attach() {
136-
let auth = CoreAuthProvider {
137-
token: Some("access-token".to_string()),
138-
account_id: None,
139-
is_fedramp_account: false,
140-
};
141-
142-
assert!(auth.auth_header_attached());
143-
assert_eq!(auth.auth_header_name(), Some("authorization"));
144-
}
145-
146-
#[test]
147-
fn core_auth_provider_adds_auth_headers() {
148-
let auth = CoreAuthProvider::for_test(Some("access-token"), Some("workspace-123"));
149-
let mut headers = HeaderMap::new();
150-
151-
crate::AuthProvider::add_auth_headers(&auth, &mut headers);
152-
153-
assert_eq!(
154-
headers
155-
.get(http::header::AUTHORIZATION)
156-
.and_then(|value| value.to_str().ok()),
157-
Some("Bearer access-token")
158-
);
159-
assert_eq!(
160-
headers
161-
.get("ChatGPT-Account-ID")
162-
.and_then(|value| value.to_str().ok()),
163-
Some("workspace-123")
164-
);
165-
}
166-
167-
#[test]
168-
fn core_auth_provider_adds_fedramp_routing_header_for_fedramp_accounts() {
169-
let auth = CoreAuthProvider {
170-
token: Some("access-token".to_string()),
171-
account_id: Some("workspace-123".to_string()),
172-
is_fedramp_account: true,
173-
};
174-
let mut headers = HeaderMap::new();
175-
176-
crate::AuthProvider::add_auth_headers(&auth, &mut headers);
177-
178-
assert_eq!(
179-
headers
180-
.get("X-OpenAI-Fedramp")
181-
.and_then(|value| value.to_str().ok()),
182-
Some("true")
183-
);
184-
}

codex-rs/codex-api/src/auth.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use http::HeaderMap;
2-
use http::HeaderValue;
2+
use std::sync::Arc;
33

44
/// Adds authentication headers to API requests.
55
///
@@ -10,25 +10,23 @@ pub trait AuthProvider: Send + Sync {
1010
fn add_auth_headers(&self, headers: &mut HeaderMap);
1111
}
1212

13-
pub(crate) fn add_fedramp_routing_header(headers: &mut HeaderMap) {
14-
headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
15-
}
16-
17-
#[cfg(test)]
18-
mod tests {
19-
use super::*;
13+
/// Shared auth handle passed through API clients.
14+
pub type SharedAuthProvider = Arc<dyn AuthProvider>;
2015

21-
#[test]
22-
fn add_fedramp_routing_header_sets_header() {
23-
let mut headers = HeaderMap::new();
24-
25-
add_fedramp_routing_header(&mut headers);
16+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17+
pub struct AuthHeaderTelemetry {
18+
pub attached: bool,
19+
pub name: Option<&'static str>,
20+
}
2621

27-
assert_eq!(
28-
headers
29-
.get("X-OpenAI-Fedramp")
30-
.and_then(|v| v.to_str().ok()),
31-
Some("true")
32-
);
22+
pub fn auth_header_telemetry(auth: &dyn AuthProvider) -> AuthHeaderTelemetry {
23+
let mut headers = HeaderMap::new();
24+
auth.add_auth_headers(&mut headers);
25+
let name = headers
26+
.contains_key(http::header::AUTHORIZATION)
27+
.then_some("authorization");
28+
AuthHeaderTelemetry {
29+
attached: name.is_some(),
30+
name,
3331
}
3432
}

codex-rs/codex-api/src/endpoint/compact.rs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::auth::AuthProvider;
1+
use crate::auth::SharedAuthProvider;
22
use crate::common::CompactionInput;
33
use crate::endpoint::session::EndpointSession;
44
use crate::error::ApiError;
@@ -12,12 +12,12 @@ use serde::Deserialize;
1212
use serde_json::to_value;
1313
use std::sync::Arc;
1414

15-
pub struct CompactClient<T: HttpTransport, A: AuthProvider> {
16-
session: EndpointSession<T, A>,
15+
pub struct CompactClient<T: HttpTransport> {
16+
session: EndpointSession<T>,
1717
}
1818

19-
impl<T: HttpTransport, A: AuthProvider> CompactClient<T, A> {
20-
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
19+
impl<T: HttpTransport> CompactClient<T> {
20+
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
2121
Self {
2222
session: EndpointSession::new(transport, provider, auth),
2323
}
@@ -86,18 +86,8 @@ mod tests {
8686
}
8787
}
8888

89-
#[derive(Clone, Default)]
90-
struct DummyAuth;
91-
92-
impl AuthProvider for DummyAuth {
93-
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
94-
}
95-
9689
#[test]
9790
fn path_is_responses_compact() {
98-
assert_eq!(
99-
CompactClient::<DummyTransport, DummyAuth>::path(),
100-
"responses/compact"
101-
);
91+
assert_eq!(CompactClient::<DummyTransport>::path(), "responses/compact");
10292
}
10393
}

codex-rs/codex-api/src/endpoint/memories.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::auth::AuthProvider;
1+
use crate::auth::SharedAuthProvider;
22
use crate::common::MemorySummarizeInput;
33
use crate::common::MemorySummarizeOutput;
44
use crate::endpoint::session::EndpointSession;
@@ -12,12 +12,12 @@ use serde::Deserialize;
1212
use serde_json::to_value;
1313
use std::sync::Arc;
1414

15-
pub struct MemoriesClient<T: HttpTransport, A: AuthProvider> {
16-
session: EndpointSession<T, A>,
15+
pub struct MemoriesClient<T: HttpTransport> {
16+
session: EndpointSession<T>,
1717
}
1818

19-
impl<T: HttpTransport, A: AuthProvider> MemoriesClient<T, A> {
20-
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
19+
impl<T: HttpTransport> MemoriesClient<T> {
20+
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
2121
Self {
2222
session: EndpointSession::new(transport, provider, auth),
2323
}
@@ -67,6 +67,7 @@ struct SummarizeResponse {
6767
#[cfg(test)]
6868
mod tests {
6969
use super::*;
70+
use crate::auth::AuthProvider;
7071
use crate::common::RawMemory;
7172
use crate::common::RawMemoryMetadata;
7273
use crate::provider::RetryConfig;
@@ -157,7 +158,7 @@ mod tests {
157158
#[test]
158159
fn path_is_memories_trace_summarize_for_wire_compatibility() {
159160
assert_eq!(
160-
MemoriesClient::<DummyTransport, DummyAuth>::path(),
161+
MemoriesClient::<DummyTransport>::path(),
161162
"memories/trace_summarize"
162163
);
163164
}
@@ -178,7 +179,7 @@ mod tests {
178179
let client = MemoriesClient::new(
179180
transport.clone(),
180181
provider("https://example.com/api/codex"),
181-
DummyAuth,
182+
Arc::new(DummyAuth),
182183
);
183184

184185
let input = MemorySummarizeInput {

0 commit comments

Comments
 (0)