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:
ccui-oai
2026-11-10 03:21:26 +00:00
committed by copyberry
parent 1099fe0694
commit 0811df3976
43 changed files with 292 additions and 51 deletions
@@ -729,6 +729,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -8925,6 +8925,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -4706,6 +4706,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -25,6 +25,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -127,6 +127,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -208,6 +208,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -208,6 +208,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -208,6 +208,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -127,6 +127,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -130,6 +130,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -127,6 +127,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -127,6 +127,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -127,6 +127,13 @@
],
"type": "string"
},
{
"description": "The workspace's inference hook explicitly denied this request.",
"enum": [
"inferenceHookDenied"
],
"type": "string"
},
{
"additionalProperties": false,
"properties": {
@@ -9,4 +9,4 @@ import type { NonSteerableTurnKind } from "./NonSteerableTurnKind";
* When an upstream HTTP status is available (for example, from the Responses API or a provider),
* it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.
*/
export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "rateLimitExceeded" | "flexUnavailable" | "serverOverloaded" | "modelNotFound" | "policyCapacityUnavailable" | "cyberPolicy" | "misalignmentPolicyViolation" | "tooManyDenials" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other" | string | { [key: string]: unknown };
export type CodexErrorInfo = "contextWindowExceeded" | "sessionBudgetExceeded" | "usageLimitExceeded" | "rateLimitExceeded" | "flexUnavailable" | "serverOverloaded" | "modelNotFound" | "policyCapacityUnavailable" | "inferenceHookDenied" | "cyberPolicy" | "misalignmentPolicyViolation" | "tooManyDenials" | { "httpConnectionFailed": { httpStatusCode: number | null, } } | { "responseStreamConnectionFailed": { httpStatusCode: number | null, } } | "internalServerError" | "unauthorized" | "badRequest" | "threadRollbackFailed" | "sandboxError" | { "responseStreamDisconnected": { httpStatusCode: number | null, } } | { "responseTooManyFailedAttempts": { httpStatusCode: number | null, } } | { "activeTurnNotSteerable": { turnKind: NonSteerableTurnKind, } } | "other" | string | { [key: string]: unknown };
@@ -83,6 +83,8 @@ pub enum CodexErrorInfo {
ModelNotFound,
// Capacity is unavailable on a route restricted by server policy.
PolicyCapacityUnavailable,
/// The workspace's inference hook explicitly denied this request.
InferenceHookDenied,
CyberPolicy,
MisalignmentPolicyViolation,
TooManyDenials,
@@ -163,6 +165,7 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
CodexErrorInfo::PolicyCapacityUnavailable
}
CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy,
CoreCodexErrorInfo::InferenceHookDenied => CodexErrorInfo::InferenceHookDenied,
CoreCodexErrorInfo::BioPolicy => CodexErrorInfo::Other,
CoreCodexErrorInfo::MisalignmentPolicyViolation => {
CodexErrorInfo::MisalignmentPolicyViolation
@@ -4907,6 +4907,10 @@ fn core_error_info_converts_to_camel_case() {
json!("policyCapacityUnavailable"),
),
(CoreCodexErrorInfo::CyberPolicy, json!("cyberPolicy")),
(
CoreCodexErrorInfo::InferenceHookDenied,
json!("inferenceHookDenied"),
),
(CoreCodexErrorInfo::BioPolicy, json!("other")),
(CoreCodexErrorInfo::InvalidPrompt, json!("other")),
(
@@ -4918,9 +4922,11 @@ fn core_error_info_converts_to_camel_case() {
json!("rateLimitExceeded"),
),
] {
let converted = CodexErrorInfo::from(core);
assert_eq!(serde_json::to_value(&converted).unwrap(), expected);
assert_eq!(
serde_json::to_value(CodexErrorInfo::from(core)).unwrap(),
expected
serde_json::from_value::<CodexErrorInfo>(expected).unwrap(),
converted
);
}
}
@@ -2818,15 +2818,17 @@ async fn thread_items_read_validates_ids_before_history_access() -> Result<()> {
Ok(())
}
#[test_case::test_case("model_not_found", CodexErrorInfo::ModelNotFound, "Access to this deployment ended."; "model_not_found")]
#[test_case::test_case("inference_hook_denied", CodexErrorInfo::InferenceHookDenied, "Blocked by the workspace hook."; "inference_hook_denied")]
#[tokio::test]
async fn thread_read_preserves_model_not_found_after_failed_turn() -> Result<()> {
async fn thread_read_preserves_provider_error_after_failed_turn(
code: &str,
expected: CodexErrorInfo,
message: &str,
) -> Result<()> {
let server = responses::start_mock_server().await;
let message = "Access to this deployment ended.";
let _response_mock = responses::mount_sse_once(
&server,
responses::sse_failed("resp-1", "model_not_found", message),
)
.await;
let response_mock =
responses::mount_sse_once(&server, responses::sse_failed("resp-1", code, message)).await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
@@ -2858,10 +2860,7 @@ async fn thread_read_preserves_model_not_found_after_failed_turn() -> Result<()>
let live: ErrorNotification =
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("error")).await??;
assert!(!live.will_retry);
assert_eq!(
live.error.codex_error_info,
Some(CodexErrorInfo::ModelNotFound)
);
assert_eq!(live.error.codex_error_info, Some(expected));
assert_eq!(live.error.message, message);
timeout(
DEFAULT_READ_TIMEOUT,
@@ -2883,6 +2882,7 @@ async fn thread_read_preserves_model_not_found_after_failed_turn() -> Result<()>
thread.turns.last().and_then(|turn| turn.error.as_ref()),
Some(&live.error)
);
assert_eq!(response_mock.requests().len(), 1);
Ok(())
}
+3
View File
@@ -2534,6 +2534,9 @@ fn websocket_error_detail(err: &ApiError) -> String {
"handshake error: model not found: [redacted]".to_string()
}
ApiError::InvalidPrompt { .. } => "handshake error: invalid prompt: [redacted]".to_string(),
ApiError::InferenceHookDenied { .. } => {
"handshake error: inference hook denied: [redacted]".to_string()
}
ApiError::ContentPolicyViolation { .. } => {
"handshake error: content policy violation".to_string()
}
+4
View File
@@ -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()
}
+18 -3
View File
@@ -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()),
+9
View File
@@ -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)
}
+1
View File
@@ -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,
@@ -17,6 +17,7 @@ pub(crate) fn should_retry_with_current_model(
CodexErrorDetails::TurnAborted
| CodexErrorDetails::Interrupted
| CodexErrorDetails::SessionBudgetExceeded
| CodexErrorDetails::InferenceHookDenied { .. }
| CodexErrorDetails::WorkspaceAccessDenied(_)
| CodexErrorDetails::ContentPolicyViolation { .. }
| CodexErrorDetails::ResponseIncomplete(_)
+3 -1
View File
@@ -2036,7 +2036,9 @@ async fn run_sampling_request(
}
return Err(err);
}
CodexErrorDetails::SessionBudgetExceeded | CodexErrorDetails::Fatal(_) => {
CodexErrorDetails::SessionBudgetExceeded
| CodexErrorDetails::InferenceHookDenied { .. }
| CodexErrorDetails::Fatal(_) => {
return Err(err);
}
_ => err,
+79 -33
View File
@@ -1200,36 +1200,78 @@ async fn explicit_denial_stops_compaction_on_model_downshift(
Ok(())
}
#[test_case::test_case(json!({"input_tokens": 120_000, "output_tokens": 30_000, "total_tokens": 150_000}); "reported_usage")]
#[test_case::test_case(Value::Null; "missing_usage")]
#[test_case::test_case(
json!({
"type": "response.incomplete",
"response": {
"id": "incomplete-compaction",
"incomplete_details": {"reason": "max_output_tokens"},
"usage": {"input_tokens": 120_000, "output_tokens": 30_000, "total_tokens": 150_000}
}
}),
"Incomplete response returned, reason: max_output_tokens",
CodexErrorInfo::Other,
270_000;
"incomplete_reported_usage"
)]
#[test_case::test_case(
json!({
"type": "response.incomplete",
"response": {
"id": "incomplete-compaction",
"incomplete_details": {"reason": "max_output_tokens"},
"usage": null
}
}),
"Incomplete response returned, reason: max_output_tokens",
CodexErrorInfo::Other,
120_000;
"incomplete_missing_usage"
)]
#[test_case::test_case(
json!({
"type": "error",
"error": {"code": "inference_hook_denied", "message": "Blocked by the workspace hook."}
}),
"Blocked by the workspace hook.",
CodexErrorInfo::InferenceHookDenied,
120_000;
"inference_hook_denied_error"
)]
#[test_case::test_case(
json!({
"type": "response.failed",
"response": {
"id": "denied-compaction",
"error": {"code": "inference_hook_denied", "message": "Blocked by the workspace hook."}
}
}),
"Blocked by the workspace hook.",
CodexErrorInfo::InferenceHookDenied,
120_000;
"inference_hook_denied_response_failed"
)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn incomplete_response_during_compaction_is_terminal_and_counted(
reported_usage: Value,
async fn terminal_compaction_failure_preserves_usage_and_history(
failure_event: Value,
expected_error: &str,
expected_error_info: CodexErrorInfo,
expected_total_tokens: i64,
) -> Result<()> {
skip_if_no_network!(Ok(()));
for (provider, expected_error) in [
for (provider, error_prefix) in [
(
non_openai_model_provider as fn(&MockServer) -> ModelProviderInfo,
"Incomplete response returned, reason: max_output_tokens",
),
(
openai_model_provider,
"Error running remote compact task: Incomplete response returned, reason: max_output_tokens",
"",
),
(openai_model_provider, "Error running remote compact task: "),
] {
let server = start_mock_server().await;
let previous_model = "gpt-5.4";
let selected_model = "gpt-5.2";
let incomplete = sse(vec![
ev_assistant_message("discarded", "DISCARDED_INCOMPLETE_SUMMARY"),
json!({
"type": "response.incomplete",
"response": {
"id": "incomplete-compaction",
"incomplete_details": {"reason": "max_output_tokens"},
"usage": reported_usage
}
}),
let failure = sse(vec![
ev_assistant_message("discarded", "DISCARDED_FAILED_SUMMARY"),
failure_event.clone(),
]);
let response_mock = mount_sse_sequence(
&server,
@@ -1238,8 +1280,8 @@ async fn incomplete_response_during_compaction_is_terminal_and_counted(
ev_assistant_message("initial", "Initial reply."),
ev_completed_with_tokens("initial", /*total_tokens*/ 120_000),
]),
incomplete.clone(),
incomplete,
failure.clone(),
failure,
],
)
.await;
@@ -1270,7 +1312,7 @@ async fn incomplete_response_during_compaction_is_terminal_and_counted(
// includes request overhead that the local history estimate does not count.
test.submit_turn(&"x".repeat(440_000)).await?;
// A smaller selected model forces compaction with the previous model.
// An incomplete result must stop both retries and fallback to the selected model.
// A terminal failure must stop both retries and fallback to the selected model.
test.codex
.start_or_steer_turn(
TurnInputRequest::user_input(vec![UserInput::Text {
@@ -1293,29 +1335,33 @@ async fn incomplete_response_during_compaction_is_terminal_and_counted(
let mut errors = Vec::new();
wait_for_event(&test.codex, |event| {
match event {
EventMsg::Error(error) => errors.push(error.message.clone()),
EventMsg::Error(error) => {
errors.push((error.message.clone(), error.codex_error_info.clone()));
}
EventMsg::StreamError(value) => panic!("unexpected compaction retry: {value:?}"),
EventMsg::RawResponseCompleted(value) => {
panic!("incomplete response marked complete: {value:?}")
panic!("failed response marked complete: {value:?}")
}
_ => {}
}
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(errors, vec![expected_error]);
assert_eq!(
errors,
vec![(
format!("{error_prefix}{expected_error}"),
Some(expected_error_info.clone())
)]
);
assert_eq!(
test.codex
.token_usage_info()
.await
.expect("partial usage")
.expect("token usage")
.total_token_usage
.total_tokens,
if reported_usage.is_null() {
120_000
} else {
270_000
},
expected_total_tokens,
);
assert_eq!(
response_mock
@@ -1346,7 +1392,7 @@ async fn incomplete_response_during_compaction_is_terminal_and_counted(
);
assert!(!body_contains_text(
&requests[2].body_json().to_string(),
"DISCARDED_INCOMPLETE_SUMMARY",
"DISCARDED_FAILED_SUMMARY",
));
}
Ok(())
+2
View File
@@ -1446,6 +1446,7 @@ async fn assert_terminal_failure(
#[test_case::test_case("flex_unavailable", 429, CodexErrorInfo::FlexUnavailable, "Flex capacity unavailable."; "flex")]
#[test_case::test_case("model_not_found", 404, CodexErrorInfo::ModelNotFound, "Access to this deployment ended."; "model")]
#[test_case::test_case("inference_hook_denied", 403, CodexErrorInfo::InferenceHookDenied, "Blocked by the workspace hook."; "inference_hook_denied")]
#[tokio::test(flavor = "current_thread")]
async fn http_and_sse_provider_errors_are_terminal(
code: &str,
@@ -1503,6 +1504,7 @@ async fn http_and_sse_provider_errors_are_terminal(
#[test_case::test_case("flex_unavailable", 429, CodexErrorInfo::FlexUnavailable, "Flex capacity unavailable."; "flex")]
#[test_case::test_case("model_not_found", 404, CodexErrorInfo::ModelNotFound, "Access to this deployment ended."; "model")]
#[test_case::test_case("inference_hook_denied", 403, CodexErrorInfo::InferenceHookDenied, "Blocked by the workspace hook."; "inference_hook_denied")]
#[tokio::test(flavor = "current_thread")]
async fn websocket_provider_errors_are_terminal(
code: &str,
@@ -197,6 +197,7 @@ fn should_retry_guardian_review(outcome: &GuardianReviewOutcome) -> bool {
CodexErrorInfo::ContextWindowExceeded
| CodexErrorInfo::SessionBudgetExceeded
| CodexErrorInfo::UsageLimitExceeded
| CodexErrorInfo::InferenceHookDenied
| CodexErrorInfo::CyberPolicy
| CodexErrorInfo::BioPolicy
| CodexErrorInfo::MisalignmentPolicyViolation
@@ -114,6 +114,13 @@ fn guardian_review_retry_only_retries_recoverable_errors() {
)),
false,
),
(
GuardianReviewOutcome::Error(GuardianReviewError::session_with_error_info(
anyhow::anyhow!("workspace inference hook denied the request"),
CodexErrorInfo::InferenceHookDenied,
)),
false,
),
(
GuardianReviewSessionOutcome::Completed(Ok(Some(GuardianReviewResponse::new(
"invalid assessment".to_owned(),
@@ -61,6 +61,7 @@ pub(super) fn sampler_failure_reason(error: &LunaSamplerError) -> &'static str {
ApiError::ContentPolicyViolation { .. } => "content_policy_violation",
ApiError::CyberPolicy { .. }
| ApiError::BioPolicy { .. }
| ApiError::InferenceHookDenied { .. }
| ApiError::MisalignmentPolicyViolation { .. } => "policy_error",
ApiError::ServerOverloaded { .. } => "server_overloaded",
ApiError::PolicyCapacityUnavailable { .. } => "policy_capacity_unavailable",
@@ -163,6 +163,7 @@ impl SamplingExecution {
| ApiError::InvalidAudioInput { .. }
| ApiError::ModelNotFound { .. }
| ApiError::InvalidPrompt { .. }
| ApiError::InferenceHookDenied { .. }
| ApiError::AccessProgramNotEnabled { .. }
| ApiError::ContentPolicyViolation { .. }
| ApiError::MisalignmentPolicyViolation { .. }
@@ -26,6 +26,7 @@ enum CodexErrorInfoWire {
ModelNotFound,
/// Capacity is unavailable on a route restricted by server policy.
PolicyCapacityUnavailable,
InferenceHookDenied,
CyberPolicy,
BioPolicy,
MisalignmentPolicyViolation,
+5
View File
@@ -173,6 +173,9 @@ pub enum CodexErrorDetails {
},
#[error("{message}")]
InvalidPrompt { message: String },
/// The workspace's inference hook explicitly denied this request.
#[error("{message}")]
InferenceHookDenied { message: String },
/// The provider rejected the input under its content policy.
#[error("{message}")]
ContentPolicyViolation {
@@ -463,6 +466,7 @@ impl CodexErr {
| CodexErrorDetails::ResponseIncomplete(_)
| CodexErrorDetails::ModelNotFound { .. }
| CodexErrorDetails::InvalidPrompt { .. }
| CodexErrorDetails::InferenceHookDenied { .. }
| CodexErrorDetails::ContentPolicyViolation { .. }
| CodexErrorDetails::ToolCollision(_)
| CodexErrorDetails::RefreshTokenFailed(_)
@@ -544,6 +548,7 @@ impl CodexErr {
CodexErrorInfo::PolicyCapacityUnavailable
}
CodexErrorDetails::FlexUnavailable => CodexErrorInfo::FlexUnavailable,
CodexErrorDetails::InferenceHookDenied { .. } => CodexErrorInfo::InferenceHookDenied,
CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy,
CodexErrorDetails::BioPolicy { .. } => CodexErrorInfo::BioPolicy,
CodexErrorDetails::ModelNotFound { .. } => CodexErrorInfo::ModelNotFound,
+3
View File
@@ -2106,6 +2106,8 @@ pub enum CodexErrorInfo {
ModelNotFound,
/// Capacity is unavailable on a route restricted by server policy.
PolicyCapacityUnavailable,
/// The workspace's inference hook explicitly denied this request.
InferenceHookDenied,
CyberPolicy,
BioPolicy,
MisalignmentPolicyViolation,
@@ -2153,6 +2155,7 @@ impl CodexErrorInfo {
| Self::ServerOverloaded
| Self::ModelNotFound
| Self::PolicyCapacityUnavailable
| Self::InferenceHookDenied
| Self::CyberPolicy
| Self::BioPolicy
| Self::MisalignmentPolicyViolation
@@ -99,6 +99,7 @@ pub fn telemetry_api_error_message(error: &ApiError) -> String {
| ApiError::InvalidAudioInput { .. }
| ApiError::InvalidPrompt { .. } => "invalid request".to_string(),
ApiError::CyberPolicy { .. } => "cyber policy".to_string(),
ApiError::InferenceHookDenied { .. } => "inference hook denied".to_string(),
ApiError::BioPolicy { .. } => "bio policy".to_string(),
ApiError::MisalignmentPolicyViolation { .. } => "misalignment policy violation".to_string(),
ApiError::ServerOverloaded { .. } => "server overloaded".to_string(),
@@ -8376,6 +8376,7 @@ class ActiveTurnNotSteerableCodexErrorInfo(BaseModel):
class CodexErrorInfo(
RootModel[
CodexErrorInfoValue
| Literal["inferenceHookDenied"]
| HttpConnectionFailedCodexErrorInfo
| ResponseStreamConnectionFailedCodexErrorInfo
| ResponseStreamDisconnectedCodexErrorInfo
@@ -8386,6 +8387,7 @@ class CodexErrorInfo(
):
root: Annotated[
CodexErrorInfoValue
| Literal["inferenceHookDenied"]
| HttpConnectionFailedCodexErrorInfo
| ResponseStreamConnectionFailedCodexErrorInfo
| ResponseStreamDisconnectedCodexErrorInfo