[codex] Read retry model from buffering events (#31262)

## Summary

- deserialize `retry_model` from streamed `safety_buffering` payloads
- preserve the existing downstream faster-model API and legacy header
fallback
- update SSE, WebSocket, and end-to-end safety-buffering coverage

## Root cause

Follow-up to #31064. The Responses API emits the retry target as
`retry_model`, but the client was looking for `faster_model`, so the
payload value was ignored in favor of the compatibility fallback.

## Behavior

A non-null `retry_model` from the buffering payload takes precedence. An
explicit null leaves the retry target unset, while an omitted field
continues to fall back to the existing response header.

## Validation

- `just test -p codex-api` (135 tests)
- `just test -p codex-core safety_buffering` (2 tests)
- `just fix -p codex-api`
- `just fmt`
- `git diff --check`
This commit is contained in:
Francis Chalissery authored and GitHub committed 2026-07-06 17:04:26 +00:00
1 parent 8917244f7d
commit 7094fa467e
4 files changed
+10 -9

No files matched your search

+1
View File
@@ -123,6 +123,7 @@ pub struct SafetyBuffering {
pub reasons: Vec<String>, pub reasons: Vec<String>,
#[serde(skip)] #[serde(skip)]
pub show_buffering_ui: bool, pub show_buffering_ui: bool,
#[serde(rename = "retry_model")]
pub faster_model: Option<String>, pub faster_model: Option<String>,
} }
@@ -1061,7 +1061,7 @@ mod tests {
"safety_buffering": { "safety_buffering": {
"use_cases": ["cyber"], "use_cases": ["cyber"],
"reasons": ["user_risk"], "reasons": ["user_risk"],
"faster_model": "gpt-fast-wire" "retry_model": "gpt-fast-wire"
} }
})) }))
.expect("deserialize safety buffering event"); .expect("deserialize safety buffering event");
+7 -7
View File
@@ -236,10 +236,10 @@ impl ResponsesStreamEvent {
treatment: &SafetyBufferingTreatment, treatment: &SafetyBufferingTreatment,
) -> Option<SafetyBuffering> { ) -> Option<SafetyBuffering> {
let value = self.safety_buffering.as_ref()?; let value = self.safety_buffering.as_ref()?;
let faster_model_present = value.as_object()?.contains_key("faster_model"); let retry_model_present = value.as_object()?.contains_key("retry_model");
let mut buffering: SafetyBuffering = serde_json::from_value(value.clone()).ok()?; let mut buffering: SafetyBuffering = serde_json::from_value(value.clone()).ok()?;
buffering.show_buffering_ui = true; buffering.show_buffering_ui = true;
if !faster_model_present { if !retry_model_present {
buffering.faster_model.clone_from(&treatment.faster_model); buffering.faster_model.clone_from(&treatment.faster_model);
} }
Some(buffering) Some(buffering)
@@ -1405,7 +1405,7 @@ mod tests {
"safety_buffering": { "safety_buffering": {
"use_cases": ["cyber"], "use_cases": ["cyber"],
"reasons": ["user_risk"], "reasons": ["user_risk"],
"faster_model": "gpt-fast-wire" "retry_model": "gpt-fast-wire"
} }
}), }),
json!({ json!({
@@ -1453,12 +1453,12 @@ mod tests {
} }
#[test] #[test]
fn safety_buffering_prefers_wire_faster_model_and_only_falls_back_when_omitted() { fn safety_buffering_prefers_wire_retry_model_and_only_falls_back_when_omitted() {
let treatment = SafetyBufferingTreatment { let treatment = SafetyBufferingTreatment {
faster_model: Some("gpt-fast-header".to_string()), faster_model: Some("gpt-fast-header".to_string()),
}; };
for (faster_model, expected_faster_model) in [ for (retry_model, expected_faster_model) in [
(None, Some("gpt-fast-header")), (None, Some("gpt-fast-header")),
(Some(Value::Null), None), (Some(Value::Null), None),
(Some(json!("gpt-fast-wire")), Some("gpt-fast-wire")), (Some(json!("gpt-fast-wire")), Some("gpt-fast-wire")),
@@ -1470,8 +1470,8 @@ mod tests {
"reasons": ["user_risk"] "reasons": ["user_risk"]
} }
}); });
if let Some(faster_model) = faster_model { if let Some(retry_model) = retry_model {
event["safety_buffering"]["faster_model"] = faster_model; event["safety_buffering"]["retry_model"] = retry_model;
} }
let event: ResponsesStreamEvent = let event: ResponsesStreamEvent =
serde_json::from_value(event).expect("deserialize safety buffering event"); serde_json::from_value(event).expect("deserialize safety buffering event");
@@ -83,7 +83,7 @@ async fn emits_safety_buffering_with_the_responses_api_model_without_header_gati
created["safety_buffering"] = json!({ created["safety_buffering"] = json!({
"use_cases": ["cyber"], "use_cases": ["cyber"],
"reasons": ["policy-check"], "reasons": ["policy-check"],
"faster_model": FASTER_MODEL, "retry_model": FASTER_MODEL,
}); });
mount_response_once( mount_response_once(
&server, &server,