mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Preserve Guardian request metadata with custom OpenAI base URLs (#57439)
## Why Custom OpenAI base URLs caused Codex backend authentication to lose Responses headers and Guardian metadata when the URL did not match a recognized backend route. ## What changed Apply model-scoped Responses headers, Guardian reviewer and classifier markers, and eligible ChatGPT parent credit requests based on the OpenAI provider and authentication configuration, regardless of the base URL. Keep API-key requests and other providers excluded from this behavior. ## Testing Extend coverage for custom `/v1` URLs and direct `/api/codex` endpoints with ChatGPT, header-based, and API-key authentication, including HTTP fallback and WebSocket requests. GitOrigin-RevId: cc4bb5b4a1a6596904ecf8965237c7a4f09e112b
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user