mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
cloud_requirements PAC support
This commit is contained in:
@@ -9,6 +9,9 @@ use crate::types::TokenUsageProfile;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use anyhow::Result;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::ClientRouteClass;
|
||||
use codex_client::OutboundProxyConfig;
|
||||
use codex_client::build_reqwest_client_for_route;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::with_chatgpt_cloudflare_cookie_store;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -149,23 +152,36 @@ impl fmt::Debug for Client {
|
||||
|
||||
impl Client {
|
||||
pub fn new(base_url: impl Into<String>) -> Result<Self> {
|
||||
let mut base_url = base_url.into();
|
||||
// Normalize common ChatGPT hostnames to include /backend-api so we hit the WHAM paths.
|
||||
// Also trim trailing slashes for consistent URL building.
|
||||
while base_url.ends_with('/') {
|
||||
base_url.pop();
|
||||
}
|
||||
if (base_url.starts_with("https://chatgpt.com")
|
||||
|| base_url.starts_with("https://chat.openai.com"))
|
||||
&& !base_url.contains("/backend-api")
|
||||
{
|
||||
base_url = format!("{base_url}/backend-api");
|
||||
}
|
||||
let base_url = normalize_base_url(base_url.into());
|
||||
let http = build_reqwest_client_with_custom_ca(with_chatgpt_cloudflare_cookie_store(
|
||||
reqwest::Client::builder(),
|
||||
))?;
|
||||
Ok(Self::with_http_client(base_url, http))
|
||||
}
|
||||
|
||||
/// Creates an authenticated client routed for the cloud config bundle endpoint.
|
||||
pub fn from_auth_for_config_bundle(
|
||||
base_url: impl Into<String>,
|
||||
auth: &CodexAuth,
|
||||
outbound_proxy_config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<Self> {
|
||||
let base_url = normalize_base_url(base_url.into());
|
||||
let path_style = PathStyle::from_base_url(&base_url);
|
||||
Ok(Self {
|
||||
let request_url = config_bundle_url(&base_url, path_style);
|
||||
let http = build_reqwest_client_for_route(
|
||||
with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()),
|
||||
&request_url,
|
||||
ClientRouteClass::Api,
|
||||
outbound_proxy_config,
|
||||
)?;
|
||||
Ok(Self::with_http_client(base_url, http)
|
||||
.with_user_agent(get_codex_user_agent())
|
||||
.with_auth_provider(codex_model_provider::auth_provider_from_auth(auth)))
|
||||
}
|
||||
|
||||
fn with_http_client(base_url: String, http: reqwest::Client) -> Self {
|
||||
let path_style = PathStyle::from_base_url(&base_url);
|
||||
Self {
|
||||
base_url,
|
||||
http,
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
@@ -173,7 +189,7 @@ impl Client {
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
path_style,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_auth(base_url: impl Into<String>, auth: &CodexAuth) -> Result<Self> {
|
||||
@@ -422,10 +438,7 @@ impl Client {
|
||||
pub async fn get_config_bundle(
|
||||
&self,
|
||||
) -> std::result::Result<ConfigBundleResponse, RequestError> {
|
||||
let url = match self.path_style {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/config/bundle", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/config/bundle", self.base_url),
|
||||
};
|
||||
let url = config_bundle_url(&self.base_url, self.path_style);
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?;
|
||||
self.decode_json::<ConfigBundleResponse>(&url, &ct, &body)
|
||||
@@ -664,6 +677,26 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_base_url(mut base_url: String) -> String {
|
||||
while base_url.ends_with('/') {
|
||||
base_url.pop();
|
||||
}
|
||||
if (base_url.starts_with("https://chatgpt.com")
|
||||
|| base_url.starts_with("https://chat.openai.com"))
|
||||
&& !base_url.contains("/backend-api")
|
||||
{
|
||||
base_url = format!("{base_url}/backend-api");
|
||||
}
|
||||
base_url
|
||||
}
|
||||
|
||||
fn config_bundle_url(base_url: &str, path_style: PathStyle) -> String {
|
||||
match path_style {
|
||||
PathStyle::CodexApi => format!("{base_url}/api/codex/config/bundle"),
|
||||
PathStyle::ChatGptApi => format!("{base_url}/wham/config/bundle"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -6,6 +6,8 @@ use codex_config::CloudConfigFragment;
|
||||
use codex_config::CloudConfigTomlBundle;
|
||||
use codex_config::CloudRequirementsFragment;
|
||||
use codex_config::CloudRequirementsTomlBundle;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_login::CodexAuth;
|
||||
use std::future::Future;
|
||||
|
||||
@@ -46,24 +48,34 @@ pub(crate) trait BundleClient: Send + Sync {
|
||||
|
||||
pub(crate) struct BackendBundleClient {
|
||||
base_url: String,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
|
||||
impl BackendBundleClient {
|
||||
pub(crate) fn new(base_url: String) -> Self {
|
||||
Self { base_url }
|
||||
pub(crate) fn from_auth_manager(base_url: String, auth_manager: &AuthManager) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
auth_route_config: auth_manager.auth_route_config().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BundleClient for BackendBundleClient {
|
||||
async fn get_bundle(&self, auth: &CodexAuth) -> Result<CloudConfigBundle, BundleRequestError> {
|
||||
let client = BackendClient::from_auth(self.base_url.clone(), auth)
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"Failed to construct backend client for cloud config bundle"
|
||||
);
|
||||
})
|
||||
.map_err(|_| BundleRequestError::Retryable(RetryableFailureKind::BackendClientInit))?;
|
||||
let client = BackendClient::from_auth_for_config_bundle(
|
||||
self.base_url.clone(),
|
||||
auth,
|
||||
self.auth_route_config
|
||||
.as_ref()
|
||||
.map(AuthRouteConfig::route_config),
|
||||
)
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"Failed to construct backend client for cloud config bundle"
|
||||
);
|
||||
})
|
||||
.map_err(|_| BundleRequestError::Retryable(RetryableFailureKind::BackendClientInit))?;
|
||||
|
||||
let response = client
|
||||
.get_config_bundle()
|
||||
@@ -134,3 +146,7 @@ fn requirements_fragment_from_delivered(
|
||||
contents: fragment.contents,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "backend_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::*;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn cloud_bundle_client_preserves_system_proxy_config() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let auth_manager = AuthManager::new_with_auth_route_config(
|
||||
codex_home.path().to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some("https://chatgpt.com/backend-api/".to_string()),
|
||||
Some(AuthRouteConfig::respect_system_proxy()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = BackendBundleClient::from_auth_manager(
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
&auth_manager,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client.auth_route_config,
|
||||
Some(AuthRouteConfig::respect_system_proxy())
|
||||
);
|
||||
}
|
||||
@@ -24,9 +24,10 @@ pub fn cloud_config_bundle_loader(
|
||||
chatgpt_base_url: String,
|
||||
codex_home: PathBuf,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let backend_client = BackendBundleClient::from_auth_manager(chatgpt_base_url, &auth_manager);
|
||||
let service = CloudConfigBundleService::new(
|
||||
auth_manager,
|
||||
Arc::new(BackendBundleClient::new(chatgpt_base_url)),
|
||||
Arc::new(backend_client),
|
||||
codex_home,
|
||||
CLOUD_CONFIG_BUNDLE_TIMEOUT,
|
||||
);
|
||||
|
||||
@@ -2196,6 +2196,11 @@ impl AuthManager {
|
||||
self.enable_codex_api_key_env
|
||||
}
|
||||
|
||||
/// Returns the route-selection policy used by auth-owned clients.
|
||||
pub fn auth_route_config(&self) -> Option<&AuthRouteConfig> {
|
||||
self.auth_route_config.as_ref()
|
||||
}
|
||||
|
||||
/// Convenience constructor returning an `Arc` wrapper.
|
||||
pub async fn shared(
|
||||
codex_home: PathBuf,
|
||||
|
||||
@@ -16,7 +16,7 @@ impl AuthRouteConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn route_config(&self) -> &OutboundProxyConfig {
|
||||
pub fn route_config(&self) -> &OutboundProxyConfig {
|
||||
&self.route_config
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user