mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Treat inference hook denials as terminal errors (#60511)
## Why An explicit denial from a workspace inference hook should stop the request, including retries and compaction model fallback. ## What changed Recognize `inference_hook_denied` provider errors and preserve the provider's message, with a fallback when none is supplied. Expose the distinct `inferenceHookDenied` error through the app-server protocol and generated schemas and SDK types. Treat the denial as non-retryable during turns, compaction, and guardian review. ## Testing Add coverage for terminal denials over HTTP, SSE, and WebSocket, error serialization and persistence in `thread/read`, and compaction failures that preserve token usage and history without retrying or falling back to another model. GitOrigin-RevId: 2ebd480f01e52f4e6bd3db8a0f92d5814d18ccb1
This commit is contained in:
@@ -43,6 +43,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
| ApiError::InvalidAudioInput { .. }
|
||||
| ApiError::ModelNotFound { .. }
|
||||
| ApiError::InvalidPrompt { .. }
|
||||
| ApiError::InferenceHookDenied { .. }
|
||||
| ApiError::ContentPolicyViolation { .. }
|
||||
| ApiError::CyberPolicy { .. }
|
||||
| ApiError::BioPolicy { .. }
|
||||
@@ -105,6 +106,9 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
ApiError::InvalidPrompt { message } => {
|
||||
CodexErr::new(CodexErrorDetails::InvalidPrompt { message })
|
||||
}
|
||||
ApiError::InferenceHookDenied { message } => {
|
||||
CodexErr::new(CodexErrorDetails::InferenceHookDenied { message })
|
||||
}
|
||||
ApiError::ContentPolicyViolation { message, status } => {
|
||||
CodexErrorDetails::ContentPolicyViolation { message, status }.into()
|
||||
}
|
||||
|
||||
@@ -335,6 +335,12 @@ fn map_api_error_uses_cyber_policy_fallback_for_missing_message() {
|
||||
fn map_api_error_preserves_typed_errors() {
|
||||
let message = "This request was rejected.";
|
||||
for (error, expected_info) in [
|
||||
(
|
||||
ApiError::InferenceHookDenied {
|
||||
message: message.to_string(),
|
||||
},
|
||||
CodexErrorInfo::InferenceHookDenied,
|
||||
),
|
||||
(
|
||||
ApiError::BioPolicy {
|
||||
message: message.to_string(),
|
||||
@@ -357,22 +363,31 @@ fn map_api_error_preserves_typed_errors() {
|
||||
|
||||
#[test]
|
||||
fn map_api_error_maps_http_and_wrapped_websocket_typed_errors() {
|
||||
for (code, expected_info, fallback) in [
|
||||
for (status, code, expected_info, fallback) in [
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"content_policy_violation",
|
||||
CodexErrorInfo::InvalidPrompt,
|
||||
"The input was rejected by the content policy.",
|
||||
),
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"bio_policy",
|
||||
CodexErrorInfo::BioPolicy,
|
||||
"This content was flagged for possible biological risk.",
|
||||
),
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid_prompt",
|
||||
CodexErrorInfo::InvalidPrompt,
|
||||
"Invalid request.",
|
||||
),
|
||||
(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
"inference_hook_denied",
|
||||
CodexErrorInfo::InferenceHookDenied,
|
||||
"Your workspace inference hook blocked this request.",
|
||||
),
|
||||
] {
|
||||
for wrapped in [false, true] {
|
||||
for (message, expected) in [
|
||||
@@ -390,11 +405,11 @@ fn map_api_error_maps_http_and_wrapped_websocket_typed_errors() {
|
||||
}
|
||||
if wrapped {
|
||||
body["type"] = serde_json::json!("error");
|
||||
body["status"] = serde_json::json!(400);
|
||||
body["status"] = serde_json::json!(status.as_u16());
|
||||
}
|
||||
let err = map_api_error(ApiError::Transport(TransportError::Http {
|
||||
retry_after: None,
|
||||
status: http::StatusCode::BAD_REQUEST,
|
||||
status,
|
||||
url: None,
|
||||
headers: None,
|
||||
body: Some(body.to_string()),
|
||||
|
||||
@@ -57,6 +57,8 @@ pub enum ApiError {
|
||||
#[error("invalid prompt: {message}")]
|
||||
InvalidPrompt { message: String },
|
||||
#[error("{message}")]
|
||||
InferenceHookDenied { message: String },
|
||||
#[error("{message}")]
|
||||
ContentPolicyViolation {
|
||||
message: String,
|
||||
status: Option<StatusCode>,
|
||||
@@ -96,6 +98,13 @@ pub(crate) fn parse_terminal_provider_error(
|
||||
.and_then(Value::as_str)
|
||||
.filter(|message| !message.trim().is_empty());
|
||||
match (code, status) {
|
||||
("inference_hook_denied", None | Some(StatusCode::FORBIDDEN)) => {
|
||||
Some(ApiError::InferenceHookDenied {
|
||||
message: message
|
||||
.unwrap_or("Your workspace inference hook blocked this request.")
|
||||
.to_owned(),
|
||||
})
|
||||
}
|
||||
("context_length_exceeded", None | Some(StatusCode::BAD_REQUEST)) => {
|
||||
Some(ApiError::ContextWindowExceeded)
|
||||
}
|
||||
|
||||
@@ -683,6 +683,7 @@ async fn process_sse_with_treatment(
|
||||
| ApiError::ContextWindowExceeded
|
||||
| ApiError::InvalidRequest { .. }
|
||||
| ApiError::SessionBudgetExceeded
|
||||
| ApiError::InferenceHookDenied { .. }
|
||||
| ApiError::AccessProgramNotEnabled { .. }
|
||||
| ApiError::ContentPolicyViolation { .. }
|
||||
| ApiError::ResponseIncomplete(_)
|
||||
|
||||
@@ -115,6 +115,7 @@ async fn terminal_failure_with_retry_header_ends_stream_immediately() {
|
||||
for (code, expected) in [
|
||||
("flex_unavailable", CodexErrorInfo::FlexUnavailable),
|
||||
("budget_reached", CodexErrorInfo::SessionBudgetExceeded),
|
||||
("inference_hook_denied", CodexErrorInfo::InferenceHookDenied),
|
||||
(
|
||||
"context_length_exceeded",
|
||||
CodexErrorInfo::ContextWindowExceeded,
|
||||
|
||||
Reference in New Issue
Block a user