Extend PAC routing across Codex network clients

This commit is contained in:
canvrno-oai
2026-06-15 11:09:13 -07:00
parent 8cd829a70a
commit a2e88abd08
26 changed files with 419 additions and 109 deletions
@@ -885,8 +885,12 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
let client = BackendClient::from_auth_with_route_config(
self.config.chatgpt_base_url.clone(),
&auth,
self.auth_manager.auth_route_config(),
)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
let profile = tokio::time::timeout(
ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT,
client.get_token_usage_profile(),
@@ -944,8 +948,12 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
let client = BackendClient::from_auth_with_route_config(
self.config.chatgpt_base_url.clone(),
&auth,
self.auth_manager.auth_route_config(),
)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
match client
.send_add_credits_nudge_email(Self::backend_credit_type(params.credit_type))
@@ -989,8 +997,12 @@ impl AccountRequestProcessor {
));
}
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
let client = BackendClient::from_auth_with_route_config(
self.config.chatgpt_base_url.clone(),
&auth,
self.auth_manager.auth_route_config(),
)
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
let snapshots = client
.get_rate_limits_many()
@@ -485,6 +485,13 @@ impl PluginRequestProcessor {
self.thread_manager.skills_manager().clear_cache();
}
fn remote_plugin_service_config(&self, config: &Config) -> RemotePluginServiceConfig {
RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
auth_route_config: self.auth_manager.auth_route_config().cloned(),
}
}
async fn load_latest_config(
&self,
fallback_cwd: Option<PathBuf>,
@@ -554,9 +561,7 @@ impl PluginRequestProcessor {
marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe);
let include_global_remote =
!explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin);
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let refresh_global_remote_catalog_cache = include_global_remote
&& codex_core_plugins::remote::has_cached_global_remote_plugin_catalog(
config.codex_home.as_path(),
@@ -990,9 +995,8 @@ impl PluginRequestProcessor {
let share_context = match share_context {
Some(context) => {
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config =
self.remote_plugin_service_config(&config);
match codex_core_plugins::remote::fetch_remote_plugin_share_context(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1096,9 +1100,7 @@ impl PluginRequestProcessor {
)));
}
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
validate_remote_plugin_id(&plugin_name)?;
let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail(
&remote_plugin_service_config,
@@ -1154,9 +1156,7 @@ impl PluginRequestProcessor {
}
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1207,9 +1207,7 @@ impl PluginRequestProcessor {
validate_client_plugin_share_targets(share_targets)?;
}
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let access_policy = codex_core_plugins::remote::RemotePluginShareAccessPolicy {
discoverability: discoverability.map(remote_plugin_share_discoverability),
share_targets: share_targets.map(remote_plugin_share_targets),
@@ -1250,9 +1248,7 @@ impl PluginRequestProcessor {
}
validate_client_plugin_share_targets(&share_targets)?;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let result = codex_core_plugins::remote::update_remote_plugin_share_targets(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1280,9 +1276,7 @@ impl PluginRequestProcessor {
_params: PluginShareListParams,
) -> Result<PluginShareListResponse, JSONRPCErrorError> {
let (config, auth) = self.load_plugin_share_config_and_auth().await?;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let data = codex_core_plugins::remote::list_remote_plugin_shares(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1319,9 +1313,7 @@ impl PluginRequestProcessor {
return Err(invalid_request("invalid remote plugin id"));
}
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let result = codex_core_plugins::remote::checkout_remote_plugin_share(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1352,9 +1344,7 @@ impl PluginRequestProcessor {
return Err(invalid_request("invalid remote plugin id"));
}
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
codex_core_plugins::remote::delete_remote_plugin_share(
&remote_plugin_service_config,
auth.as_ref(),
@@ -1472,9 +1462,7 @@ impl PluginRequestProcessor {
validate_remote_plugin_id(&remote_plugin_id)?;
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let remote_detail =
codex_core_plugins::remote::fetch_remote_plugin_detail_with_download_urls(
&remote_plugin_service_config,
@@ -1521,6 +1509,7 @@ impl PluginRequestProcessor {
let result = codex_core_plugins::remote_bundle::download_and_install_remote_plugin_bundle(
config.codex_home.to_path_buf(),
validated_bundle,
remote_plugin_service_config.auth_route_config.as_ref(),
)
.await
.map_err(remote_plugin_bundle_install_error_to_jsonrpc)?;
@@ -1859,9 +1848,7 @@ impl PluginRequestProcessor {
validate_remote_plugin_id(&plugin_id)?;
let auth = self.auth_manager.auth().await;
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
};
let remote_plugin_service_config = self.remote_plugin_service_config(&config);
let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin(
&remote_plugin_service_config,
auth.as_ref(),
@@ -2118,6 +2105,7 @@ fn remote_plugin_catalog_error_to_jsonrpc(
| RemotePluginCatalogError::ArchiveTooLarge { .. }
| RemotePluginCatalogError::UnknownMarketplace { .. } => invalid_request(message),
RemotePluginCatalogError::AuthToken(_)
| RemotePluginCatalogError::HttpClient { .. }
| RemotePluginCatalogError::Request { .. }
| RemotePluginCatalogError::UnexpectedStatus { .. }
| RemotePluginCatalogError::Decode { .. }
+18 -1
View File
@@ -13,6 +13,7 @@ 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::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_login::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
@@ -189,7 +190,23 @@ impl Client {
}
pub fn from_auth(base_url: impl Into<String>, auth: &CodexAuth) -> Result<Self> {
Ok(Self::new(base_url)?
Self::from_auth_with_route_config(base_url, auth, /*auth_route_config*/ None)
}
/// Creates an authenticated backend client using the configured outbound route policy.
pub fn from_auth_with_route_config(
base_url: impl Into<String>,
auth: &CodexAuth,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Self> {
let base_url = normalize_base_url(base_url.into());
let http = build_reqwest_client_for_route(
with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()),
&base_url,
ClientRouteClass::Api,
auth_route_config.and_then(AuthRouteConfig::route_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)))
}
+8 -3
View File
@@ -1,6 +1,7 @@
use codex_core::config::Config;
use codex_login::AuthManager;
use codex_login::default_client::create_client;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::create_client_for_route;
use anyhow::Context;
use serde::de::DeserializeOwned;
@@ -38,13 +39,17 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
"ChatGPT account ID not available, please re-run `codex login`"
);
// Make direct HTTP request to ChatGPT backend API with the token
let client = create_client();
let url = format!(
"{}/{}",
chatgpt_base_url.trim_end_matches('/'),
path.trim_start_matches('/')
);
let client = create_client_for_route(
&url,
ClientRouteClass::Api,
auth_manager.auth_route_config(),
)
.context("Failed to configure ChatGPT backend client")?;
let mut request = client
.get(&url)
+11 -6
View File
@@ -58,6 +58,7 @@ use codex_core_skills::config_rules::SkillConfigRules;
use codex_core_skills::config_rules::skill_config_rules_from_stack;
use codex_hooks::plugin_hook_declarations;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
@@ -89,6 +90,7 @@ pub struct PluginsConfigInput {
pub plugins_enabled: bool,
pub remote_plugin_enabled: bool,
pub chatgpt_base_url: String,
pub auth_route_config: Option<AuthRouteConfig>,
}
impl PluginsConfigInput {
@@ -97,12 +99,14 @@ impl PluginsConfigInput {
plugins_enabled: bool,
remote_plugin_enabled: bool,
chatgpt_base_url: String,
auth_route_config: Option<AuthRouteConfig>,
) -> Self {
Self {
config_layer_stack,
plugins_enabled,
remote_plugin_enabled,
chatgpt_base_url,
auth_route_config,
}
}
}
@@ -189,6 +193,7 @@ struct ConfiguredMarketplaceUpgradeState {
fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig {
RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
auth_route_config: config.auth_route_config.clone(),
}
}
@@ -1333,7 +1338,7 @@ impl PluginsManager {
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) {
if config.plugins_enabled {
self.start_curated_repo_sync();
self.start_curated_repo_sync(config.auth_route_config.clone());
let should_spawn_marketplace_auto_upgrade = {
let mut state = match self.configured_marketplace_upgrade_state.write() {
Ok(state) => state,
@@ -1630,7 +1635,7 @@ impl PluginsManager {
}
}
fn start_curated_repo_sync(self: &Arc<Self>) {
fn start_curated_repo_sync(self: &Arc<Self>, auth_route_config: Option<AuthRouteConfig>) {
if CURATED_REPO_SYNC_STARTED.swap(true, Ordering::SeqCst) {
return;
}
@@ -1638,8 +1643,8 @@ impl PluginsManager {
let codex_home = self.codex_home.clone();
if let Err(err) = std::thread::Builder::new()
.name("plugins-curated-repo-sync".to_string())
.spawn(
move || match sync_openai_plugins_repo(codex_home.as_path()) {
.spawn(move || {
match sync_openai_plugins_repo(codex_home.as_path(), auth_route_config.as_ref()) {
Ok(curated_plugin_version) => {
let configured_curated_plugin_ids =
configured_curated_plugin_ids_from_codex_home(codex_home.as_path());
@@ -1664,8 +1669,8 @@ impl PluginsManager {
CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);
warn!("failed to sync curated plugins repo: {err}");
}
},
)
}
})
{
CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);
warn!("failed to start curated plugins repo sync task: {err}");
@@ -1412,6 +1412,7 @@ async fn plugin_cache_ignores_unrelated_session_overrides() {
/*plugins_enabled*/ true,
/*remote_plugin_enabled*/ false,
"https://chatgpt.com".to_string(),
/*auth_route_config*/ None,
)
};
let manager = PluginsManager::new(codex_home.path().to_path_buf());
+41 -8
View File
@@ -6,8 +6,10 @@ use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::SkillInterface;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_plugin::PluginId;
use codex_utils_absolute_path::AbsolutePathBuf;
use reqwest::RequestBuilder;
@@ -97,6 +99,30 @@ const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 5] = [
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemotePluginServiceConfig {
pub chatgpt_base_url: String,
pub auth_route_config: Option<AuthRouteConfig>,
}
impl RemotePluginServiceConfig {
pub(crate) fn build_http_client(&self, url: &str) -> anyhow::Result<reqwest::Client> {
build_default_reqwest_client_for_route(
url,
ClientRouteClass::Api,
self.auth_route_config.as_ref(),
)
.map_err(Into::into)
}
}
fn build_remote_plugin_http_client(
config: &RemotePluginServiceConfig,
url: &str,
) -> Result<reqwest::Client, RemotePluginCatalogError> {
config
.build_http_client(url)
.map_err(|source| RemotePluginCatalogError::HttpClient {
url: url.to_string(),
source,
})
}
#[derive(Debug, Clone, PartialEq)]
@@ -253,6 +279,13 @@ pub enum RemotePluginCatalogError {
#[error("failed to read auth token for remote plugin catalog: {0}")]
AuthToken(#[source] std::io::Error),
#[error("failed to configure remote plugin catalog client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote plugin catalog request to {url}: {source}")]
Request {
url: String,
@@ -959,7 +992,7 @@ pub async fn fetch_remote_plugin_skill_detail(
}
let url = remote_plugin_skill_detail_url(config, plugin_id, skill_name)?;
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.get(&url), auth)?;
let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?;
if response.plugin_id != plugin_id {
@@ -1086,7 +1119,7 @@ pub async fn install_remote_plugin(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}/install");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(
client
.post(&url)
@@ -1129,7 +1162,7 @@ pub async fn uninstall_remote_plugin(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?;
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
if response.id != plugin_id {
@@ -1535,7 +1568,7 @@ async fn get_remote_plugin_list_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/list");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("scope", scope.api_value())]);
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
@@ -1555,7 +1588,7 @@ async fn get_remote_shared_workspace_plugins_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/workspace/shared");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
if let Some(page_token) = page_token {
@@ -1573,7 +1606,7 @@ async fn get_remote_plugin_installed_page(
) -> Result<RemotePluginInstalledResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/installed");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("scope", scope.api_value())]);
if include_download_urls {
@@ -1593,7 +1626,7 @@ async fn fetch_plugin_detail(
) -> Result<RemotePluginDirectoryItem, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
if include_download_urls {
request = request.query(&[("includeDownloadUrls", true)]);
@@ -227,6 +227,7 @@ pub async fn sync_remote_installed_plugin_bundles_once(
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
codex_home.clone(),
bundle,
config.auth_route_config.as_ref(),
)
.await
{
+13 -8
View File
@@ -2,7 +2,6 @@ use super::*;
use crate::plugin_bundle_archive::PluginBundlePackError;
use crate::plugin_bundle_archive::pack_plugin_bundle_tar_gz;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_utils_absolute_path::AbsolutePathBuf;
use reqwest::RequestBuilder;
use reqwest::StatusCode;
@@ -163,7 +162,7 @@ pub async fn save_remote_plugin_share(
let etag = upload
.etag
.ok_or(RemotePluginCatalogError::MissingUploadEtag)?;
put_workspace_plugin_upload(&upload.upload_url, archive_bytes).await?;
put_workspace_plugin_upload(config, &upload.upload_url, archive_bytes).await?;
let share_targets = access_policy.share_targets;
let share_targets =
ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?;
@@ -279,7 +278,7 @@ pub async fn delete_remote_plugin_share(
let auth = ensure_chatgpt_auth(auth)?;
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.delete(&url), auth)?;
send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?;
if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) {
@@ -312,7 +311,7 @@ pub async fn update_remote_plugin_share_targets(
.unwrap_or_default();
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.put(&url), auth)?.json(
&RemotePluginShareUpdateTargetsRequest {
discoverability,
@@ -378,7 +377,7 @@ async fn get_created_workspace_plugins_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/workspace/created");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
if let Some(page_token) = page_token {
@@ -396,7 +395,7 @@ async fn create_workspace_plugin_upload(
) -> Result<RemoteWorkspacePluginUploadUrlResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/public/plugins/workspace/upload-url");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?.json(
&RemoteWorkspacePluginUploadUrlRequest {
filename,
@@ -409,10 +408,16 @@ async fn create_workspace_plugin_upload(
}
async fn put_workspace_plugin_upload(
config: &RemotePluginServiceConfig,
upload_url: &str,
archive_bytes: Vec<u8>,
) -> Result<(), RemotePluginCatalogError> {
let client = build_reqwest_client();
let client = config.build_http_client(upload_url).map_err(|source| {
RemotePluginCatalogError::HttpClient {
url: "workspace plugin upload URL".to_string(),
source,
}
})?;
let request = client
.put(upload_url)
.timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT)
@@ -450,7 +455,7 @@ async fn finalize_workspace_plugin_upload(
} else {
format!("{base_url}/public/plugins/workspace")
};
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?.json(&body);
send_and_decode(request, &url).await
}
@@ -95,6 +95,7 @@ pub async fn checkout_remote_plugin_share(
crate::remote_bundle::download_and_extract_remote_plugin_bundle_to_path(
bundle,
local_plugin_path.clone(),
config.auth_route_config.as_ref(),
)
.await
.map_err(|err| {
@@ -25,6 +25,7 @@ use wiremock::matchers::query_param_is_missing;
fn test_config(server: &MockServer) -> RemotePluginServiceConfig {
RemotePluginServiceConfig {
chatgpt_base_url: format!("{}/backend-api", server.uri()),
auth_route_config: None,
}
}
+24 -2
View File
@@ -5,7 +5,9 @@ use crate::store::PluginInstallResult;
use crate::store::PluginStore;
use crate::store::PluginStoreError;
use crate::store::validate_plugin_version_segment;
use codex_login::default_client::build_reqwest_client;
use codex_login::AuthRouteConfig;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_plugin::PluginId;
use codex_plugin::PluginIdError;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -88,6 +90,13 @@ pub enum RemotePluginBundleInstallError {
source: reqwest::Error,
},
#[error("failed to configure remote plugin bundle client for {url}: {source}")]
DownloadClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("remote plugin bundle download from {url} failed with status {status}: {body}")]
DownloadStatus {
url: String,
@@ -226,10 +235,12 @@ fn is_loopback_url(url: &Url) -> bool {
pub async fn download_and_install_remote_plugin_bundle(
codex_home: PathBuf,
bundle: ValidatedRemotePluginBundle,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<PluginInstallResult, RemotePluginBundleInstallError> {
let bundle_bytes = download_remote_plugin_bundle_with_limit(
&bundle.bundle_download_url,
/*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,
auth_route_config,
)
.await?;
tokio::task::spawn_blocking(move || {
@@ -246,10 +257,12 @@ pub async fn download_and_install_remote_plugin_bundle(
pub(crate) async fn download_and_extract_remote_plugin_bundle_to_path(
bundle: ValidatedRemotePluginBundle,
destination: AbsolutePathBuf,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<AbsolutePathBuf, RemotePluginBundleInstallError> {
let bundle_bytes = download_remote_plugin_bundle_with_limit(
&bundle.bundle_download_url,
/*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,
auth_route_config,
)
.await?;
tokio::task::spawn_blocking(move || {
@@ -266,8 +279,17 @@ pub(crate) async fn download_and_extract_remote_plugin_bundle_to_path(
async fn download_remote_plugin_bundle_with_limit(
bundle_download_url: &str,
max_bytes: u64,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, RemotePluginBundleInstallError> {
let client = build_reqwest_client();
let client = build_default_reqwest_client_for_route(
bundle_download_url,
ClientRouteClass::Api,
auth_route_config,
)
.map_err(|source| RemotePluginBundleInstallError::DownloadClient {
url: bundle_download_url.to_string(),
source: source.into(),
})?;
let response = client
.get(bundle_download_url)
.timeout(REMOTE_PLUGIN_BUNDLE_DOWNLOAD_TIMEOUT)
+28 -3
View File
@@ -1,6 +1,5 @@
use crate::remote::RemotePluginServiceConfig;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_protocol::protocol::Product;
use serde::Deserialize;
use std::time::Duration;
@@ -35,6 +34,13 @@ pub enum RemotePluginMutationError {
#[error("chatgpt base url cannot be used for plugin mutation")]
InvalidBaseUrlPath,
#[error("failed to configure remote plugin mutation client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote plugin mutation request to {url}: {source}")]
Request {
url: String,
@@ -73,6 +79,13 @@ pub enum RemotePluginMutationError {
#[derive(Debug, thiserror::Error)]
pub enum RemotePluginFetchError {
#[error("failed to configure remote featured plugin client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote featured plugin request to {url}: {source}")]
Request {
url: String,
@@ -102,7 +115,13 @@ pub async fn fetch_remote_featured_plugin_ids(
) -> Result<Vec<String>, RemotePluginFetchError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/plugins/featured");
let client = build_reqwest_client();
let client =
config
.build_http_client(&url)
.map_err(|source| RemotePluginFetchError::HttpClient {
url: url.clone(),
source,
})?;
let mut request = client
.get(&url)
.query(&[(
@@ -173,7 +192,13 @@ async fn post_remote_plugin_mutation(
) -> Result<RemotePluginMutationResponse, RemotePluginMutationError> {
let auth = ensure_codex_backend_auth(auth)?;
let url = remote_plugin_mutation_url(config, plugin_id, action)?;
let client = build_reqwest_client();
let client =
config
.build_http_client(&url)
.map_err(|source| RemotePluginMutationError::HttpClient {
url: url.clone(),
source,
})?;
let request = client
.post(url.clone())
.timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT)
+59 -18
View File
@@ -6,6 +6,9 @@ use std::process::Output;
use std::process::Stdio;
use std::time::Duration;
use codex_login::AuthRouteConfig;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC;
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_METRIC;
use reqwest::Client;
@@ -14,8 +17,6 @@ use tempfile::TempDir;
use tracing::warn;
use zip::ZipArchive;
use codex_login::default_client::build_reqwest_client;
const GITHUB_API_BASE_URL: &str = "https://api.github.com";
const GITHUB_API_ACCEPT_HEADER: &str = "application/vnd.github+json";
const GITHUB_API_VERSION_HEADER: &str = "2022-11-28";
@@ -67,12 +68,16 @@ fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf {
codex_home.join(CURATED_PLUGINS_SHA_FILE)
}
pub fn sync_openai_plugins_repo(codex_home: &Path) -> Result<String, String> {
pub fn sync_openai_plugins_repo(
codex_home: &Path,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
sync_openai_plugins_repo_with_transport_overrides(
codex_home,
"git",
GITHUB_API_BASE_URL,
CURATED_PLUGINS_BACKUP_ARCHIVE_API_URL,
auth_route_config,
)
}
@@ -81,23 +86,33 @@ fn sync_openai_plugins_repo_with_transport_overrides(
git_binary: &str,
api_base_url: &str,
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let _file_guard = lock_curated_plugins_startup_sync(codex_home)?;
match sync_openai_plugins_repo_via_git(codex_home, git_binary) {
let route_aware_http =
auth_route_config.is_some_and(AuthRouteConfig::requires_route_aware_transport);
let git_result = if route_aware_http {
Err("route-aware plugin sync requires the HTTP transport".to_string())
} else {
sync_openai_plugins_repo_via_git(codex_home, git_binary)
};
match git_result {
Ok(remote_sha) => {
emit_curated_plugins_startup_sync_metric("git", "success");
emit_curated_plugins_startup_sync_final_metric("git", "success");
Ok(remote_sha)
}
Err(err) => {
emit_curated_plugins_startup_sync_metric("git", "failure");
warn!(
error = %err,
git_binary,
"git sync failed for curated plugin sync; falling back to GitHub HTTP"
);
match sync_openai_plugins_repo_via_http(codex_home, api_base_url) {
if !route_aware_http {
emit_curated_plugins_startup_sync_metric("git", "failure");
warn!(
error = %err,
git_binary,
"git sync failed for curated plugin sync; falling back to GitHub HTTP"
);
}
match sync_openai_plugins_repo_via_http(codex_home, api_base_url, auth_route_config) {
Ok(remote_sha) => {
emit_curated_plugins_startup_sync_metric("http", "success");
emit_curated_plugins_startup_sync_final_metric("http", "success");
@@ -125,6 +140,7 @@ fn sync_openai_plugins_repo_with_transport_overrides(
let result = sync_openai_plugins_repo_via_backup_archive(
codex_home,
backup_archive_api_url,
auth_route_config,
);
let status = if result.is_ok() { "success" } else { "failure" };
emit_curated_plugins_startup_sync_metric("export_archive", status);
@@ -288,6 +304,7 @@ fn run_git_in_repo(
fn sync_openai_plugins_repo_via_http(
codex_home: &Path,
api_base_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let repo_path = curated_plugins_repo_path(codex_home);
let sha_path = codex_home.join(CURATED_PLUGINS_SHA_FILE);
@@ -295,7 +312,10 @@ fn sync_openai_plugins_repo_via_http(
.enable_all()
.build()
.map_err(|err| format!("failed to create curated plugins sync runtime: {err}"))?;
let remote_sha = runtime.block_on(fetch_curated_repo_remote_sha(api_base_url))?;
let remote_sha = runtime.block_on(fetch_curated_repo_remote_sha(
api_base_url,
auth_route_config,
))?;
let local_sha = read_sha_file(&sha_path);
if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.is_dir() {
@@ -303,7 +323,11 @@ fn sync_openai_plugins_repo_via_http(
}
let staged_repo_dir = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(api_base_url, &remote_sha))?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(
api_base_url,
&remote_sha,
auth_route_config,
))?;
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
activate_curated_repo(&repo_path, staged_repo_dir)?;
@@ -314,6 +338,7 @@ fn sync_openai_plugins_repo_via_http(
fn sync_openai_plugins_repo_via_backup_archive(
codex_home: &Path,
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let repo_path = curated_plugins_repo_path(codex_home);
let sha_path = curated_plugins_sha_path(codex_home);
@@ -324,6 +349,7 @@ fn sync_openai_plugins_repo_via_backup_archive(
let staged_repo_dir = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_backup_archive_zip(
backup_archive_api_url,
auth_route_config,
))?;
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
@@ -700,10 +726,13 @@ fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> {
}
}
async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result<String, String> {
async fn fetch_curated_repo_remote_sha(
api_base_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let api_base_url = api_base_url.trim_end_matches('/');
let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}");
let client = build_reqwest_client();
let client = build_startup_sync_client(&repo_url, auth_route_config)?;
let repo_body = fetch_github_text(&client, &repo_url, "get curated plugins repository").await?;
let repo_summary: GitHubRepositorySummary =
serde_json::from_str(&repo_body).map_err(|err| {
@@ -733,18 +762,20 @@ async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result<String, Str
async fn fetch_curated_repo_zipball(
api_base_url: &str,
remote_sha: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, String> {
let api_base_url = api_base_url.trim_end_matches('/');
let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}");
let zipball_url = format!("{repo_url}/zipball/{remote_sha}");
let client = build_reqwest_client();
let client = build_startup_sync_client(&zipball_url, auth_route_config)?;
fetch_github_bytes(&client, &zipball_url, "download curated plugins archive").await
}
async fn fetch_curated_repo_backup_archive_zip(
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, String> {
let client = build_reqwest_client();
let client = build_startup_sync_client(backup_archive_api_url, auth_route_config)?;
let export_body = fetch_public_text(
&client,
backup_archive_api_url,
@@ -763,14 +794,24 @@ async fn fetch_curated_repo_backup_archive_zip(
));
}
let download_client =
build_startup_sync_client(&export_response.download_url, auth_route_config)?;
fetch_public_bytes(
&client,
&download_client,
&export_response.download_url,
"download curated plugins export archive",
)
.await
}
fn build_startup_sync_client(
url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Client, String> {
build_default_reqwest_client_for_route(url, ClientRouteClass::Api, auth_route_config)
.map_err(|err| format!("failed to configure curated plugins HTTP client: {err}"))
}
fn read_extracted_backup_archive_git_sha(repo_path: &Path) -> Result<Option<String>, String> {
let git_dir = repo_path.join(".git");
if !git_dir.is_dir() {
@@ -178,6 +178,7 @@ async fn run_sync_with_transport_overrides(
&git_binary,
&api_base_url,
&backup_archive_api_url,
/*auth_route_config*/ None,
)
})
.await
@@ -190,7 +191,11 @@ async fn run_http_sync(
) -> Result<String, String> {
let api_base_url = api_base_url.into();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_via_http(codex_home.as_path(), &api_base_url)
sync_openai_plugins_repo_via_http(
codex_home.as_path(),
&api_base_url,
/*auth_route_config*/ None,
)
})
.await
.expect("sync task should join")
@@ -329,6 +334,7 @@ exit 1
git_path.to_str().expect("utf8 path"),
"http://127.0.0.1:9",
"http://127.0.0.1:9/backend-api/plugins/export/curated",
/*auth_route_config*/ None,
)
};
let first = scope.spawn(run_sync);
@@ -115,6 +115,7 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin
/*default_enabled*/ false,
),
"https://chatgpt.com/backend-api/".to_string(),
/*auth_route_config*/ None,
)
}
+37 -5
View File
@@ -63,10 +63,12 @@ use codex_api::create_text_param_for_request;
use codex_api::response_create_client_metadata;
use codex_app_server_protocol::AuthMode;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_login::RefreshTokenError;
use codex_login::UnauthorizedRecovery;
use codex_login::default_client::build_reqwest_client;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_otel::SessionTelemetry;
use codex_otel::current_span_w3c_trace_context;
@@ -171,6 +173,7 @@ pub(crate) struct CompactConversationRequestSettings {
struct ModelClientState {
thread_id: ThreadId,
provider: SharedModelProvider,
auth_route_config: Option<AuthRouteConfig>,
auth_env_telemetry: AuthEnvTelemetry,
session_source: SessionSource,
model_verbosity: Option<VerbosityConfig>,
@@ -325,6 +328,10 @@ impl ModelClient {
beta_features_header: Option<String>,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
) -> Self {
let auth_route_config = auth_manager
.as_ref()
.and_then(|manager| manager.auth_route_config())
.cloned();
let model_provider = create_model_provider(provider_info, auth_manager);
let codex_api_key_env_enabled = model_provider
.auth_manager()
@@ -337,6 +344,7 @@ impl ModelClient {
state: Arc::new(ModelClientState {
thread_id,
provider: model_provider,
auth_route_config,
auth_env_telemetry,
session_source,
model_verbosity,
@@ -441,7 +449,8 @@ impl ModelClient {
return Ok(Vec::new());
}
let client_setup = self.current_client_setup().await?;
let transport = ReqwestTransport::new(build_reqwest_client());
let transport =
self.build_http_transport(&client_setup.api_provider, RESPONSES_COMPACT_ENDPOINT)?;
let request_telemetry = Self::build_request_telemetry(
session_telemetry,
AuthRequestTelemetryContext::new(
@@ -534,7 +543,7 @@ impl ModelClient {
sideband_headers.extend(sideband_websocket_auth_headers(
client_setup.api_auth.as_ref(),
));
let transport = ReqwestTransport::new(build_reqwest_client());
let transport = self.build_http_transport(&client_setup.api_provider, "/realtime/calls")?;
let response =
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
.create_with_session_and_headers(sdp, session_config, extra_headers)
@@ -565,7 +574,8 @@ impl ModelClient {
}
let client_setup = self.current_client_setup().await?;
let transport = ReqwestTransport::new(build_reqwest_client());
let transport =
self.build_http_transport(&client_setup.api_provider, MEMORIES_SUMMARIZE_ENDPOINT)?;
let request_telemetry = Self::build_request_telemetry(
session_telemetry,
AuthRequestTelemetryContext::new(
@@ -765,6 +775,11 @@ impl ModelClient {
pub fn responses_websocket_enabled(&self) -> bool {
if !self.state.provider.info().supports_websockets
|| self.state.disable_websockets.load(Ordering::Relaxed)
|| self
.state
.auth_route_config
.as_ref()
.is_some_and(|config| config.requires_route_aware_transport())
{
return false;
}
@@ -772,6 +787,21 @@ impl ModelClient {
true
}
fn build_http_transport(
&self,
api_provider: &ApiProvider,
endpoint: &str,
) -> Result<ReqwestTransport> {
let request_url = api_provider.url_for_path(endpoint);
let client = build_default_reqwest_client_for_route(
&request_url,
ClientRouteClass::Api,
self.state.auth_route_config.as_ref(),
)
.map_err(std::io::Error::from)?;
Ok(ReqwestTransport::new(client))
}
/// Returns auth + provider configuration resolved from the current session auth state.
///
/// This centralizes setup used by both prewarm and normal request paths so they stay in
@@ -1212,7 +1242,9 @@ impl ModelClientSession {
let mut pending_retry = PendingUnauthorizedRetry::default();
loop {
let client_setup = self.client.current_client_setup().await?;
let transport = ReqwestTransport::new(build_reqwest_client());
let transport = self
.client
.build_http_transport(&client_setup.api_provider, RESPONSES_ENDPOINT)?;
let request_auth_context = AuthRequestTelemetryContext::new(
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
client_setup.api_auth.as_ref(),
+30
View File
@@ -16,7 +16,9 @@ use crate::test_support::responses_metadata as test_responses_metadata;
use codex_api::ApiError;
use codex_api::ResponseEvent;
use codex_app_server_protocol::AuthMode;
use codex_login::AuthCredentialsStoreMode;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_model_provider::BearerAuthProvider;
use codex_model_provider_info::CHATGPT_CODEX_BASE_URL;
@@ -80,6 +82,34 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
)
}
#[tokio::test]
async fn route_aware_model_client_uses_http_instead_of_direct_websocket() {
let codex_home = TempDir::new().expect("temporary Codex home");
let auth_manager = Arc::new(
AuthManager::new_with_auth_route_config(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
Some(AuthRouteConfig::system()),
)
.await,
);
let model_client = ModelClient::new(
Some(auth_manager),
ThreadId::new(),
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
SessionSource::Exec,
/*model_verbosity*/ None,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
/*beta_features_header*/ None,
/*attestation_provider*/ None,
);
assert!(!model_client.responses_websocket_enabled());
}
fn test_responses_metadata_for_client(
client: &ModelClient,
turn_id: Option<&str>,
+3
View File
@@ -1397,6 +1397,9 @@ impl Config {
self.features.enabled(Feature::Plugins),
self.features.enabled(Feature::RemotePlugin),
self.chatgpt_base_url.clone(),
self.system_proxy
.as_ref()
.map(auth_route_config_from_system_proxy_config),
)
}
@@ -324,6 +324,7 @@ remote_plugin = true
codex_home.path(),
&RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
auth_route_config: None,
},
Some(&auth),
)
+21 -8
View File
@@ -6,7 +6,7 @@
use codex_client::BuildCustomCaTransportError;
use codex_client::BuildRouteAwareHttpClientError;
use codex_client::ClientRouteClass;
pub use codex_client::ClientRouteClass;
use codex_client::CodexHttpClient;
pub use codex_client::CodexRequestBuilder;
use codex_client::build_reqwest_client_for_route;
@@ -247,6 +247,15 @@ pub(crate) fn build_auth_reqwest_client_with_auth_route_config(
pub(crate) fn build_default_reqwest_client_for_auth_route(
endpoint: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
build_default_reqwest_client_for_route(endpoint, ClientRouteClass::Auth, auth_route_config)
}
/// Builds the standard Codex reqwest client for a known destination.
pub fn build_default_reqwest_client_for_route(
endpoint: &str,
route_class: ClientRouteClass,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
let Some(route_config) = auth_route_config.and_then(AuthRouteConfig::route_config) else {
return Ok(build_reqwest_client());
@@ -256,17 +265,21 @@ pub(crate) fn build_default_reqwest_client_for_auth_route(
if is_sandboxed() {
return build_reqwest_client_with_custom_ca(builder).map_err(Into::into);
}
build_reqwest_client_for_route(
builder,
endpoint,
ClientRouteClass::Auth,
Some(route_config),
)
build_reqwest_client_for_route(builder, endpoint, route_class, Some(route_config))
}
pub(crate) fn create_client_for_auth_route(
endpoint: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<CodexHttpClient, BuildRouteAwareHttpClientError> {
create_client_for_route(endpoint, ClientRouteClass::Auth, auth_route_config)
}
/// Creates the standard traced Codex HTTP client for a known destination.
pub fn create_client_for_route(
endpoint: &str,
route_class: ClientRouteClass,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<CodexHttpClient, BuildRouteAwareHttpClientError> {
if auth_route_config
.and_then(AuthRouteConfig::route_config)
@@ -275,7 +288,7 @@ pub(crate) fn create_client_for_auth_route(
return Ok(create_client());
}
build_default_reqwest_client_for_auth_route(endpoint, auth_route_config)
build_default_reqwest_client_for_route(endpoint, route_class, auth_route_config)
.map(CodexHttpClient::new)
}
@@ -117,12 +117,40 @@ fn auth_route_client_without_proxy_config_preserves_default_client_fallback() {
);
assert!(raw_no_proxy_client.is_ok());
let api_no_proxy_client = build_default_reqwest_client_for_route(
"https://api.openai.com/v1/models",
ClientRouteClass::Api,
/*auth_route_config*/ None,
);
assert!(api_no_proxy_client.is_ok());
let traced_api_no_proxy_client = create_client_for_route(
"https://api.openai.com/v1/models",
ClientRouteClass::Api,
/*auth_route_config*/ None,
);
assert!(traced_api_no_proxy_client.is_ok());
let auth_route_config = AuthRouteConfig::direct();
let explicit_proxy_client = create_client_for_auth_route(
"https://auth.openai.com/oauth/token",
Some(&auth_route_config),
);
assert!(explicit_proxy_client.is_err());
let explicit_api_client = build_default_reqwest_client_for_route(
"https://api.openai.com/v1/models",
ClientRouteClass::Api,
Some(&auth_route_config),
);
assert!(explicit_api_client.is_err());
let traced_explicit_api_client = create_client_for_route(
"https://api.openai.com/v1/models",
ClientRouteClass::Api,
Some(&auth_route_config),
);
assert!(traced_explicit_api_client.is_err());
}
#[test]
+15 -2
View File
@@ -3,9 +3,9 @@ use codex_client::OutboundProxyMode;
use codex_config::types::SystemProxyFeatureConfigToml;
use codex_config::types::SystemProxyFeatureModeToml;
/// Stable route-selection policy for auth-owned clients.
/// Stable route-selection policy for Codex-owned clients.
///
/// Auth call sites should accept this type instead of lower-level resolver
/// Call sites should accept this type instead of lower-level resolver
/// details. Config parsing decides whether to construct one, and the client
/// layer remains responsible for platform-specific proxy resolution.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
@@ -35,6 +35,13 @@ impl AuthRouteConfig {
self.route_config.as_ref()
}
/// Returns whether clients need a proxy-aware transport rather than a direct socket.
pub fn requires_route_aware_transport(&self) -> bool {
self.route_config
.as_ref()
.is_some_and(|config| config.mode() != OutboundProxyMode::Direct)
}
fn from_outbound_proxy_mode(mode: OutboundProxyMode) -> Self {
Self {
route_config: Some(OutboundProxyConfig::new(mode)),
@@ -102,5 +109,11 @@ mod tests {
Some(expected_config)
);
}
assert!(AuthRouteConfig::auto().requires_route_aware_transport());
assert!(AuthRouteConfig::env().requires_route_aware_transport());
assert!(AuthRouteConfig::system().requires_route_aware_transport());
assert!(!AuthRouteConfig::direct().requires_route_aware_transport());
assert!(!AuthRouteConfig::default().requires_route_aware_transport());
}
}
+7 -3
View File
@@ -18,9 +18,13 @@ async fn rate_limits_check(auth_manager: &AuthManager, config: &Config) -> Optio
return None;
}
let client = BackendClient::from_auth(config.chatgpt_base_url.clone(), &auth)
.map_err(|err| warn!(%err, "failed to construct backend client"))
.ok()?;
let client = BackendClient::from_auth_with_route_config(
config.chatgpt_base_url.clone(),
&auth,
auth_manager.auth_route_config(),
)
.map_err(|err| warn!(%err, "failed to construct backend client"))
.ok()?;
let snapshots = client
.get_rate_limits_many()
+16 -2
View File
@@ -12,9 +12,11 @@ use codex_feedback::FeedbackRequestTags;
use codex_feedback::emit_feedback_request_tags_with_auth_env;
use codex_login::AuthEnvTelemetry;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_login::collect_auth_env_telemetry;
use codex_login::default_client::build_reqwest_client;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::manager::ModelsEndpointClient;
use codex_otel::TelemetryAuthMode;
@@ -36,16 +38,19 @@ const MODELS_ENDPOINT: &str = "/models";
pub(crate) struct OpenAiModelsEndpoint {
provider_info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
auth_route_config: Option<AuthRouteConfig>,
}
impl OpenAiModelsEndpoint {
pub(crate) fn new(
provider_info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
auth_route_config: Option<AuthRouteConfig>,
) -> Self {
Self {
provider_info,
auth_manager,
auth_route_config,
}
}
@@ -88,7 +93,14 @@ impl ModelsEndpointClient for OpenAiModelsEndpoint {
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
let api_provider = self.provider_info.to_api_provider(auth_mode)?;
let api_auth = resolve_provider_auth(auth.as_ref(), &self.provider_info)?;
let transport = ReqwestTransport::new(build_reqwest_client());
let request_url = api_provider.url_for_path(MODELS_ENDPOINT);
let http = build_default_reqwest_client_for_route(
&request_url,
ClientRouteClass::Api,
self.auth_route_config.as_ref(),
)
.map_err(std::io::Error::from)?;
let transport = ReqwestTransport::new(http);
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()),
@@ -230,6 +242,7 @@ mod tests {
let endpoint = OpenAiModelsEndpoint::new(
provider_info_with_command_auth(),
/*auth_manager*/ None,
/*auth_route_config*/ None,
);
assert!(endpoint.has_command_auth());
@@ -240,6 +253,7 @@ mod tests {
let endpoint = OpenAiModelsEndpoint::new(
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
/*auth_manager*/ None,
/*auth_route_config*/ None,
);
assert!(!endpoint.has_command_auth());
+8
View File
@@ -5,6 +5,7 @@ use std::sync::Arc;
use codex_api::Provider;
use codex_api::SharedAuthProvider;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::manager::OpenAiModelsManager;
@@ -190,14 +191,20 @@ pub fn create_model_provider(
struct ConfiguredModelProvider {
info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
auth_route_config: Option<AuthRouteConfig>,
}
impl ConfiguredModelProvider {
fn new(provider_info: ModelProviderInfo, auth_manager: Option<Arc<AuthManager>>) -> Self {
let auth_route_config = auth_manager
.as_ref()
.and_then(|manager| manager.auth_route_config())
.cloned();
let auth_manager = auth_manager_for_provider(auth_manager, &provider_info);
Self {
info: provider_info,
auth_manager,
auth_route_config,
}
}
}
@@ -282,6 +289,7 @@ impl ModelProvider for ConfiguredModelProvider {
let endpoint = Arc::new(OpenAiModelsEndpoint::new(
self.info.clone(),
self.auth_manager.clone(),
self.auth_route_config.clone(),
));
Arc::new(OpenAiModelsManager::new(
codex_home,