mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why `features.respect_system_proxy` already routes authentication traffic through the OS proxy APIs, but it does not affect the primary inference path. That leaves users behind OS-managed proxies unable to send normal Responses API requests even after login succeeds. This PR is the first product-path migration onto the route-aware transport introduced in #31323 and refined in #31331. It also establishes the construction pattern for later migrations: the effective feature state is resolved once into a required HTTP client factory rather than represented by an optional per-call setting. The scope remains limited to the two HTTP Responses endpoints; WebSockets, model discovery, memories, realtime, and file uploads remain follow-up migrations. ## What changed - Replace the optional proxy marker with an explicit `OutboundProxyPolicy::{ReqwestDefault, RespectSystemProxy}` and a required `HttpClientFactory`. The policy has no default, and the lower-level route-aware reqwest builder is now private. - Have `Config` construct the factory from the effective feature state and require every `ModelClient` constructor to receive it. There is no optional setter or implicit `None` fallback. - Build HTTP clients for `/responses` and `/responses/compact` with `ClientRouteClass::Api`, using the complete destination URL so PAC rules can make URL-specific decisions. - Layer route-aware selection onto Codex's existing default headers, Cloudflare cookie store, custom CA handling, and sandbox no-proxy behavior. - Add an integration test that loads `features.respect_system_proxy` through `config.toml`, creates a real Codex session, and verifies that both a normal Responses turn and remote compaction reach an isolated local proxy. ## Review guide 1. `http-client/src/outbound_proxy.rs` defines the mandatory policy/factory boundary and keeps route resolution private. 2. `core/src/config/mod.rs`, `core/src/session/session.rs`, and `core/src/client.rs` show the compile-time invariant: effective config creates the factory, and `ModelClient` cannot be constructed without one. 3. `login/src/auth/default_client.rs` preserves existing default-client behavior while accepting the required factory for migrated routes. 4. `core/src/client.rs` switches only streaming Responses and remote compaction HTTP transports to the API route class. 5. `core/tests/suite/responses_api_system_proxy.rs` is the behavioral regression boundary. Its Linux subprocess deliberately sets the CGI marker that disables reqwest's implicit environment-proxy handling, so the test fails if session wiring or either Responses call site falls back to the default client. ## Test plan - `cargo check --tests -p codex-http-client -p codex-login -p codex-core` - `just test -p codex-login` - `just test -p codex-core respect_system_proxy_feature_resolves_enabled` - Existing `compact_uses_bearer_after_agent_identity_session_fallback` coverage passes with the new transport construction. - New Linux integration coverage: `responses_and_compact_use_enabled_system_proxy` - `just bazel-lock-check` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31335). * #31342 * __->__ #31335
393 lines
13 KiB
Rust
393 lines
13 KiB
Rust
use codex_core::CodexThread;
|
|
use codex_core::ModelClient;
|
|
use codex_core::NewThread;
|
|
use codex_core::Prompt;
|
|
use codex_core::ResponseEvent;
|
|
use codex_core::StartThreadOptions;
|
|
use codex_core::ThreadManager;
|
|
use codex_core::config::Config;
|
|
use codex_core::content_items_to_text;
|
|
use codex_core::detached_memory_responses_metadata;
|
|
use codex_core::resolve_installation_id;
|
|
use codex_features::Feature;
|
|
use codex_login::AuthManager;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::auth::AgentIdentityAuthPolicy;
|
|
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
|
|
use codex_login::default_client::originator;
|
|
use codex_model_provider::ModelProvider;
|
|
use codex_model_provider::SharedModelProvider;
|
|
use codex_model_provider::create_model_provider;
|
|
use codex_otel::SessionTelemetry;
|
|
use codex_otel::TelemetryAuthMode;
|
|
use codex_protocol::SessionId;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::openai_models::ModelInfo;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::protocol::InitialHistory;
|
|
use codex_protocol::protocol::InternalSessionSource;
|
|
use codex_protocol::protocol::Op;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::ThreadSource;
|
|
use codex_protocol::protocol::TokenUsage;
|
|
use codex_protocol::user_input::UserInput;
|
|
use codex_rollout_trace::InferenceTraceContext;
|
|
use codex_state::StateRuntime;
|
|
use codex_terminal_detection::user_agent;
|
|
use futures::StreamExt;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
pub(crate) struct SpawnedConsolidationAgent {
|
|
pub(crate) thread_id: ThreadId,
|
|
pub(crate) thread: Arc<CodexThread>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct StageOneRequestContext {
|
|
pub(crate) model_info: ModelInfo,
|
|
pub(crate) session_telemetry: SessionTelemetry,
|
|
pub(crate) reasoning_effort: Option<ReasoningEffort>,
|
|
pub(crate) reasoning_summary: ReasoningSummary,
|
|
pub(crate) service_tier: Option<String>,
|
|
}
|
|
|
|
impl StageOneRequestContext {
|
|
pub(crate) fn start_timer(&self, name: &str) -> Option<codex_otel::Timer> {
|
|
self.session_telemetry.start_timer(name, &[]).ok()
|
|
}
|
|
|
|
pub(crate) fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]) {
|
|
self.session_telemetry.counter(name, inc, tags);
|
|
}
|
|
|
|
pub(crate) fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) {
|
|
self.session_telemetry.histogram(name, value, tags);
|
|
}
|
|
}
|
|
|
|
pub(crate) struct MemoryStartupContext {
|
|
thread_id: ThreadId,
|
|
thread: Arc<CodexThread>,
|
|
thread_manager: Arc<ThreadManager>,
|
|
auth_manager: Arc<AuthManager>,
|
|
provider: SharedModelProvider,
|
|
session_telemetry: SessionTelemetry,
|
|
}
|
|
|
|
fn build_session_telemetry(
|
|
auth_manager: &AuthManager,
|
|
thread_id: ThreadId,
|
|
config: &Config,
|
|
source: SessionSource,
|
|
model: &str,
|
|
originator: String,
|
|
) -> SessionTelemetry {
|
|
let auth = auth_manager.auth_cached();
|
|
let auth = auth.as_ref();
|
|
let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from);
|
|
let account_id = auth.and_then(CodexAuth::get_account_id);
|
|
let account_email = auth.and_then(CodexAuth::get_account_email);
|
|
let auth_env_telemetry = collect_auth_env_telemetry(
|
|
&config.model_provider,
|
|
auth_manager.codex_api_key_env_enabled(),
|
|
);
|
|
SessionTelemetry::new(
|
|
thread_id,
|
|
model,
|
|
model,
|
|
account_id,
|
|
account_email,
|
|
auth_mode,
|
|
originator,
|
|
config.otel.log_user_prompt,
|
|
user_agent(),
|
|
source,
|
|
)
|
|
.with_auth_env(auth_env_telemetry.to_otel_metadata())
|
|
}
|
|
|
|
impl MemoryStartupContext {
|
|
pub(crate) fn new(
|
|
thread_manager: Arc<ThreadManager>,
|
|
auth_manager: Arc<AuthManager>,
|
|
thread_id: ThreadId,
|
|
thread: Arc<CodexThread>,
|
|
config: &Config,
|
|
source: SessionSource,
|
|
) -> Self {
|
|
let provider = create_model_provider(
|
|
config.model_provider.clone(),
|
|
Some(Arc::clone(&auth_manager)),
|
|
);
|
|
Self::new_with_provider(
|
|
thread_manager,
|
|
auth_manager,
|
|
thread_id,
|
|
thread,
|
|
config,
|
|
source,
|
|
provider,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn new_for_testing(
|
|
thread_manager: Arc<ThreadManager>,
|
|
auth_manager: Arc<AuthManager>,
|
|
thread_id: ThreadId,
|
|
thread: Arc<CodexThread>,
|
|
config: &Config,
|
|
source: SessionSource,
|
|
provider: SharedModelProvider,
|
|
) -> Self {
|
|
Self::new_with_provider(
|
|
thread_manager,
|
|
auth_manager,
|
|
thread_id,
|
|
thread,
|
|
config,
|
|
source,
|
|
provider,
|
|
)
|
|
}
|
|
|
|
fn new_with_provider(
|
|
thread_manager: Arc<ThreadManager>,
|
|
auth_manager: Arc<AuthManager>,
|
|
thread_id: ThreadId,
|
|
thread: Arc<CodexThread>,
|
|
config: &Config,
|
|
source: SessionSource,
|
|
provider: SharedModelProvider,
|
|
) -> Self {
|
|
let model = config.model.as_deref().unwrap_or("unknown");
|
|
let session_telemetry = build_session_telemetry(
|
|
&auth_manager,
|
|
thread_id,
|
|
config,
|
|
source,
|
|
model,
|
|
originator().value,
|
|
);
|
|
|
|
Self {
|
|
thread_id,
|
|
thread,
|
|
thread_manager,
|
|
auth_manager,
|
|
provider,
|
|
session_telemetry,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn thread_id(&self) -> ThreadId {
|
|
self.thread_id
|
|
}
|
|
|
|
pub(crate) fn state_db(&self) -> Option<Arc<StateRuntime>> {
|
|
self.thread.state_db()
|
|
}
|
|
|
|
pub(crate) fn provider(&self) -> &dyn ModelProvider {
|
|
self.provider.as_ref()
|
|
}
|
|
|
|
pub(crate) fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]) {
|
|
self.session_telemetry.counter(name, inc, tags);
|
|
}
|
|
|
|
pub(crate) fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) {
|
|
self.session_telemetry.histogram(name, value, tags);
|
|
}
|
|
|
|
pub(crate) fn start_timer(&self, name: &str) -> Option<codex_otel::Timer> {
|
|
self.session_telemetry.start_timer(name, &[]).ok()
|
|
}
|
|
|
|
pub(crate) async fn stage_one_request_context(
|
|
&self,
|
|
config: &Config,
|
|
model_name: &str,
|
|
reasoning_effort: ReasoningEffort,
|
|
) -> StageOneRequestContext {
|
|
let config_snapshot = self.thread.config_snapshot().await;
|
|
let model_info = self
|
|
.thread_manager
|
|
.get_models_manager()
|
|
.get_model_info(model_name, &config.to_models_manager_config())
|
|
.await;
|
|
let reasoning_summary = config
|
|
.model_reasoning_summary
|
|
.unwrap_or(model_info.default_reasoning_summary);
|
|
|
|
StageOneRequestContext {
|
|
model_info,
|
|
session_telemetry: build_session_telemetry(
|
|
&self.auth_manager,
|
|
self.thread_id,
|
|
config,
|
|
config_snapshot.session_source,
|
|
model_name,
|
|
config_snapshot.originator,
|
|
),
|
|
reasoning_effort: Some(reasoning_effort),
|
|
reasoning_summary,
|
|
service_tier: config_snapshot.service_tier,
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn stream_stage_one_prompt(
|
|
&self,
|
|
config: &Config,
|
|
prompt: &Prompt,
|
|
context: &StageOneRequestContext,
|
|
) -> anyhow::Result<(String, Option<TokenUsage>)> {
|
|
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
|
let config_snapshot = self.thread.config_snapshot().await;
|
|
let session_source = config_snapshot.session_source;
|
|
let session_id = SessionId::from(self.thread_id);
|
|
let session_id_string = session_id.to_string();
|
|
let model_client = ModelClient::new(
|
|
Some(Arc::clone(&self.auth_manager)),
|
|
AgentIdentityAuthPolicy::JwtOnly,
|
|
self.thread_id,
|
|
config.model_provider.clone(),
|
|
session_source.clone(),
|
|
config_snapshot.originator,
|
|
config.model_verbosity,
|
|
config.features.enabled(Feature::EnableRequestCompression),
|
|
config.features.enabled(Feature::RuntimeMetrics),
|
|
/*beta_features_header*/ None,
|
|
config.features.enabled(Feature::ItemIds),
|
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
|
/*attestation_provider*/ None,
|
|
config.http_client_factory(),
|
|
);
|
|
|
|
let mut client_session = model_client.new_session();
|
|
let window_id = format!("{}:0", self.thread_id);
|
|
let responses_metadata = detached_memory_responses_metadata(
|
|
installation_id,
|
|
session_id_string,
|
|
self.thread_id.to_string(),
|
|
window_id,
|
|
&session_source,
|
|
&config.cwd,
|
|
/*sandbox*/ None,
|
|
)
|
|
.await;
|
|
let mut stream = client_session
|
|
.stream(
|
|
prompt,
|
|
&context.model_info,
|
|
&context.session_telemetry,
|
|
context.reasoning_effort.clone(),
|
|
context.reasoning_summary,
|
|
context.service_tier.clone(),
|
|
&responses_metadata,
|
|
&InferenceTraceContext::disabled(),
|
|
)
|
|
.await?;
|
|
|
|
let mut result = String::new();
|
|
let mut token_usage = None;
|
|
while let Some(message) = stream.next().await.transpose()? {
|
|
match message {
|
|
ResponseEvent::OutputTextDelta(delta) => result.push_str(&delta),
|
|
ResponseEvent::OutputItemDone(item) => {
|
|
if result.is_empty()
|
|
&& let codex_protocol::models::ResponseItem::Message { content, .. } = item
|
|
&& let Some(text) = content_items_to_text(&content)
|
|
{
|
|
result.push_str(&text);
|
|
}
|
|
}
|
|
ResponseEvent::Completed {
|
|
token_usage: usage, ..
|
|
} => {
|
|
token_usage = usage;
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok((result, token_usage))
|
|
}
|
|
|
|
pub(crate) async fn spawn_consolidation_agent(
|
|
&self,
|
|
config: Config,
|
|
prompt: Vec<UserInput>,
|
|
) -> anyhow::Result<SpawnedConsolidationAgent> {
|
|
let environments = self
|
|
.thread_manager
|
|
.default_environment_selections(&config.cwd);
|
|
let NewThread {
|
|
thread_id, thread, ..
|
|
} = self
|
|
.thread_manager
|
|
.start_thread_with_options(StartThreadOptions {
|
|
config,
|
|
allow_provider_model_fallback: false,
|
|
initial_history: InitialHistory::New,
|
|
history_mode: None,
|
|
session_source: Some(SessionSource::Internal(
|
|
InternalSessionSource::MemoryConsolidation,
|
|
)),
|
|
thread_source: Some(ThreadSource::MemoryConsolidation),
|
|
dynamic_tools: Vec::new(),
|
|
metrics_service_name: None,
|
|
parent_trace: None,
|
|
environments,
|
|
thread_extension_init: Default::default(),
|
|
supports_openai_form_elicitation: false,
|
|
})
|
|
.await?;
|
|
|
|
let agent = SpawnedConsolidationAgent { thread_id, thread };
|
|
if let Err(err) = agent
|
|
.thread
|
|
.submit(Op::UserInput {
|
|
items: prompt,
|
|
final_output_json_schema: None,
|
|
responsesapi_client_metadata: None,
|
|
additional_context: Default::default(),
|
|
thread_settings: Default::default(),
|
|
})
|
|
.await
|
|
{
|
|
if let Err(shutdown_err) = self.shutdown_consolidation_agent(agent).await {
|
|
tracing::warn!(
|
|
"failed to shut down consolidation agent after submit error: {shutdown_err}"
|
|
);
|
|
}
|
|
return Err(err.into());
|
|
}
|
|
|
|
Ok(agent)
|
|
}
|
|
|
|
pub(crate) async fn shutdown_consolidation_agent(
|
|
&self,
|
|
agent: SpawnedConsolidationAgent,
|
|
) -> anyhow::Result<()> {
|
|
let SpawnedConsolidationAgent { thread_id, thread } = agent;
|
|
let thread = self
|
|
.thread_manager
|
|
.remove_thread(&thread_id)
|
|
.await
|
|
.unwrap_or(thread);
|
|
|
|
tokio::time::timeout(Duration::from_secs(10), thread.shutdown_and_wait())
|
|
.await
|
|
.map_err(|_| {
|
|
anyhow::anyhow!("memory consolidation agent {thread_id} shutdown timed out")
|
|
})??;
|
|
|
|
Ok(())
|
|
}
|
|
}
|