Handle bio policy errors in Codex (#31439)

## Summary

- Treat streamed Responses `bio_policy` failures as terminal invalid
requests instead of retryable stream errors.
- Recognize the new biology policy code and message in the TUI while
preserving the legacy `invalid_prompt` contract.
- Keep the existing dedicated biology safety notice and add
regression/snapshot coverage for all supported error shapes.

## Why

[openai/openai#1068559](https://github.com/openai/openai/pull/1068559)
gates a Responses API contract change from `invalid_prompt` and the
legacy message to `bio_policy` and new biology copy.

Without this compatibility change, streamed blocks are retried as
transient failures and the OSS TUI falls back to a generic/raw error
instead of the dedicated safety notice.

## Validation

- `just test -p codex-api` — 137 passed
- `just test -p codex-tui
app_server_safety_access_errors_render_dedicated_notice` — passed
- `just fix -p codex-api`
- `just fix -p codex-tui`
- `just fmt`
- `just test -p codex-tui` — 2,957 passed; two reproducible failures
remain in untouched Guardian feature-flag persistence tests:
-
`update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
-
`update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
This commit is contained in:
Francis Chalissery
2026-07-07 10:33:42 -07:00
committed by GitHub
parent f6e251c3ac
commit 78df1237d1
4 changed files with 89 additions and 30 deletions
+35 -19
View File
@@ -398,7 +398,8 @@ pub fn process_responses_event(
} else if is_cyber_policy_error(&error) {
let message = cyber_policy_message(error.message);
response_error = ApiError::CyberPolicy { message };
} else if is_invalid_prompt_error(&error) {
} else if matches!(error.code.as_deref(), Some("invalid_prompt" | "bio_policy"))
{
let message = error
.message
.unwrap_or_else(|| "Invalid request.".to_string());
@@ -632,10 +633,6 @@ fn is_usage_not_included(error: &Error) -> bool {
error.code.as_deref() == Some("usage_not_included")
}
fn is_invalid_prompt_error(error: &Error) -> bool {
error.code.as_deref() == Some("invalid_prompt")
}
fn is_cyber_policy_error(error: &Error) -> bool {
error.code.as_deref() == Some("cyber_policy")
}
@@ -1076,23 +1073,42 @@ mod tests {
}
#[tokio::test]
async fn invalid_prompt_without_type_is_invalid_request() {
let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_invalid_prompt_no_type","object":"response","created_at":1759771628,"status":"failed","background":false,"error":{"code":"invalid_prompt","message":"Invalid prompt: we've limited access to this content for safety reasons."},"incomplete_details":null}}"#;
async fn content_policy_errors_without_type_are_invalid_requests() {
for (code, expected_message) in [
(
"invalid_prompt",
"Invalid prompt: we've limited access to this content for safety reasons.",
),
(
"bio_policy",
"This content was flagged for possible biological risk.",
),
] {
let raw_error = json!({
"type": "response.failed",
"sequence_number": 3,
"response": {
"id": "resp_content_policy_no_type",
"object": "response",
"created_at": 1759771628,
"status": "failed",
"background": false,
"error": { "code": code, "message": expected_message },
"incomplete_details": null,
},
})
.to_string();
let sse1 = format!("event: response.failed\ndata: {raw_error}\n\n");
let sse1 = format!("event: response.failed\ndata: {raw_error}\n\n");
let events = collect_events(&[sse1.as_bytes()]).await;
let events = collect_events(&[sse1.as_bytes()]).await;
assert_eq!(events.len(), 1);
match &events[0] {
Err(ApiError::InvalidRequest { message }) => {
assert_eq!(
message,
"Invalid prompt: we've limited access to this content for safety reasons."
);
assert_eq!(events.len(), 1);
match &events[0] {
Err(ApiError::InvalidRequest { message }) => {
assert_eq!(message, expected_message);
}
other => panic!("unexpected event for {code}: {other:?}"),
}
other => panic!("unexpected event: {other:?}"),
}
}