Add trusted request-country metadata to Responses requests (#61535)

## What changed

- Add `RequestCountry` for host-supplied country codes. Normalize two-letter ASCII codes to uppercase, omit the field by default, and serialize explicit unknown (`XX`) as `null` to prevent fallback to request provenance.
- Carry `request_country` through thread defaults and captured step settings into Codex backend HTTP and WebSocket requests. Allow live country updates without enabling `step_model_switching`, and preserve WebSocket continuation reuse when the country changes.
- Propagate the captured country to subagents, reviews, and compaction requests.

## Testing

Add coverage for country validation and serialization, thread and live-turn update lifetimes, prewarm settings capture, agent inheritance and reloads, compaction and reviews, WebSocket country changes, and HTTP fallback.

GitOrigin-RevId: 787cd1fa7ba5d465a4eb64a92f13c79e93d9bca7
This commit is contained in:
goodman-oai
2026-11-14 15:44:38 +00:00
committed by copyberry
parent b1573f1024
commit 65a5410f72
53 changed files with 854 additions and 64 deletions
+5
View File
@@ -349,6 +349,8 @@ pub struct ResponsesApiRequest {
pub client_metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_programs: Option<AccessPrograms>,
#[serde(skip_serializing_if = "codex_protocol::RequestCountry::is_omitted")]
pub request_country: codex_protocol::RequestCountry,
}
impl<'a> From<&'a ResponsesApiRequest> for ResponseCreateWsRequest<'a> {
@@ -372,6 +374,7 @@ impl<'a> From<&'a ResponsesApiRequest> for ResponseCreateWsRequest<'a> {
generate: None,
client_metadata: request.client_metadata.clone(),
access_programs: request.access_programs,
request_country: &request.request_country,
}
}
}
@@ -408,6 +411,8 @@ pub struct ResponseCreateWsRequest<'a> {
pub client_metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_programs: Option<AccessPrograms>,
#[serde(skip_serializing_if = "codex_protocol::RequestCountry::is_omitted")]
pub request_country: &'a codex_protocol::RequestCountry,
}
pub fn response_create_client_metadata(
@@ -1171,7 +1171,22 @@ mod tests {
#[test]
fn direct_serialization_preserves_websocket_request_payload() {
for (country_code, expected_country) in [
(None, None),
(Some(" de "), Some(json!("DE"))),
(Some("xX"), Some(Value::Null)),
] {
assert_serialized_request(country_code, expected_country);
}
}
fn assert_serialized_request(country_code: Option<&str>, expected_country: Option<Value>) {
let api_request = ResponsesApiRequest {
request_country: country_code
.map(codex_protocol::RequestCountry::from_iso_code)
.transpose()
.expect("country is valid")
.unwrap_or_default(),
model: "gpt-test".to_string(),
input: vec![ResponseItem::Message {
status: None,
@@ -1224,6 +1239,10 @@ mod tests {
let mut expected_payload =
serde_json::to_value(&api_request).expect("serialize responses API request");
assert_eq!(
expected_payload.get("request_country"),
expected_country.as_ref()
);
expected_payload["type"] = json!("response.create");
expected_payload["previous_response_id"] = json!("resp-1");
expected_payload["generate"] = json!(false);
@@ -1235,6 +1254,10 @@ mod tests {
let wire_payload =
serde_json::from_str::<Value>(&request_text).expect("parse websocket request");
assert_eq!(
wire_payload.get("request_country"),
expected_country.as_ref()
);
assert_eq!(wire_payload, expected_payload);
}
+4
View File
@@ -335,6 +335,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
let transport = RecordingTransport::new(state.clone());
let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
let request = ResponsesApiRequest {
request_country: Default::default(),
model: "gpt-test".into(),
input: vec![ResponseItem::Message {
status: None,
@@ -392,6 +393,7 @@ async fn responses_client_stream_request_sends_routing_fields_ahead_of_large_inp
// buffering a potentially multi-megabyte prompt first.
let large_input = "x".repeat(2 * 1024 * 1024);
let request = ResponsesApiRequest {
request_country: Default::default(),
model: "gpt-test".into(),
input: vec![ResponseItem::Message {
status: None,
@@ -491,6 +493,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
provider.retry.max_attempts = 2;
let request = ResponsesApiRequest {
request_country: Default::default(),
model: "gpt-test".into(),
input: Vec::new(),
tools: Some(empty_tools().into()),
@@ -609,6 +612,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth));
let request = ResponsesApiRequest {
request_country: Default::default(),
model: "gpt-test".into(),
input: vec![ResponseItem::Message {
status: None,