mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
committed by
GitHub
parent
f6e251c3ac
commit
78df1237d1
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1182,11 +1182,26 @@ async fn live_app_server_cyber_policy_error_renders_dedicated_notice() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_server_safety_access_errors_render_dedicated_notice() {
|
||||
let message = "Invalid prompt: we've limited access to this content for safety reasons.";
|
||||
for message in [
|
||||
message.to_string(),
|
||||
json!({ "error": { "message": message } }).to_string(),
|
||||
] {
|
||||
let legacy_message = "Invalid prompt: we've limited access to this content for safety reasons.";
|
||||
let bio_policy_message = "This content was flagged for possible biological risk.";
|
||||
let cases = [
|
||||
("legacy plain message", legacy_message.to_string()),
|
||||
(
|
||||
"legacy JSON message",
|
||||
json!({ "error": { "message": legacy_message } }).to_string(),
|
||||
),
|
||||
("bio policy plain message", bio_policy_message.to_string()),
|
||||
(
|
||||
"bio policy JSON message",
|
||||
json!({ "error": { "message": bio_policy_message } }).to_string(),
|
||||
),
|
||||
(
|
||||
"bio policy code",
|
||||
json!({ "error": { "code": "bio_policy", "message": "copy may change" } }).to_string(),
|
||||
),
|
||||
];
|
||||
let mut rendered_cases = Vec::new();
|
||||
for (case, message) in cases {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_non_retry_error(message, /*codex_error_info*/ None);
|
||||
|
||||
@@ -1195,8 +1210,17 @@ async fn app_server_safety_access_errors_render_dedicated_notice() {
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(rendered.contains("This content can't be shown"));
|
||||
assert!(rendered.contains("biological research"));
|
||||
assert!(!rendered.contains("Invalid prompt:"));
|
||||
rendered_cases.push((case, rendered));
|
||||
}
|
||||
|
||||
let canonical = &rendered_cases[0].1;
|
||||
for (case, rendered) in &rendered_cases[1..] {
|
||||
assert_eq!(rendered, canonical, "unexpected rendering for {case}");
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"app_server_bio_policy_error_renders_dedicated_notice",
|
||||
rendered_cases.last().unwrap().1.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
expression: rendered_cases.last().unwrap().1.as_str()
|
||||
---
|
||||
ⓘ This content can't be shown
|
||||
We take extra caution with requests involving biological research and
|
||||
applications that could pose safety risks. Eligible researchers can apply
|
||||
for Trusted Access.
|
||||
Trusted Access: https://www.openai.com/form/trusted-access-for-biology-
|
||||
research/
|
||||
Learn more: https://help.openai.com/en/articles/20001326
|
||||
@@ -5,8 +5,15 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
const SAFETY_ACCESS_BLOCK_PREFIX: &str =
|
||||
const LEGACY_SAFETY_ACCESS_BLOCK_PREFIX: &str =
|
||||
"Invalid prompt: we've limited access to this content for safety reasons.";
|
||||
const BIO_POLICY_SAFETY_ACCESS_BLOCK_PREFIX: &str =
|
||||
"This content was flagged for possible biological risk.";
|
||||
|
||||
fn is_safety_access_block_message(message: &str) -> bool {
|
||||
message.starts_with(LEGACY_SAFETY_ACCESS_BLOCK_PREFIX)
|
||||
|| message.starts_with(BIO_POLICY_SAFETY_ACCESS_BLOCK_PREFIX)
|
||||
}
|
||||
|
||||
impl ChatWidget {
|
||||
/// Synchronize the bottom-pane "task running" indicator with the current lifecycles.
|
||||
@@ -430,11 +437,12 @@ impl ChatWidget {
|
||||
.is_some_and(is_app_server_cyber_policy_error)
|
||||
{
|
||||
self.on_cyber_policy_error();
|
||||
} else if message.starts_with(SAFETY_ACCESS_BLOCK_PREFIX)
|
||||
} else if is_safety_access_block_message(&message)
|
||||
|| serde_json::from_str::<serde_json::Value>(&message).is_ok_and(|response| {
|
||||
response["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(|message| message.starts_with(SAFETY_ACCESS_BLOCK_PREFIX))
|
||||
response["error"]["code"].as_str() == Some("bio_policy")
|
||||
|| response["error"]["message"]
|
||||
.as_str()
|
||||
.is_some_and(is_safety_access_block_message)
|
||||
})
|
||||
{
|
||||
self.input_queue.submit_pending_steers_after_interrupt = false;
|
||||
|
||||
Reference in New Issue
Block a user