core: route Responses API through system proxy

This commit is contained in:
Michael Bolin
2026-07-06 18:49:49 -07:00
parent 641aa1b619
commit d16339257e
9 changed files with 191 additions and 16 deletions
+1
View File
@@ -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 }
+33 -2
View File
@@ -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(),
+4
View File
@@ -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(())
}
+6
View File
@@ -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(
+1
View File
@@ -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,
+1
View File
@@ -99,6 +99,7 @@ mod request_permissions_tool;
mod request_plugin_install;
mod request_user_input;
mod responses_api_proxy_headers;
mod responses_api_system_proxy;
mod responses_lite;
mod resume;
mod resume_warning;
@@ -0,0 +1,117 @@
//! Verifies that the effective system-proxy feature reaches both HTTP Responses API paths.
use anyhow::Result;
use codex_config::CONFIG_TOML_FILE;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_compact_json_once;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use std::process::Command;
const SUBPROCESS_ENV: &str = "CODEX_RESPONSES_SYSTEM_PROXY_TEST_CHILD";
const SUBPROCESS_SENTINEL: &str = "responses system proxy child ran";
const TEST_NAME: &str =
"suite::responses_api_system_proxy::responses_and_compact_use_enabled_system_proxy";
const TARGET_BASE_URL: &str = "http://responses-api.invalid/v1";
#[cfg_attr(
not(target_os = "linux"),
ignore = "system proxy environment fallback is Linux-specific"
)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_and_compact_use_enabled_system_proxy() -> Result<()> {
if std::env::var_os(SUBPROCESS_ENV).is_none() {
return run_test_in_subprocess().await;
}
eprintln!("{SUBPROCESS_SENTINEL}");
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex()
.with_pre_build_hook(|codex_home| {
std::fs::write(
codex_home.join(CONFIG_TOML_FILE),
"[features]\nrespect_system_proxy = true\n",
)
.expect("system-proxy feature config should be written");
})
.with_config(|config| {
config.model_provider.base_url = Some(TARGET_BASE_URL.to_string());
});
let test = builder.build_with_auto_env(&server).await?;
assert!(test.config.respect_system_proxy);
test.submit_turn("exercise the Responses API proxy route")
.await?;
test.codex.submit(Op::Compact).await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
Ok(())
}
async fn run_test_in_subprocess() -> Result<()> {
skip_if_no_network!(Ok(()));
let proxy = start_mock_server().await;
let responses_mock = mount_sse_once(
&proxy,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let compact_mock = mount_compact_json_once(
&proxy,
serde_json::json!({
"output": [ResponseItem::Compaction {
id: None,
encrypted_content: "encrypted compacted history".to_string(),
internal_chat_message_metadata_passthrough: None,
}]
}),
)
.await;
// Reqwest disables its default environment proxy matcher in CGI processes. The route-aware
// client resolves `HTTP_PROXY` explicitly, so this marker makes the test fail if either
// Responses endpoint regresses to the default client.
let output = Command::new(std::env::current_exe()?)
.args(["--exact", TEST_NAME, "--nocapture"])
.env(SUBPROCESS_ENV, "1")
.env("HTTP_PROXY", proxy.uri())
.env("REQUEST_METHOD", "GET")
.env_remove("http_proxy")
.env_remove("ALL_PROXY")
.env_remove("all_proxy")
.env_remove("NO_PROXY")
.env_remove("no_proxy")
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"system-proxy subprocess failed\nstdout:\n{}\nstderr:\n{stderr}",
String::from_utf8_lossy(&output.stdout),
);
assert!(
stderr.contains(SUBPROCESS_SENTINEL),
"system-proxy subprocess did not execute the expected test\nstderr:\n{stderr}"
);
assert_eq!(responses_mock.single_request().path(), "/v1/responses");
assert_eq!(
compact_mock.single_request().path(),
"/v1/responses/compact"
);
Ok(())
}