mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
core: route Responses API through system proxy
This commit is contained in:
Generated
+1
@@ -2647,6 +2647,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-home",
|
||||
"codex-hooks",
|
||||
"codex-http-client",
|
||||
"codex-image-generation-extension",
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
|
||||
@@ -49,6 +49,7 @@ codex-shell-command = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-install-context = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
|
||||
@@ -62,10 +62,13 @@ use codex_api::auth_header_telemetry;
|
||||
use codex_api::build_session_headers;
|
||||
use codex_api::create_text_param_for_request;
|
||||
use codex_api::response_create_client_metadata;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::OutboundProxyConfig;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::RefreshTokenError;
|
||||
use codex_login::UnauthorizedRecovery;
|
||||
use codex_login::default_client::build_default_reqwest_client_for_route;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
@@ -248,6 +251,7 @@ pub struct ModelClient {
|
||||
state: Arc<ModelClientState>,
|
||||
agent_identity_policy: AgentIdentityAuthPolicy,
|
||||
prompt_cache_key_override: Option<String>,
|
||||
outbound_proxy_config: Option<OutboundProxyConfig>,
|
||||
}
|
||||
|
||||
/// A turn-scoped streaming session created from a [`ModelClient`].
|
||||
@@ -439,9 +443,18 @@ impl ModelClient {
|
||||
}),
|
||||
agent_identity_policy,
|
||||
prompt_cache_key_override: None,
|
||||
outbound_proxy_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_outbound_proxy_config(
|
||||
mut self,
|
||||
outbound_proxy_config: Option<OutboundProxyConfig>,
|
||||
) -> Self {
|
||||
self.outbound_proxy_config = outbound_proxy_config;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_prompt_cache_key_override(
|
||||
mut self,
|
||||
prompt_cache_key_override: Option<String>,
|
||||
@@ -532,7 +545,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_responses_transport(&client_setup.api_provider, RESPONSES_COMPACT_ENDPOINT)?;
|
||||
let request_telemetry = Self::build_request_telemetry(
|
||||
session_telemetry,
|
||||
AuthRequestTelemetryContext::new(
|
||||
@@ -938,6 +952,21 @@ impl ModelClient {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_responses_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.outbound_proxy_config.as_ref(),
|
||||
)
|
||||
.map_err(std::io::Error::from)?;
|
||||
Ok(ReqwestTransport::new(client))
|
||||
}
|
||||
|
||||
pub(crate) async fn prewarm_auth(&self) -> Result<()> {
|
||||
self.current_client_setup().await.map(|_| ())
|
||||
}
|
||||
@@ -1372,7 +1401,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_responses_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(),
|
||||
|
||||
@@ -3,6 +3,8 @@ use super::CompactConversationRequestSettings;
|
||||
use super::ModelClient;
|
||||
use super::PendingUnauthorizedRetry;
|
||||
use super::Prompt;
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::RESPONSES_ENDPOINT;
|
||||
use super::UnauthorizedRecoveryExecution;
|
||||
use super::X_CODEX_INSTALLATION_ID_HEADER;
|
||||
use super::X_CODEX_PARENT_THREAD_ID_HEADER;
|
||||
@@ -56,6 +58,8 @@ use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -81,6 +85,10 @@ use wiremock::matchers::path;
|
||||
|
||||
const TEST_CHATGPT_ID_TOKEN: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfdXNlcl9pZCI6InVzZXItMTIzNDUiLCJ1c2VyX2lkIjoidXNlci0xMjM0NSIsImNoYXRncHRfcGxhbl90eXBlIjoicHJvIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjb3VudC0xMjMifX0.c2ln";
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
#[cfg(target_os = "linux")]
|
||||
const RESPONSES_PROXY_TEST_CHILD_ENV: &str = "CODEX_RESPONSES_PROXY_TEST_CHILD";
|
||||
#[cfg(target_os = "linux")]
|
||||
const RESPONSES_PROXY_TEST_CHILD_RAN: &str = "responses proxy child ran";
|
||||
|
||||
fn test_model_client(session_source: SessionSource) -> ModelClient {
|
||||
test_model_client_with_thread_id(ThreadId::new(), session_source)
|
||||
@@ -107,6 +115,48 @@ fn test_model_client_with_thread_id(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn responses_transport_uses_enabled_outbound_proxy_policy() {
|
||||
if std::env::var_os(RESPONSES_PROXY_TEST_CHILD_ENV).is_none() {
|
||||
let test_name = "client::tests::responses_transport_uses_enabled_outbound_proxy_policy";
|
||||
let output = Command::new(std::env::current_exe().expect("current test executable"))
|
||||
.args(["--exact", test_name, "--nocapture"])
|
||||
.env(RESPONSES_PROXY_TEST_CHILD_ENV, "1")
|
||||
.env("HTTP_PROXY", "://invalid")
|
||||
.env_remove("http_proxy")
|
||||
.output()
|
||||
.expect("proxy-policy subprocess should run");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"proxy-policy subprocess failed\nstdout:\n{}\nstderr:\n{stderr}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(RESPONSES_PROXY_TEST_CHILD_RAN),
|
||||
"proxy-policy subprocess did not execute the expected test\nstderr:\n{stderr}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
eprintln!("{RESPONSES_PROXY_TEST_CHILD_RAN}");
|
||||
let api_provider =
|
||||
create_oss_provider_with_base_url("http://responses-api.invalid/v1", WireApi::Responses)
|
||||
.to_api_provider(/*auth_mode*/ None)
|
||||
.expect("test API provider should resolve");
|
||||
let client = test_model_client(SessionSource::Cli).with_outbound_proxy_config(Some(
|
||||
codex_http_client::OutboundProxyConfig::respect_system_proxy(),
|
||||
));
|
||||
|
||||
let error = match client.build_responses_transport(&api_provider, RESPONSES_ENDPOINT) {
|
||||
Err(codex_protocol::error::CodexErr::Io(error)) => error,
|
||||
Err(error) => panic!("expected an IO error, got {error:?}"),
|
||||
Ok(_) => panic!("expected invalid enabled proxy configuration"),
|
||||
};
|
||||
assert!(error.to_string().contains("selected for api"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -1590,6 +1590,10 @@ respect_system_proxy = true
|
||||
.await?;
|
||||
|
||||
assert!(config.respect_system_proxy);
|
||||
assert_eq!(
|
||||
config.outbound_proxy_config(),
|
||||
Some(codex_http_client::OutboundProxyConfig::respect_system_proxy())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ use codex_features::MultiAgentV2ConfigToml;
|
||||
use codex_features::NetworkProxyConfigToml;
|
||||
use codex_features::TokenBudgetConfigToml;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_http_client::OutboundProxyConfig;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_login::AuthRouteConfig;
|
||||
@@ -1481,6 +1482,11 @@ impl Config {
|
||||
.then(AuthRouteConfig::respect_system_proxy)
|
||||
}
|
||||
|
||||
pub(crate) fn outbound_proxy_config(&self) -> Option<OutboundProxyConfig> {
|
||||
self.respect_system_proxy
|
||||
.then(OutboundProxyConfig::respect_system_proxy)
|
||||
}
|
||||
|
||||
/// Build the plugin-manager input from the effective config.
|
||||
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
||||
PluginsConfigInput::new(
|
||||
|
||||
@@ -1113,6 +1113,7 @@ impl Session {
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
attestation_provider,
|
||||
)
|
||||
.with_outbound_proxy_config(config.outbound_proxy_config())
|
||||
.with_prompt_cache_key_override(
|
||||
crate::guardian::prompt_cache_key_override_for_review_session(
|
||||
&session_configuration.session_source,
|
||||
|
||||
@@ -8,8 +8,9 @@ use codex_http_client::BuildCustomCaTransportError;
|
||||
use codex_http_client::BuildRouteAwareHttpClientError;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::HttpClient;
|
||||
use codex_http_client::OutboundProxyConfig;
|
||||
pub use codex_http_client::RequestBuilder as CodexRequestBuilder;
|
||||
use codex_http_client::build_reqwest_client_for_route;
|
||||
use codex_http_client::build_reqwest_client_for_route as build_route_aware_reqwest_client;
|
||||
use codex_http_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_http_client::with_chatgpt_cloudflare_cookie_store;
|
||||
use codex_terminal_detection::user_agent;
|
||||
@@ -235,6 +236,28 @@ pub fn try_build_reqwest_client() -> Result<reqwest::Client, BuildCustomCaTransp
|
||||
build_reqwest_client_with_custom_ca(default_reqwest_client_builder())
|
||||
}
|
||||
|
||||
/// Builds the default Codex reqwest client for a concrete outbound route.
|
||||
///
|
||||
/// When route-aware proxy handling is disabled, or the client is running inside the Codex
|
||||
/// sandbox, this preserves the default client's existing proxy behavior. Otherwise it resolves
|
||||
/// the destination through the shared system/PAC-aware routing policy.
|
||||
pub fn build_default_reqwest_client_for_route(
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
route_config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
if route_config.is_none() || is_sandboxed() {
|
||||
return Ok(build_reqwest_client());
|
||||
}
|
||||
|
||||
build_route_aware_reqwest_client(
|
||||
default_reqwest_client_builder(),
|
||||
request_url,
|
||||
route_class,
|
||||
route_config,
|
||||
)
|
||||
}
|
||||
|
||||
fn default_reqwest_client_builder() -> reqwest::ClientBuilder {
|
||||
let mut builder = reqwest::Client::builder().default_headers(default_headers());
|
||||
if is_sandboxed() {
|
||||
@@ -248,7 +271,7 @@ pub(crate) fn build_raw_auth_reqwest_client(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
build_reqwest_client_for_route(
|
||||
build_route_aware_reqwest_client(
|
||||
reqwest::Client::builder(),
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
@@ -261,20 +284,10 @@ pub(crate) fn build_default_auth_reqwest_client(
|
||||
endpoint: &str,
|
||||
auth_route_config: Option<&AuthRouteConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let Some(route_config) = auth_route_config.map(AuthRouteConfig::route_config) else {
|
||||
return Ok(build_reqwest_client());
|
||||
};
|
||||
|
||||
if is_sandboxed() {
|
||||
// Preserve the sandbox's existing no-proxy policy; sandboxed command egress is routed
|
||||
// separately through network-proxy.
|
||||
return Ok(build_reqwest_client());
|
||||
}
|
||||
build_reqwest_client_for_route(
|
||||
default_reqwest_client_builder(),
|
||||
build_default_reqwest_client_for_route(
|
||||
endpoint,
|
||||
ClientRouteClass::Auth,
|
||||
Some(route_config),
|
||||
auth_route_config.map(AuthRouteConfig::route_config),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user