From 559264d92e4462e887d7508c705599f33daf4f1e Mon Sep 17 00:00:00 2001 From: Rennie Date: Tue, 22 Sep 2026 19:51:58 +0000 Subject: [PATCH] Preserve `invalid_prompt` as a distinct error classification (#47353) ## Why `invalid_prompt` responses were classified as generic invalid requests, losing the server's specific error classification. ## What changed Add `InvalidPrompt` variants to the API and core error types and serialize the core protocol classification as `invalid_prompt`. Recognize the code in HTTP 400 responses, wrapped WebSocket errors, and SSE `response.failed` events while preserving server messages and keeping the error non-retryable. Map it to `other` in the app-server v2 protocol. ## Testing Extend coverage for typed error mapping, missing and blank messages, protocol conversion, and guardian retry handling. Exercise HTTP and SSE failures in core integration tests, asserting the error message, serialized classification, and a single request without retry. GitOrigin-RevId: 5d9a299a013b06158a4dc27a4054f3dbc5fd0f4d --- .../app-server-exports-stable.json.zst | Bin 155265 -> 155272 bytes .../src/protocol/v2/shared.rs | 1 + .../src/protocol/v2/tests.rs | 1 + codex-rs/cli/src/doctor.rs | 1 + codex-rs/codex-api/src/api_bridge.rs | 16 +- codex-rs/codex-api/src/api_bridge_tests.rs | 138 +++++++++++------- codex-rs/codex-api/src/error.rs | 2 + codex-rs/codex-api/src/sse/responses.rs | 50 ++++--- .../tests/suite/safety_check_downgrade.rs | 72 +++++---- codex-rs/ext/guardian-reviewer/src/retry.rs | 1 + .../ext/guardian-reviewer/src/retry_tests.rs | 7 + .../guardian-v2/src/async_scorer/metrics.rs | 2 +- .../src/async_scorer/sampler/execution.rs | 1 + codex-rs/protocol/src/codex_error_info.rs | 1 + codex-rs/protocol/src/error.rs | 4 + codex-rs/protocol/src/protocol.rs | 2 + codex-rs/response-debug-context/src/lib.rs | 4 +- 17 files changed, 194 insertions(+), 109 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 7e34c1d7535ba356acb60c3aff424ce202d1dfd5..e9b07c3ca4f4aa51f192f60b7cddfd7cdce56d4e 100644 GIT binary patch delta 20342 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 ZL?{>SqawS8LCO`Z6Y54gtfAjE#1@;T4MG3_ delta 20340 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 for CodexErrorInfo { CoreCodexErrorInfo::InternalServerError => CodexErrorInfo::InternalServerError, CoreCodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized, CoreCodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest, + CoreCodexErrorInfo::InvalidPrompt => CodexErrorInfo::Other, CoreCodexErrorInfo::ThreadRollbackFailed => CodexErrorInfo::ThreadRollbackFailed, CoreCodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError, CoreCodexErrorInfo::ResponseStreamDisconnected { http_status_code } => { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 99f7003fe6..7b8f7913e7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -4729,6 +4729,7 @@ fn core_error_info_converts_to_camel_case() { for (core, expected) in [ (CoreCodexErrorInfo::CyberPolicy, json!("cyberPolicy")), (CoreCodexErrorInfo::BioPolicy, json!("other")), + (CoreCodexErrorInfo::InvalidPrompt, json!("other")), ( CoreCodexErrorInfo::RateLimitExceeded, json!("rateLimitExceeded"), diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 444d6e4929..6cc337a881 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2436,6 +2436,7 @@ fn websocket_error_detail(err: &ApiError) -> String { | ApiError::RateLimitExceeded { .. } | ApiError::RateLimit(_) | ApiError::InvalidRequest { .. } + | ApiError::InvalidPrompt { .. } | ApiError::CyberPolicy { .. } | ApiError::BioPolicy { .. } | ApiError::MisalignmentPolicyViolation { .. } diff --git a/codex-rs/codex-api/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs index d6ff0270ba..15c9cb9245 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -53,6 +53,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr { }) } ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message), + ApiError::InvalidPrompt { message } => { + CodexErr::new(CodexErrorDetails::InvalidPrompt { message }) + } ApiError::CyberPolicy { message } => { CodexErr::new(CodexErrorDetails::CyberPolicy { message }) } @@ -118,11 +121,16 @@ pub fn map_api_error(err: ApiError) -> CodexErr { if status == http::StatusCode::BAD_REQUEST { if let Ok(parsed) = serde_json::from_str::(&body_text) && let Some(error) = parsed.get("error") - && let Some(code @ (CYBER_POLICY_ERROR_CODE | BIO_POLICY_ERROR_CODE)) = - error.get("code").and_then(Value::as_str) + && let Some( + code @ (CYBER_POLICY_ERROR_CODE + | BIO_POLICY_ERROR_CODE + | INVALID_PROMPT_ERROR_CODE), + ) = error.get("code").and_then(Value::as_str) { let fallback_message = if code == BIO_POLICY_ERROR_CODE { BIO_POLICY_FALLBACK_MESSAGE + } else if code == INVALID_PROMPT_ERROR_CODE { + INVALID_PROMPT_FALLBACK_MESSAGE } else { CYBER_POLICY_FALLBACK_MESSAGE }; @@ -134,6 +142,8 @@ pub fn map_api_error(err: ApiError) -> CodexErr { .unwrap_or_else(|| fallback_message.to_string()); if code == BIO_POLICY_ERROR_CODE { CodexErr::new(CodexErrorDetails::BioPolicy { message }) + } else if code == INVALID_PROMPT_ERROR_CODE { + CodexErr::new(CodexErrorDetails::InvalidPrompt { message }) } else { CodexErr::new(CodexErrorDetails::CyberPolicy { message }) } @@ -234,6 +244,8 @@ const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id"; const CF_RAY_HEADER: &str = "cf-ray"; const X_OPENAI_AUTHORIZATION_ERROR_HEADER: &str = "x-openai-authorization-error"; const X_ERROR_JSON_HEADER: &str = "x-error-json"; +const INVALID_PROMPT_ERROR_CODE: &str = "invalid_prompt"; +const INVALID_PROMPT_FALLBACK_MESSAGE: &str = "Invalid request."; const CYBER_POLICY_ERROR_CODE: &str = "cyber_policy"; const CYBER_POLICY_FALLBACK_MESSAGE: &str = "This request has been flagged for possible cybersecurity risk."; diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index 595232d2d9..660262ea97 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -185,48 +185,72 @@ fn map_api_error_uses_cyber_policy_fallback_for_missing_message() { } #[test] -fn map_api_error_preserves_bio_policy() { - let err = map_api_error(ApiError::BioPolicy { - message: "This request was blocked by bio policy.".to_string(), - }); - assert_eq!(err.to_codex_protocol_error(), CodexErrorInfo::BioPolicy); - assert_eq!(err.to_string(), "This request was blocked by bio policy."); - assert_eq!(err.retry_delay(/*retry_count*/ 1), None); +fn map_api_error_preserves_typed_errors() { + let message = "This request was rejected."; + for (error, expected_info) in [ + ( + ApiError::BioPolicy { + message: message.to_string(), + }, + CodexErrorInfo::BioPolicy, + ), + ( + ApiError::InvalidPrompt { + message: message.to_string(), + }, + CodexErrorInfo::InvalidPrompt, + ), + ] { + let err = map_api_error(error); + assert_eq!(err.to_codex_protocol_error(), expected_info); + assert_eq!(err.to_string(), message); + assert_eq!(err.retry_delay(/*retry_count*/ 1), None); + } } #[test] -fn map_api_error_maps_http_and_wrapped_websocket_bio_policy() { - for wrapped in [false, true] { - for message in [ - Some("This request was blocked by bio policy."), - None, - Some(""), - Some(" "), - ] { - let mut body = serde_json::json!({"error": {"code": "bio_policy"}}); - if let Some(message) = message { - body["error"]["message"] = serde_json::json!(message); - } - if wrapped { - body["type"] = serde_json::json!("error"); - body["status"] = serde_json::json!(400); - } - let err = map_api_error(ApiError::Transport(TransportError::Http { - status: http::StatusCode::BAD_REQUEST, - url: None, - headers: None, - body: Some(body.to_string()), - })); +fn map_api_error_maps_http_and_wrapped_websocket_typed_errors() { + for (code, expected_info, fallback) in [ + ( + "bio_policy", + CodexErrorInfo::BioPolicy, + "This content was flagged for possible biological risk.", + ), + ( + "invalid_prompt", + CodexErrorInfo::InvalidPrompt, + "Invalid request.", + ), + ] { + for wrapped in [false, true] { + for (message, expected) in [ + ( + Some("This request was rejected."), + "This request was rejected.", + ), + (None, fallback), + (Some(""), fallback), + (Some(" "), fallback), + ] { + let mut body = serde_json::json!({"error": {"code": code}}); + if let Some(message) = message { + body["error"]["message"] = serde_json::json!(message); + } + if wrapped { + body["type"] = serde_json::json!("error"); + body["status"] = serde_json::json!(400); + } + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::BAD_REQUEST, + url: None, + headers: None, + body: Some(body.to_string()), + })); - let expected = message - .filter(|message| !message.trim().is_empty()) - .unwrap_or("This content was flagged for possible biological risk."); - let CodexErrorDetails::BioPolicy { message } = err.details() else { - panic!("expected CodexErrorDetails::BioPolicy, got {err:?}"); - }; - assert_eq!(message, expected); - assert_eq!(err.to_codex_protocol_error(), CodexErrorInfo::BioPolicy); - assert_eq!(err.retry_delay(/*retry_count*/ 1), None); + assert_eq!(err.to_string(), expected); + assert_eq!(err.to_codex_protocol_error(), expected_info); + assert_eq!(err.retry_delay(/*retry_count*/ 1), None); + } } } } @@ -359,25 +383,27 @@ fn map_api_error_preserves_misalignment_details_from_wrapped_websocket_error() { } #[test] -fn map_api_error_keeps_unknown_400_errors_generic() { - let body = serde_json::json!({ - "error": { - "message": "Some other bad request.", - "code": "some_other_policy" - } - }) - .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.clone()), - })); +fn map_api_error_keeps_other_400_errors_generic() { + for code in ["invalid_request", "some_other_policy"] { + let body = serde_json::json!({ + "error": { + "message": "Some other bad request.", + "code": code + } + }) + .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.clone()), + })); - let CodexErrorDetails::InvalidRequest(message) = err.details() else { - panic!("expected CodexErrorDetails::InvalidRequest, got {err:?}"); - }; - assert_eq!(message, &body); + let CodexErrorDetails::InvalidRequest(message) = err.details() else { + panic!("expected CodexErrorDetails::InvalidRequest, got {err:?}"); + }; + assert_eq!(message, &body); + } } #[test] diff --git a/codex-rs/codex-api/src/error.rs b/codex-rs/codex-api/src/error.rs index 9c282ba803..a353a59b8b 100644 --- a/codex-rs/codex-api/src/error.rs +++ b/codex-rs/codex-api/src/error.rs @@ -33,6 +33,8 @@ pub enum ApiError { RateLimit(String), #[error("invalid request: {message}")] InvalidRequest { message: String }, + #[error("invalid prompt: {message}")] + InvalidPrompt { message: String }, #[error("cyber policy: {message}")] CyberPolicy { message: String }, #[error("bio policy: {message}")] diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index 3f867d5d0b..3baf9346e9 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -455,7 +455,7 @@ pub fn process_responses_event( let message = error .message .unwrap_or_else(|| "Invalid request.".to_string()); - response_error = ApiError::InvalidRequest { message }; + response_error = ApiError::InvalidPrompt { message }; } else if is_server_overloaded_error(&error) { response_error = ApiError::ServerOverloaded; } else { @@ -1404,7 +1404,7 @@ mod tests { assert_eq!(events.len(), 1); match (code, &events[0]) { - ("invalid_prompt", Err(ApiError::InvalidRequest { message })) + ("invalid_prompt", Err(ApiError::InvalidPrompt { message })) | ("bio_policy", Err(ApiError::BioPolicy { message })) => { assert_eq!(message, expected_message); } @@ -1414,23 +1414,35 @@ mod tests { } #[tokio::test] - async fn bio_policy_error_uses_fallback_for_missing_or_blank_message() { - for message in [None, Some(""), Some(" ")] { - let mut event = json!({ - "type": "response.failed", - "response": { "error": { "code": "bio_policy" } }, - }); - if let Some(message) = message { - event["response"]["error"]["message"] = json!(message); - } - let sse = format!("event: response.failed\ndata: {event}\n\n"); - let events = collect_events(&[sse.as_bytes()]).await; - match events.as_slice() { - [Err(ApiError::BioPolicy { message })] => assert_eq!( - message, - "This content was flagged for possible biological risk." - ), - other => panic!("unexpected events: {other:?}"), + async fn typed_errors_handle_missing_or_blank_message() { + for (code, fallback) in [ + ( + "bio_policy", + "This content was flagged for possible biological risk.", + ), + ("invalid_prompt", "Invalid request."), + ] { + for message in [None, Some(""), Some(" ")] { + let mut event = json!({ + "type": "response.failed", + "response": { "error": { "code": code } }, + }); + if let Some(message) = message { + event["response"]["error"]["message"] = json!(message); + } + let expected = match (code, message) { + ("invalid_prompt", Some(message)) => message, + _ => fallback, + }; + let sse = format!("event: response.failed\ndata: {event}\n\n"); + let events = collect_events(&[sse.as_bytes()]).await; + match (code, events.as_slice()) { + ("bio_policy", [Err(ApiError::BioPolicy { message })]) + | ("invalid_prompt", [Err(ApiError::InvalidPrompt { message })]) => { + assert_eq!(message, expected); + } + other => panic!("unexpected events: {other:?}"), + } } } } diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index efd37e51b6..d571575f8b 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -39,6 +39,7 @@ const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber"; const CYBER_POLICY_MESSAGE: &str = "This request has been flagged for potentially high-risk cyber activity."; const BIO_POLICY_MESSAGE: &str = "This request has been flagged for possible biological risk."; +const INVALID_PROMPT_MESSAGE: &str = "This prompt was rejected."; fn disabled_text_turn(test: &TestCodex, text: &str) -> TurnInputRequest { let (sandbox_policy, permission_profile) = @@ -108,48 +109,59 @@ async fn openai_model_header_mismatch_emits_warning_event() -> Result<()> { #[test_case::test_case("cyber_policy", CYBER_POLICY_MESSAGE, CodexErrorInfo::CyberPolicy; "cyber")] #[test_case::test_case("bio_policy", BIO_POLICY_MESSAGE, CodexErrorInfo::BioPolicy; "bio")] +#[test_case::test_case("invalid_prompt", INVALID_PROMPT_MESSAGE, CodexErrorInfo::InvalidPrompt; "invalid_prompt")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn policy_response_emits_typed_error_without_retry( +async fn error_response_emits_typed_error_without_retry( code: &str, message: &str, error_info: CodexErrorInfo, ) -> Result<()> { skip_if_no_network!(Ok(())); - let server = start_mock_server().await; - let response = ResponseTemplate::new(400).set_body_json(serde_json::json!({ - "error": { - "message": message, - "type": "invalid_request", - "param": null, - "code": code - } - })); - let mock = mount_response_once(&server, response).await; + let error = serde_json::json!({ + "message": message, + "type": "invalid_request", + "param": null, + "code": code + }); + for response in [ + ResponseTemplate::new(400).set_body_json(serde_json::json!({"error": error})), + sse_response(sse(vec![serde_json::json!({ + "type": "response.failed", + "response": {"error": error} + })])), + ] { + let server = start_mock_server().await; + let mock = mount_response_once(&server, response).await; - let mut builder = test_codex().with_model(REQUESTED_MODEL); - let test = builder.build_with_auto_env(&server).await?; + let mut builder = test_codex().with_model(REQUESTED_MODEL); + let test = builder.build_with_auto_env(&server).await?; - test.codex - .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { - text: "trigger policy error".to_string(), - text_elements: Vec::new(), - }])) - .await?; + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "trigger error".to_string(), + text_elements: Vec::new(), + }])) + .await?; - let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; - let EventMsg::Error(error) = error else { - panic!("expected error event"); - }; - assert_eq!(error.message, message); - assert_eq!(error.codex_error_info, Some(error_info)); + let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; + let EventMsg::Error(error) = error else { + panic!("expected error event"); + }; + assert_eq!(error.message, message); + assert_eq!(error.codex_error_info.as_ref(), Some(&error_info)); + assert_eq!( + serde_json::to_value(&error.codex_error_info)?, + serde_json::json!(code) + ); - let _ = wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; + let _ = wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; - mock.single_request(); + mock.single_request(); + } Ok(()) } diff --git a/codex-rs/ext/guardian-reviewer/src/retry.rs b/codex-rs/ext/guardian-reviewer/src/retry.rs index e1af53f466..0c485ac3bc 100644 --- a/codex-rs/ext/guardian-reviewer/src/retry.rs +++ b/codex-rs/ext/guardian-reviewer/src/retry.rs @@ -104,6 +104,7 @@ fn should_retry_guardian_review(outcome: &GuardianReviewOutcome) -> bool { | CodexErrorInfo::MisalignmentPolicyViolation | CodexErrorInfo::Unauthorized | CodexErrorInfo::BadRequest + | CodexErrorInfo::InvalidPrompt | CodexErrorInfo::SandboxError | CodexErrorInfo::ActiveTurnNotSteerable { .. } | CodexErrorInfo::ThreadRollbackFailed diff --git a/codex-rs/ext/guardian-reviewer/src/retry_tests.rs b/codex-rs/ext/guardian-reviewer/src/retry_tests.rs index cdc80f2490..ffb053031f 100644 --- a/codex-rs/ext/guardian-reviewer/src/retry_tests.rs +++ b/codex-rs/ext/guardian-reviewer/src/retry_tests.rs @@ -99,6 +99,13 @@ fn guardian_review_retry_only_retries_transient_session_and_parse_errors() { )), false, ), + ( + GuardianReviewOutcome::Error(GuardianReviewError::session_with_error_info( + anyhow::anyhow!("invalid prompt"), + CodexErrorInfo::InvalidPrompt, + )), + false, + ), ( GuardianReviewOutcome::Error(GuardianReviewError::parse(anyhow::anyhow!("parse"))), true, diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs b/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs index 21fbd94e57..5841259415 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/metrics.rs @@ -48,7 +48,7 @@ pub(super) fn sampler_failure_reason(error: &LunaSamplerError) -> &'static str { ApiError::UsageNotIncluded => "usage_not_included", ApiError::Retryable { .. } => "retryable_api_error", ApiError::RateLimitExceeded { .. } | ApiError::RateLimit(_) => "rate_limit", - ApiError::InvalidRequest { .. } => "invalid_request", + ApiError::InvalidRequest { .. } | ApiError::InvalidPrompt { .. } => "invalid_request", ApiError::CyberPolicy { .. } | ApiError::BioPolicy { .. } | ApiError::MisalignmentPolicyViolation { .. } => "policy_error", diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs index 0e50a96e90..0265fb647f 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/sampler/execution.rs @@ -92,6 +92,7 @@ impl SamplingExecution { | ApiError::UsageNotIncluded | ApiError::RateLimit(_) | ApiError::InvalidRequest { .. } + | ApiError::InvalidPrompt { .. } | ApiError::MisalignmentPolicyViolation { .. } | ApiError::CyberPolicy { .. } | ApiError::BioPolicy { .. }, diff --git a/codex-rs/protocol/src/codex_error_info.rs b/codex-rs/protocol/src/codex_error_info.rs index 35536f91fd..fd883e0558 100644 --- a/codex-rs/protocol/src/codex_error_info.rs +++ b/codex-rs/protocol/src/codex_error_info.rs @@ -34,6 +34,7 @@ enum CodexErrorInfoWire { InternalServerError, Unauthorized, BadRequest, + InvalidPrompt, SandboxError, ResponseStreamDisconnected { http_status_code: Option, diff --git a/codex-rs/protocol/src/error.rs b/codex-rs/protocol/src/error.rs index 609cdcab95..14825e4db6 100644 --- a/codex-rs/protocol/src/error.rs +++ b/codex-rs/protocol/src/error.rs @@ -126,6 +126,8 @@ pub enum CodexErrorDetails { /// Invalid request. #[error("{0}")] InvalidRequest(String), + #[error("{message}")] + InvalidPrompt { message: String }, /// Multiple registered tools share the same effective name. #[error("duplicate tool: {0}")] ToolCollision(String), @@ -387,6 +389,7 @@ impl CodexErr { | CodexErrorDetails::QuotaExceeded | CodexErrorDetails::InvalidImageRequest() | CodexErrorDetails::InvalidRequest(_) + | CodexErrorDetails::InvalidPrompt { .. } | CodexErrorDetails::ToolCollision(_) | CodexErrorDetails::RefreshTokenFailed(_) | CodexErrorDetails::UnsupportedOperation(_) @@ -452,6 +455,7 @@ impl CodexErr { CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded, CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy, CodexErrorDetails::BioPolicy { .. } => CodexErrorInfo::BioPolicy, + CodexErrorDetails::InvalidPrompt { .. } => CodexErrorInfo::InvalidPrompt, CodexErrorDetails::MisalignmentPolicyViolation { .. } => { CodexErrorInfo::MisalignmentPolicyViolation } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f5231dbca8..3b44039af6 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1861,6 +1861,7 @@ pub enum CodexErrorInfo { InternalServerError, Unauthorized, BadRequest, + InvalidPrompt, SandboxError, /// The response SSE stream disconnected in the middle of a turnbefore completion. ResponseStreamDisconnected { @@ -1898,6 +1899,7 @@ impl CodexErrorInfo { | Self::InternalServerError | Self::Unauthorized | Self::BadRequest + | Self::InvalidPrompt | Self::SandboxError | Self::ResponseStreamDisconnected { .. } | Self::ResponseTooManyFailedAttempts { .. } diff --git a/codex-rs/response-debug-context/src/lib.rs b/codex-rs/response-debug-context/src/lib.rs index 1a32b91cd9..9014af3e80 100644 --- a/codex-rs/response-debug-context/src/lib.rs +++ b/codex-rs/response-debug-context/src/lib.rs @@ -80,7 +80,9 @@ pub fn telemetry_api_error_message(error: &ApiError) -> String { ApiError::Retryable { .. } => "retryable error".to_string(), ApiError::RateLimitExceeded { .. } => "rate limit exceeded".to_string(), ApiError::RateLimit(_) => "rate limit".to_string(), - ApiError::InvalidRequest { .. } => "invalid request".to_string(), + ApiError::InvalidRequest { .. } | ApiError::InvalidPrompt { .. } => { + "invalid request".to_string() + } ApiError::CyberPolicy { .. } => "cyber policy".to_string(), ApiError::BioPolicy { .. } => "bio policy".to_string(), ApiError::MisalignmentPolicyViolation { .. } => "misalignment policy violation".to_string(),