Compare commits

...

1 Commits

Author SHA1 Message Date
Fouad Matin
6046c049f0 fix(codex-api): safety check message 2026-04-17 04:00:46 -07:00
4 changed files with 68 additions and 0 deletions

View File

@@ -1,6 +1,8 @@
use crate::AuthProvider as ApiAuthProvider;
use crate::TransportError;
use crate::error::ApiError;
use crate::error::CYBER_SAFETY_BLOCK_ADVICE;
use crate::error::is_cyber_safety_block;
use crate::rate_limits::parse_promo_message;
use crate::rate_limits::parse_rate_limit_for_limit;
use base64::Engine;
@@ -60,6 +62,8 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
.contains("The image data you provided does not represent a valid image")
{
CodexErr::InvalidImageRequest()
} else if body_error_code_is_cyber_safety_block(&body_text) {
CodexErr::InvalidRequest(CYBER_SAFETY_BLOCK_ADVICE.to_string())
} else {
CodexErr::InvalidRequest(body_text)
}
@@ -161,6 +165,17 @@ fn extract_x_error_json_code(headers: Option<&HeaderMap>) -> Option<String> {
.map(str::to_string)
}
fn body_error_code_is_cyber_safety_block(body_text: &str) -> bool {
let Ok(body) = serde_json::from_str::<Value>(body_text) else {
return false;
};
let error_code = body
.get("error")
.and_then(|error| error.get("code"))
.and_then(Value::as_str);
is_cyber_safety_block(error_code)
}
#[derive(Debug, Deserialize)]
struct UsageErrorResponse {
error: UsageErrorBody,

View File

@@ -26,6 +26,29 @@ fn map_api_error_maps_server_overloaded_from_503_body() {
assert!(matches!(err, CodexErr::ServerOverloaded));
}
#[test]
fn map_api_error_maps_cyber_safety_bad_request_to_guidance() {
let body = serde_json::json!({
"error": {
"code": "cyber_security",
"message": "ProtectionClient blocked request",
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::InvalidRequest(message) = err else {
panic!("expected CodexErr::InvalidRequest, got {err:?}");
};
assert_eq!(message, CYBER_SAFETY_BLOCK_ADVICE);
assert!(!CodexErr::InvalidRequest(message).is_retryable());
}
#[test]
fn map_api_error_maps_usage_limit_limit_name_header() {
let mut headers = HeaderMap::new();

View File

@@ -4,6 +4,12 @@ use http::StatusCode;
use std::time::Duration;
use thiserror::Error;
pub(crate) const CYBER_SAFETY_BLOCK_ADVICE: &str = "This request has been flagged for potentially high-risk cyber activity. Apply for trusted access: https://chatgpt.com/cyber to avoid future blocks,\n /fork or edit the last message and try again, or let us know with /feedback if you think this is a safety check false positive.";
pub(crate) fn is_cyber_safety_block(error_code: Option<&str>) -> bool {
error_code.is_some_and(|code| code.to_ascii_lowercase().contains("cyber"))
}
#[derive(Debug, Error)]
pub enum ApiError {
#[error(transparent)]

View File

@@ -1,6 +1,8 @@
use crate::common::ResponseEvent;
use crate::common::ResponseStream;
use crate::error::ApiError;
use crate::error::CYBER_SAFETY_BLOCK_ADVICE;
use crate::error::is_cyber_safety_block;
use crate::rate_limits::parse_all_rate_limits;
use crate::telemetry::SseTelemetry;
use codex_client::ByteStream;
@@ -283,6 +285,10 @@ pub fn process_responses_event(
response_error = ApiError::QuotaExceeded;
} else if is_usage_not_included(&error) {
response_error = ApiError::UsageNotIncluded;
} else if is_cyber_safety_block(error.code.as_deref()) {
response_error = ApiError::InvalidRequest {
message: CYBER_SAFETY_BLOCK_ADVICE.to_string(),
};
} else if is_invalid_prompt_error(&error) {
let message = error
.message
@@ -817,6 +823,24 @@ mod tests {
}
}
#[tokio::test]
async fn cyber_safety_block_is_invalid_request_with_guidance() {
let raw_error = r#"{"type":"response.failed","sequence_number":3,"response":{"id":"resp_cyber_safety","object":"response","created_at":1759771629,"status":"failed","background":false,"error":{"code":"cyber_security","message":"This request has been flagged for potentially high-risk cyber activity. Learn more here: https://platform.openai.com/docs/guides/safety-checks/cybersecurity"},"incomplete_details":null}}"#;
let sse1 = format!("event: response.failed\ndata: {raw_error}\n\n");
let events = collect_events(&[sse1.as_bytes()]).await;
assert_eq!(events.len(), 1);
match &events[0] {
Err(ApiError::InvalidRequest { message }) => {
assert_eq!(message, CYBER_SAFETY_BLOCK_ADVICE);
}
other => panic!("unexpected event: {other:?}"),
}
}
#[tokio::test]
async fn table_driven_event_kinds() {
struct TestCase {