diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index a3a1429ee0..3ce7006b82 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1337,11 +1337,7 @@ impl ModelClient { fn responses_headers(&self, auth: Option<&CodexAuth>, model: &str) -> ApiHeaderMap { self.codex_responses_headers .as_ref() - .filter(|config| { - config.model == model - && self.uses_codex_backend(auth) - && self.state.provider.info().supports_codex_backend_routes() - }) + .filter(|config| config.model == model && self.uses_codex_backend(auth)) .map(|config| config.headers.clone()) .unwrap_or_default() } @@ -1382,7 +1378,6 @@ impl ModelClient { Some(CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_)) ) && self.uses_codex_backend(auth) - && self.state.provider.info().supports_codex_backend_routes() { metadata .get_or_insert_with(HashMap::new) diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 1e249231be..a08faf802c 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -2097,7 +2097,7 @@ fn thread_responses_headers_are_scoped_to_model_and_backend_auth() { let chatgpt_auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let api_key_auth = CodexAuth::from_api_key("test-api-key"); for (auth, model, expected) in [ - (Some(&chatgpt_auth), "selected-model", headers), + (Some(&chatgpt_auth), "selected-model", headers.clone()), (Some(&chatgpt_auth), "other-model", http::HeaderMap::new()), ( Some(&api_key_auth), @@ -2117,7 +2117,7 @@ fn thread_responses_headers_are_scoped_to_model_and_backend_auth() { ); assert_eq!( model_client.responses_headers(Some(&chatgpt_auth), "selected-model"), - http::HeaderMap::new(), + headers, ); } diff --git a/codex-rs/core/src/responses_headers.rs b/codex-rs/core/src/responses_headers.rs index 44d9a5d28e..67fd1e3544 100644 --- a/codex-rs/core/src/responses_headers.rs +++ b/codex-rs/core/src/responses_headers.rs @@ -5,9 +5,10 @@ use http::HeaderMap; /// Seed in `StartThreadOptions::thread_extension_init` to supply Responses headers. /// -/// Applied only to the selected model with Codex backend auth and routing. The headers -/// are sent on HTTP requests and WebSocket handshakes; changes require a new socket. -/// Other provider endpoints and unrelated threads do not inherit these headers. +/// Applied only to the selected model on the OpenAI provider with Codex backend +/// auth, regardless of the base URL. The headers are sent on HTTP requests and +/// WebSocket handshakes; changes require a new socket. Other providers and +/// unrelated threads do not inherit these headers. #[derive(Clone)] pub struct CodexResponsesHeaders { pub model: String, diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 2f2b45e6f6..032490764e 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -24,6 +24,7 @@ use codex_extension_api::ToolStartInput; use codex_features::CurrentTimeSource; use codex_features::Feature; use codex_history::RolloutItem; +use codex_login::AuthHeaders; use codex_login::CodexAuth; use codex_models_manager::manager::RefreshStrategy; use codex_otel::MetricsClient; @@ -144,8 +145,10 @@ impl TimeProvider for RecordingTimeProvider { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[test_case(CodexAuth::from_api_key("test-api-key"), "OpenAI", "/v1", true, "/v1/responses", true; "api_key_uses_responses")] #[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "OpenAI", "/backend-api/codex", false, "/backend-api/codex/responses", true; "chatgpt_marks_guardian_by_default")] +#[test_case(CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())), "OpenAI", "/api/codex", false, "/api/codex/responses", true; "header_auth_marks_guardian_on_direct_backend")] +#[test_case(CodexAuth::from_api_key("test-api-key"), "OpenAI", "/api/codex", true, "/api/codex/responses", true; "api_key_does_not_mark_guardian_on_direct_backend")] #[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "OpenAI", "/backend-api/codex", true, "/backend-api/codex/responses", true; "legacy_opt_in_still_accepted")] -#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "OpenAI", "/v1", true, "/v1/responses", true; "custom_openai_url_uses_responses")] +#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "OpenAI", "/v1", true, "/v1/responses", true; "custom_openai_url_marks_guardian")] #[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "Custom", "/backend-api/codex", true, "/backend-api/codex/responses", true; "custom_provider_uses_responses")] #[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "OpenAI", "/backend-api/codex", true, "/backend-api/codex/responses", false; "retry_without_response_id_keeps_last_parent")] async fn guardian_session_inherits_parent_http_fallback( @@ -156,8 +159,8 @@ async fn guardian_session_inherits_parent_http_fallback( expected_guardian_path: &str, response_id_present: bool, ) -> Result<()> { - let credits_enabled = - auth.uses_codex_backend() && provider_name == "OpenAI" && base_path == "/backend-api/codex"; + let credits_enabled = auth.uses_codex_backend() && provider_name == "OpenAI"; + let requests_parent_credits = credits_enabled && auth.is_chatgpt_auth(); skip_if_no_network!(Ok(())); let configured_policy = "Use the task-configured Guardian policy."; @@ -299,7 +302,7 @@ async fn guardian_session_inherits_parent_http_fallback( .cloned(), body["client_metadata"].get("parent_response_id"), ), - (credits_enabled.then(|| json!("true")), None) + (requests_parent_credits.then(|| json!("true")), None) ); } } @@ -878,18 +881,21 @@ async fn guardian_requests_record_only_their_own_tool_calls() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[test_case(CodexAuth::from_api_key("test-api-key"), true, "gpt-5.6-luna"; "api_key_uses_luna_with_responses_lite")] -#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), true, "codex-auto-review"; "chatgpt_uses_codex_auto_review")] -#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), false, "codex-auto-review"; "chatgpt_without_free_guardian")] +#[test_case(CodexAuth::from_api_key("test-api-key"), "/v1", true, "gpt-5.6-luna"; "api_key_uses_luna_with_responses_lite")] +#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "/backend-api/codex", true, "codex-auto-review"; "chatgpt_uses_codex_auto_review")] +#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "/backend-api/codex", false, "codex-auto-review"; "chatgpt_without_free_guardian")] +#[test_case(CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())), "/api/codex", false, "codex-auto-review"; "header_auth_uses_codex_auto_review_on_direct_backend")] +#[test_case(CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())), "/v1", false, "codex-auto-review"; "header_auth_marks_guardian_on_custom_url")] async fn guardian_session_prewarms_and_is_reused_for_first_review( auth: CodexAuth, + base_path: &str, free_guardian: bool, expected_model: &str, ) -> Result<()> { skip_if_no_network!(Ok(())); - let uses_codex_backend = auth.uses_codex_backend(); - let credits_enabled = uses_codex_backend; + let credits_enabled = auth.uses_codex_backend(); + let requests_parent_credits = credits_enabled && auth.is_chatgpt_auth(); let bundled_models = codex_models_manager::bundled_models_response()?.models; let catalog_auto_review = bundled_models .iter() @@ -950,7 +956,7 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( let time_provider = Arc::new(RecordingTimeProvider { thread_ids: Mutex::new(Vec::new()), }); - let backend_base_url = format!("{}/backend-api/codex", server.uri()); + let backend_base_url = format!("{}{base_path}", server.uri()); let mut builder = test_codex() .with_auth(auth) .with_pre_build_hook(move |home| { @@ -961,9 +967,7 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( .expect("Guardian endpoint configuration should be written"); }) .with_config(move |config| { - if uses_codex_backend { - config.model_provider.base_url = Some(backend_base_url); - } + config.model_provider.base_url = Some(backend_base_url); let rules_dir = config.codex_home.join("rules"); fs::create_dir_all(&rules_dir).expect("create execution policy directory"); let policy_justification = format!( @@ -1074,7 +1078,7 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( ( credits_enabled.then(|| json!(parent_response_id)), None, - credits_enabled.then(|| json!("true")), + requests_parent_credits.then(|| json!("true")), None, ) ); @@ -1207,14 +1211,7 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( for handshake in server.handshakes() { let is_guardian = handshake.header("x-openai-subagent").as_deref() == Some("guardian"); let is_guardian_request = credits_enabled && is_guardian; - assert_eq!( - handshake.uri(), - if uses_codex_backend { - "/backend-api/codex/responses" - } else { - "/v1/responses" - } - ); + assert_eq!(handshake.uri(), format!("{base_path}/responses")); assert_eq!( handshake.header("x-codex-guardian").as_deref(), is_guardian_request.then_some("reviewer") diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/connection_pool.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/connection_pool.rs index e5197f7e42..23165a419f 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/connection_pool.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/connection_pool.rs @@ -439,7 +439,7 @@ impl ConnectionPool { .await .as_ref() .is_some_and(CodexAuth::uses_codex_backend) - && provider.supports_codex_backend_routes() + && provider.is_openai() && provider.requires_openai_auth && provider.env_key.is_none() && provider.experimental_bearer_token.is_none() diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs index 897a4be7ef..69ea60efe3 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler_tests.rs @@ -8,6 +8,7 @@ use codex_guardian_context::PreviousReviews; use codex_http_client::HttpClientFactory; use codex_http_client::OutboundProxyPolicy; use codex_login::AgentIdentityAuthPolicy; +use codex_login::AuthHeaders; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::ExternalAuth; @@ -541,11 +542,23 @@ async fn classifier_sends_guardian_header_only_with_codex_backend_auth() -> Resu None, ), ( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - "/v1", + CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())), + "/api/codex", + Some("classifier"), + None, + ), + ( + CodexAuth::from_api_key("test-api-key"), + "/api/codex", None, Some("priority"), ), + ( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + "/v1", + Some("classifier"), + None, + ), ( CodexAuth::from_api_key("test-api-key"), "/v1", diff --git a/codex-rs/model-provider-info/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs index ba3bd79290..78f476af1a 100644 --- a/codex-rs/model-provider-info/src/model_provider_info_tests.rs +++ b/codex-rs/model-provider-info/src/model_provider_info_tests.rs @@ -245,6 +245,8 @@ fn codex_backend_routes_require_codex_base_url() { (None, true), (Some(CHATGPT_CODEX_BASE_URL), true), (Some("https://chatgpt-staging.com/backend-api/codex/"), true), + (Some("https://backend.example.com/api/codex"), false), + (Some("https://backend.example.com/api/codex/"), false), (Some("https://proxy.example.com/v1"), false), ] { let provider = ModelProviderInfo::create_openai_provider(base_url.map(str::to_owned));