Propagate safety buffering events to app-server clients (#29371)

Responses API safety buffering metadata currently stops at the transport
boundary, so app-server clients cannot render the in-progress safety
review state.

This change:
- decodes and deduplicates `safety_buffering` metadata from Responses
API SSE and WebSocket events without suppressing the original response
event
- emits a typed core event containing the requested model plus backend
use cases and reasons
- forwards that event as `turn/safetyBuffering/updated` through
app-server v2 and updates generated protocol schemas
- keeps the side-channel event out of persisted rollouts and turn timing

This supports the Codex Apps buffering UX and depends on the Responses
API backend work in https://github.com/openai/openai/pull/1044569 and
https://github.com/openai/openai/pull/1044571.

Validation:
- focused `codex-core` safety-buffering integration test passes
- `cargo check -p codex-core -p codex-app-server -p
codex-app-server-protocol`
- `just fix -p codex-api -p codex-protocol -p codex-core -p
codex-app-server-protocol -p codex-app-server -p codex-rollout -p
codex-rollout-trace -p codex-otel`
- `just fmt`
- broad package test run: 4,430/4,492 passed; 62 unrelated
local-environment/concurrency failures involved unavailable test
binaries, MCP subprocess setup, and app-server timeouts
This commit is contained in:
Francis Chalissery
2026-06-22 03:39:14 +00:00
committed by GitHub
parent b21f0e7a98
commit 566f7bf631
25 changed files with 449 additions and 1 deletions
@@ -1655,6 +1655,7 @@ server_notification_definitions! {
ModelVerification => "model/verification" (v2::ModelVerificationNotification),
#[experimental("turn/moderationMetadata")]
TurnModerationMetadata => "turn/moderationMetadata" (v2::TurnModerationMetadataNotification),
ModelSafetyBufferingUpdated => "model/safetyBuffering/updated" (v2::ModelSafetyBufferingUpdatedNotification),
Warning => "warning" (v2::WarningNotification),
GuardianWarning => "guardianWarning" (v2::GuardianWarningNotification),
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
@@ -3347,6 +3348,33 @@ mod tests {
Ok(())
}
#[test]
fn serialize_model_safety_buffering_updated_notification() -> Result<()> {
let notification = ServerNotification::ModelSafetyBufferingUpdated(
v2::ModelSafetyBufferingUpdatedNotification {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
model: "gpt-5.4".to_string(),
use_cases: vec!["cyber".to_string()],
reasons: vec!["user_risk".to_string()],
},
);
assert_eq!(
json!({
"method": "model/safetyBuffering/updated",
"params": {
"threadId": "thr_123",
"turnId": "turn_123",
"model": "gpt-5.4",
"useCases": ["cyber"],
"reasons": ["user_risk"]
}
}),
serde_json::to_value(&notification)?,
);
Ok(())
}
#[test]
fn serialize_thread_realtime_output_audio_delta_notification() -> Result<()> {
let notification = ServerNotification::ThreadRealtimeOutputAudioDelta(
@@ -162,3 +162,14 @@ pub struct TurnModerationMetadataNotification {
pub turn_id: String,
pub metadata: JsonValue,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ModelSafetyBufferingUpdatedNotification {
pub thread_id: String,
pub turn_id: String,
pub model: String,
pub use_cases: Vec<String>,
pub reasons: Vec<String>,
}