mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Extend PAC routing across Codex network clients
This commit is contained in:
@@ -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 { .. }
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user