mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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>
This commit is contained in:
committed by
GitHub
Unverified
parent
91e8eebd03
commit
a803790a10
Generated
+16
-1
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PlanType>,
|
||||
resets_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CoreAuthProvider {
|
||||
pub token: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
/// Shared auth handle passed through API clients.
|
||||
pub type SharedAuthProvider = Arc<dyn AuthProvider>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct AuthHeaderTelemetry {
|
||||
pub attached: bool,
|
||||
pub name: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn add_fedramp_routing_header_sets_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
add_fedramp_routing_header(&mut headers);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
pub struct CompactClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> CompactClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> CompactClient<T> {
|
||||
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::<DummyTransport, DummyAuth>::path(),
|
||||
"responses/compact"
|
||||
);
|
||||
assert_eq!(CompactClient::<DummyTransport>::path(), "responses/compact");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
pub struct MemoriesClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> MemoriesClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> MemoriesClient<T> {
|
||||
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::<DummyTransport, DummyAuth>::path(),
|
||||
MemoriesClient::<DummyTransport>::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 {
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
pub struct ModelsClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> ModelsClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> ModelsClient<T> {
|
||||
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
|
||||
Self {
|
||||
session: EndpointSession::new(transport, provider, auth),
|
||||
}
|
||||
@@ -76,6 +76,7 @@ impl<T: HttpTransport, A: AuthProvider> ModelsClient<T, A> {
|
||||
#[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
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
pub struct RealtimeCallClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
}
|
||||
|
||||
/// Answer from creating a WebRTC Realtime call.
|
||||
@@ -44,8 +44,8 @@ struct BackendRealtimeCallRequest<'a> {
|
||||
session: &'a Value,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> RealtimeCallClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> RealtimeCallClient<T> {
|
||||
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<String, ApiError>
|
||||
#[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())
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
session: EndpointSession<T, A>,
|
||||
pub struct ResponsesClient<T: HttpTransport> {
|
||||
session: EndpointSession<T>,
|
||||
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ pub struct ResponsesOptions {
|
||||
pub turn_state: Option<Arc<OnceLock<String>>>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> ResponsesClient<T, A> {
|
||||
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> ResponsesClient<T> {
|
||||
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
|
||||
Self {
|
||||
session: EndpointSession::new(transport, provider, auth),
|
||||
sse_telemetry: None,
|
||||
|
||||
@@ -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<A: AuthProvider> {
|
||||
pub struct ResponsesWebsocketClient {
|
||||
provider: Provider,
|
||||
auth: A,
|
||||
auth: SharedAuthProvider,
|
||||
}
|
||||
|
||||
impl<A: AuthProvider> ResponsesWebsocketClient<A> {
|
||||
pub fn new(provider: Provider, auth: A) -> Self {
|
||||
impl ResponsesWebsocketClient {
|
||||
pub fn new(provider: Provider, auth: SharedAuthProvider) -> Self {
|
||||
Self { provider, auth }
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T: HttpTransport, A: AuthProvider> {
|
||||
pub(crate) struct EndpointSession<T: HttpTransport> {
|
||||
transport: T,
|
||||
provider: Provider,
|
||||
auth: A,
|
||||
auth: SharedAuthProvider,
|
||||
request_telemetry: Option<Arc<dyn RequestTelemetry>>,
|
||||
}
|
||||
|
||||
impl<T: HttpTransport, A: AuthProvider> EndpointSession<T, A> {
|
||||
pub(crate) fn new(transport: T, provider: Provider, auth: A) -> Self {
|
||||
impl<T: HttpTransport> EndpointSession<T> {
|
||||
pub(crate) fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
|
||||
Self {
|
||||
transport,
|
||||
provider,
|
||||
|
||||
@@ -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<UploadedOpenAiFile, OpenAiFileError> {
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+45
-46
@@ -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<Arc<AuthManager>>,
|
||||
conversation_id: ThreadId,
|
||||
window_generation: AtomicU64,
|
||||
installation_id: String,
|
||||
provider: ModelProviderInfo,
|
||||
provider: SharedModelProvider,
|
||||
auth_env_telemetry: AuthEnvTelemetry,
|
||||
session_source: SessionSource,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
@@ -164,8 +165,8 @@ struct ModelClientState {
|
||||
/// share the same auth/provider setup flow.
|
||||
struct CurrentClientSetup {
|
||||
auth: Option<CodexAuth>,
|
||||
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<Arc<AuthManager>>,
|
||||
conversation_id: ThreadId,
|
||||
installation_id: String,
|
||||
provider: ModelProviderInfo,
|
||||
provider_info: ModelProviderInfo,
|
||||
session_source: SessionSource,
|
||||
model_verbosity: Option<VerbosityConfig>,
|
||||
enable_request_compression: bool,
|
||||
include_timing_metrics: bool,
|
||||
beta_features_header: Option<String>,
|
||||
) -> 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<Arc<AuthManager>> {
|
||||
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<CurrentClientSetup> {
|
||||
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<Arc<OnceLock<String>>>,
|
||||
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<W3cTraceContext>,
|
||||
) -> Result<WebsocketStreamOutcome> {
|
||||
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<ServiceTier>,
|
||||
turn_metadata_header: Option<&str>,
|
||||
) -> Result<ResponseStream> {
|
||||
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<AuthMode>,
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Arc<AuthManager>>,
|
||||
pub(crate) model_info: ModelInfo,
|
||||
pub(crate) session_telemetry: SessionTelemetry,
|
||||
pub(crate) provider: ModelProviderInfo,
|
||||
pub(crate) provider: SharedModelProvider,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,7 +27,7 @@ impl SessionTask for CompactTask {
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Option<String> {
|
||||
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,
|
||||
|
||||
@@ -224,7 +224,7 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
|
||||
let base_config = turn.config.clone();
|
||||
let mut config = (*base_config).clone();
|
||||
config.model = Some(turn.model_info.slug.clone());
|
||||
config.model_provider = turn.provider.clone();
|
||||
config.model_provider = turn.provider.info().clone();
|
||||
config.model_reasoning_effort = turn
|
||||
.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level);
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_config::types::ShellEnvironmentPolicy;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -353,10 +354,10 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
let manager = thread_manager();
|
||||
session.services.agent_control = manager.agent_control();
|
||||
let mut config = (*turn.config).clone();
|
||||
let provider =
|
||||
let provider_info =
|
||||
built_in_model_providers(/* openai_base_url */ /*openai_base_url*/ None)["ollama"].clone();
|
||||
config.model_provider_id = "ollama".to_string();
|
||||
config.model_provider = provider.clone();
|
||||
config.model_provider = provider_info.clone();
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
@@ -365,7 +366,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy should be set");
|
||||
turn.provider = provider;
|
||||
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
|
||||
turn.config = Arc::new(config);
|
||||
|
||||
let invocation = invocation(
|
||||
@@ -3505,7 +3506,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions = Some(base_instructions.text);
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.clone();
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
@@ -3559,7 +3560,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions = None;
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.clone();
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
|
||||
@@ -234,7 +234,7 @@ move /y tokens.next tokens.txt >nul
|
||||
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()) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<CoreAuthProvider> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Arc<AuthManager>>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Option<Arc<AuthManager>> {
|
||||
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<AuthManager>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Arc<AuthManager> {
|
||||
match provider.auth.clone() {
|
||||
Some(config) => AuthManager::external_bearer_only(config),
|
||||
None => auth_manager,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "model-provider",
|
||||
crate_name = "codex_model_provider",
|
||||
)
|
||||
@@ -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 }
|
||||
@@ -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<Arc<AuthManager>>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> Option<Arc<AuthManager>> {
|
||||
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<BearerAuthProvider> {
|
||||
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<SharedAuthProvider> {
|
||||
Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?))
|
||||
}
|
||||
@@ -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<String>,
|
||||
pub account_id: Option<String>,
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<Arc<AuthManager>>;
|
||||
|
||||
/// Returns the current provider-scoped auth value, if one is configured.
|
||||
async fn auth(&self) -> Option<CodexAuth>;
|
||||
|
||||
/// Returns provider configuration adapted for the API client.
|
||||
async fn api_provider(&self) -> codex_protocol::error::Result<Provider> {
|
||||
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<SharedAuthProvider> {
|
||||
let auth = self.auth().await;
|
||||
resolve_provider_auth(auth.as_ref(), self.info())
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared runtime model provider handle.
|
||||
pub type SharedModelProvider = Arc<dyn ModelProvider>;
|
||||
|
||||
/// Creates the default runtime model provider for configured provider metadata.
|
||||
pub fn create_model_provider(
|
||||
provider_info: ModelProviderInfo,
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
) -> 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<Arc<AuthManager>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ModelProvider for ConfiguredModelProvider {
|
||||
fn info(&self) -> &ModelProviderInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
|
||||
self.auth_manager.clone()
|
||||
}
|
||||
|
||||
async fn auth(&self) -> Option<CodexAuth> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<Vec<ModelInfo>>,
|
||||
catalog_mode: CatalogMode,
|
||||
collaboration_modes_config: CollaborationModesConfig,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
etag: RwLock<Option<String>>,
|
||||
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<AuthManager>,
|
||||
model_catalog: Option<ModelsResponse>,
|
||||
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<dyn RequestTelemetry> = 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<ModelPreset> = 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);
|
||||
|
||||
Reference in New Issue
Block a user