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:
@@ -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::HttpClientFactory;
|
||||
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>,
|
||||
http_client_factory: HttpClientFactory,
|
||||
}
|
||||
|
||||
/// A turn-scoped streaming session created from a [`ModelClient`].
|
||||
@@ -396,7 +400,9 @@ impl ModelClient {
|
||||
/// Creates a new session-scoped `ModelClient`.
|
||||
///
|
||||
/// All arguments are expected to be stable for the lifetime of a Codex session. Per-turn values
|
||||
/// are passed to [`ModelClientSession::stream`] (and other turn-scoped methods) explicitly.
|
||||
/// are passed to [`ModelClientSession::stream`] (and other turn-scoped methods) explicitly. The
|
||||
/// HTTP client factory must come from the effective session configuration so every transport
|
||||
/// observes the resolved outbound proxy policy.
|
||||
pub fn new(
|
||||
auth_manager: Option<Arc<AuthManager>>,
|
||||
agent_identity_policy: AgentIdentityAuthPolicy,
|
||||
@@ -410,6 +416,7 @@ impl ModelClient {
|
||||
beta_features_header: Option<String>,
|
||||
item_ids_enabled: bool,
|
||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||
http_client_factory: HttpClientFactory,
|
||||
) -> Self {
|
||||
let model_provider = create_model_provider(provider_info, auth_manager);
|
||||
let codex_api_key_env_enabled = model_provider
|
||||
@@ -439,6 +446,7 @@ impl ModelClient {
|
||||
}),
|
||||
agent_identity_policy,
|
||||
prompt_cache_key_override: None,
|
||||
http_client_factory,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,7 +540,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 +947,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(
|
||||
&self.http_client_factory,
|
||||
&request_url,
|
||||
ClientRouteClass::Api,
|
||||
)
|
||||
.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 +1396,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(),
|
||||
|
||||
@@ -19,6 +19,8 @@ use codex_api::AgentIdentityTelemetry;
|
||||
use codex_api::ApiError;
|
||||
use codex_api::ResponseEvent;
|
||||
use codex_api::TransportError;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
@@ -104,6 +106,7 @@ fn test_model_client_with_thread_id(
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,6 +151,7 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
let prompt = Prompt {
|
||||
input: vec![ResponseItem::Message {
|
||||
@@ -824,6 +828,7 @@ fn model_client_with_counting_attestation(
|
||||
Some(Arc::new(CountingAttestationProvider {
|
||||
calls: attestation_calls.clone(),
|
||||
})),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
(model_client, attestation_calls)
|
||||
}
|
||||
|
||||
@@ -1590,6 +1590,10 @@ respect_system_proxy = true
|
||||
.await?;
|
||||
|
||||
assert!(config.respect_system_proxy);
|
||||
assert_eq!(
|
||||
config.http_client_factory().outbound_proxy_policy(),
|
||||
codex_http_client::OutboundProxyPolicy::RespectSystemProxy
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ 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::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_login::AuthRouteConfig;
|
||||
@@ -1481,6 +1483,16 @@ impl Config {
|
||||
.then(AuthRouteConfig::respect_system_proxy)
|
||||
}
|
||||
|
||||
/// Creates the HTTP client factory resolved from the effective feature configuration.
|
||||
pub fn http_client_factory(&self) -> HttpClientFactory {
|
||||
let outbound_proxy_policy = if self.respect_system_proxy {
|
||||
OutboundProxyPolicy::RespectSystemProxy
|
||||
} else {
|
||||
OutboundProxyPolicy::ReqwestDefault
|
||||
};
|
||||
HttpClientFactory::new(outbound_proxy_policy)
|
||||
}
|
||||
|
||||
/// Build the plugin-manager input from the effective config.
|
||||
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
||||
PluginsConfigInput::new(
|
||||
|
||||
@@ -1112,6 +1112,7 @@ impl Session {
|
||||
Self::build_model_client_beta_features_header(config.as_ref()),
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
attestation_provider,
|
||||
config.http_client_factory(),
|
||||
)
|
||||
.with_prompt_cache_key_override(
|
||||
crate::guardian::prompt_cache_key_override_for_review_session(
|
||||
|
||||
@@ -33,6 +33,8 @@ use codex_core_skills::HostSkillsSnapshot;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::AgentIdentityAuthPolicy;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -491,6 +493,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
)
|
||||
.new_session()
|
||||
}
|
||||
@@ -5461,6 +5464,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
Session::build_model_client_beta_features_header(config.as_ref()),
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
||||
codex_code_mode::InProcessCodeModeSessionProvider,
|
||||
@@ -7587,6 +7591,7 @@ where
|
||||
Session::build_model_client_beta_features_header(config.as_ref()),
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
||||
codex_code_mode::InProcessCodeModeSessionProvider,
|
||||
|
||||
@@ -132,6 +132,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||
let mut client_session = client.new_session();
|
||||
@@ -266,6 +267,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||
let mut client_session = client.new_session();
|
||||
@@ -386,6 +388,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||
let mut client_session = client.new_session();
|
||||
|
||||
@@ -1335,6 +1335,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
||||
let mut client_session = client.new_session();
|
||||
@@ -2949,6 +2950,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ false,
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
||||
let mut client_session = client.new_session();
|
||||
|
||||
@@ -2252,6 +2252,7 @@ async fn websocket_harness_with_provider_options(
|
||||
/*beta_features_header*/ None,
|
||||
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
|
||||
/*attestation_provider*/ None,
|
||||
config.http_client_factory(),
|
||||
);
|
||||
|
||||
WebsocketTestHarness {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user