mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user