Propagate safety buffering treatment metadata (#29473)

## Summary

- read the request-scoped safety-buffering treatment from HTTP response
headers and per-turn WebSocket metadata through one shared header parser
- combine that treatment with Responses API safety-buffering signals
- propagate `showBufferingUi` and nullable `fasterModel` through the
existing `model/safetyBuffering/updated` app-server notification
- update the app-server documentation and generated JSON and TypeScript
schemas

The public implementation contains no model mapping or real model
identifier. Tests and protocol examples use generic `current-model` and
`faster-model` placeholders only.

## Dependencies

- server-side treatment evaluation:
https://github.com/openai/openai/pull/1060247
- initial Responses API safety-buffering propagation:
https://github.com/openai/codex/pull/29371
- Codex App UI: https://github.com/openai/openai/pull/1057789

## Validation

- Codex API tests: 129 passed
- focused Codex core safety-buffering integration test passed
- app-server protocol tests passed after regenerating schema fixtures
- Clippy fix and repository formatting completed successfully

The broader app-server run compiled all changed crates and completed
with 1,269 passing tests. Its remaining failures were unrelated
environment limitations: macOS sandbox application was denied, one
expected test binary was unavailable, and several existing subprocess
tests timed out as a result.
This commit is contained in:
Francis Chalissery
2026-06-22 19:51:03 -07:00
committed by GitHub
Unverified
parent 67009bc53f
commit 7c22d376e5
17 changed files with 193 additions and 15 deletions
+18
View File
@@ -118,6 +118,24 @@ pub enum ResponseEvent {
pub struct SafetyBuffering {
pub use_cases: Vec<String>,
pub reasons: Vec<String>,
#[serde(skip)]
pub show_buffering_ui: bool,
#[serde(skip)]
pub faster_model: Option<String>,
}
impl SafetyBuffering {
pub(crate) fn with_treatment(mut self, treatment: &SafetyBufferingTreatment) -> Self {
self.show_buffering_ui = treatment.show_buffering_ui;
self.faster_model.clone_from(&treatment.faster_model);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SafetyBufferingTreatment {
pub show_buffering_ui: bool,
pub faster_model: Option<String>,
}
#[derive(Debug, Serialize, Clone, PartialEq)]
@@ -2,9 +2,11 @@ use crate::auth::SharedAuthProvider;
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::common::ResponsesWsRequest;
use crate::common::SafetyBufferingTreatment;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::rate_limits::parse_rate_limit_event;
use crate::safety_buffering::treatment_from_headers;
use crate::sse::ResponsesStreamEvent;
use crate::sse::process_responses_event;
use crate::telemetry::WebsocketTelemetry;
@@ -595,12 +597,12 @@ fn map_wrapped_websocket_error_event(
Some(ApiError::Transport(TransportError::Http {
status,
url: None,
headers: headers.map(json_headers_to_http_headers),
headers: headers.as_ref().map(json_headers_to_http_headers),
body: Some(original_payload),
}))
}
fn json_headers_to_http_headers(headers: JsonMap<String, Value>) -> HeaderMap {
fn json_headers_to_http_headers(headers: &JsonMap<String, Value>) -> HeaderMap {
let mut mapped = HeaderMap::new();
for (name, value) in headers {
let Ok(header_name) = HeaderName::from_bytes(name.as_bytes()) else {
@@ -614,9 +616,9 @@ fn json_headers_to_http_headers(headers: JsonMap<String, Value>) -> HeaderMap {
mapped
}
fn json_header_value(value: Value) -> Option<HeaderValue> {
fn json_header_value(value: &Value) -> Option<HeaderValue> {
let value = match value {
Value::String(value) => value,
Value::String(value) => value.clone(),
Value::Number(value) => value.to_string(),
Value::Bool(value) => value.to_string(),
_ => return None,
@@ -634,6 +636,7 @@ async fn run_websocket_response_stream(
turn_state: Option<&OnceLock<String>>,
) -> Result<(), ApiError> {
let mut last_server_model: Option<String> = None;
let mut safety_buffering_treatment = SafetyBufferingTreatment::default();
send_websocket_request(
ws_stream,
request_text,
@@ -687,9 +690,17 @@ async fn run_websocket_response_stream(
{
let _ = turn_state.set(response_turn_state);
}
if let Some(headers) = event.headers.as_ref().and_then(Value::as_object)
&& let Some(treatment) =
treatment_from_headers(&json_headers_to_http_headers(headers))
{
safety_buffering_treatment = treatment;
}
let model_verifications = event.model_verifications();
let turn_moderation_metadata = event.turn_moderation_metadata();
let safety_buffering = event.safety_buffering();
let safety_buffering = event
.safety_buffering()
.map(|buffering| buffering.with_treatment(&safety_buffering_treatment));
if event.kind() == "codex.rate_limits" {
if let Some(snapshot) = parse_rate_limit_event(&text) {
let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;
+1
View File
@@ -8,6 +8,7 @@ pub(crate) mod images;
pub(crate) mod provider;
pub(crate) mod rate_limits;
pub(crate) mod requests;
pub(crate) mod safety_buffering;
pub(crate) mod search;
pub(crate) mod sse;
pub(crate) mod telemetry;
@@ -0,0 +1,54 @@
use crate::common::SafetyBufferingTreatment;
use http::HeaderMap;
pub(crate) const X_CODEX_SAFETY_BUFFERING_ENABLED_HEADER: &str = "x-codex-safety-buffering-enabled";
pub(crate) const X_CODEX_SAFETY_BUFFERING_FASTER_MODEL_HEADER: &str =
"x-codex-safety-buffering-faster-model";
pub(crate) fn treatment_from_headers(headers: &HeaderMap) -> Option<SafetyBufferingTreatment> {
let show_buffering_ui = headers
.get(X_CODEX_SAFETY_BUFFERING_ENABLED_HEADER)
.and_then(|value| value.to_str().ok())?
.eq_ignore_ascii_case("true");
let faster_model = if show_buffering_ui {
headers
.get(X_CODEX_SAFETY_BUFFERING_FASTER_MODEL_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::to_string)
} else {
None
};
Some(SafetyBufferingTreatment {
show_buffering_ui,
faster_model,
})
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderValue;
use pretty_assertions::assert_eq;
#[test]
fn reads_treatment_from_http_headers() {
let mut headers = HeaderMap::new();
headers.insert(
X_CODEX_SAFETY_BUFFERING_ENABLED_HEADER,
HeaderValue::from_static("true"),
);
headers.insert(
X_CODEX_SAFETY_BUFFERING_FASTER_MODEL_HEADER,
HeaderValue::from_static("faster-model"),
);
assert_eq!(
treatment_from_headers(&headers),
Some(SafetyBufferingTreatment {
show_buffering_ui: true,
faster_model: Some("faster-model".to_string()),
})
);
}
}
+34 -3
View File
@@ -1,8 +1,10 @@
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::common::SafetyBuffering;
use crate::common::SafetyBufferingTreatment;
use crate::error::ApiError;
use crate::rate_limits::parse_all_rate_limits;
use crate::safety_buffering::treatment_from_headers;
use crate::telemetry::SseTelemetry;
use codex_client::ByteStream;
use codex_client::StreamResponse;
@@ -55,6 +57,8 @@ pub fn spawn_response_stream(
.get(REQUEST_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let safety_buffering_treatment =
treatment_from_headers(&stream_response.headers).unwrap_or_default();
if let Some(turn_state) = turn_state.as_ref()
&& let Some(header_value) = stream_response
.headers
@@ -79,7 +83,14 @@ pub fn spawn_response_stream(
.send(Ok(ResponseEvent::ServerReasoningIncluded(true)))
.await;
}
process_sse(stream_response.bytes, tx_event, idle_timeout, telemetry).await;
process_sse_with_treatment(
stream_response.bytes,
tx_event,
idle_timeout,
telemetry,
safety_buffering_treatment,
)
.await;
});
ResponseStream {
@@ -149,7 +160,7 @@ struct ResponseCompletedOutputTokensDetails {
pub struct ResponsesStreamEvent {
#[serde(rename = "type")]
pub(crate) kind: String,
headers: Option<Value>,
pub(crate) headers: Option<Value>,
metadata: Option<Value>,
response: Option<Value>,
item: Option<Value>,
@@ -437,11 +448,29 @@ pub fn process_responses_event(
Ok(None)
}
#[cfg(test)]
pub async fn process_sse(
stream: ByteStream,
tx_event: mpsc::Sender<Result<ResponseEvent, ApiError>>,
idle_timeout: Duration,
telemetry: Option<Arc<dyn SseTelemetry>>,
) {
process_sse_with_treatment(
stream,
tx_event,
idle_timeout,
telemetry,
SafetyBufferingTreatment::default(),
)
.await;
}
async fn process_sse_with_treatment(
stream: ByteStream,
tx_event: mpsc::Sender<Result<ResponseEvent, ApiError>>,
idle_timeout: Duration,
telemetry: Option<Arc<dyn SseTelemetry>>,
safety_buffering_treatment: SafetyBufferingTreatment,
) {
let mut stream = stream.eventsource();
let mut response_error: Option<ApiError> = None;
@@ -486,7 +515,9 @@ pub async fn process_sse(
};
let model_verifications = event.model_verifications();
let turn_moderation_metadata = event.turn_moderation_metadata();
let safety_buffering = event.safety_buffering();
let safety_buffering = event
.safety_buffering()
.map(|buffering| buffering.with_treatment(&safety_buffering_treatment));
if let Some(model) = event.response_model()
&& last_server_model.as_deref() != Some(model.as_str())