Treat audio validation failures as non-retryable input errors (#58434)

## What changed

Map audio size, data, format, and sample-rate validation failures to `InvalidAudioInput` for HTTP 400 responses and streamed errors. Preserve the provider's message, falling back to `Invalid audio input.` when it is absent or blank.

Classify these errors as non-retryable and expose them as `BadRequest`. Redact their messages in doctor diagnostics and group them as invalid requests in telemetry.

## Testing

Add coverage for all six audio validation codes across HTTP errors, streamed `error` events, and `response.failed` events, checking error classification, message preservation, and the absence of retries. Extend doctor diagnostic redaction coverage for `InvalidAudioInput`.

GitOrigin-RevId: 0a59964bcabd83997bdb32fc9318a5482dac33b0
This commit is contained in:
Hung
2026-10-30 21:01:23 +00:00
committed by copyberry
parent 2546265e3e
commit 5a7353f040
8 changed files with 80 additions and 5 deletions
+4
View File
@@ -37,6 +37,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
| ApiError::FlexUnavailable
| ApiError::RateLimit(_)
| ApiError::InvalidRequest { .. }
| ApiError::InvalidAudioInput { .. }
| ApiError::ModelNotFound { .. }
| ApiError::InvalidPrompt { .. }
| ApiError::CyberPolicy { .. }
@@ -83,6 +84,9 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
})
}
ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
ApiError::InvalidAudioInput { message } => {
CodexErr::new(CodexErrorDetails::InvalidAudioInput { message })
}
ApiError::ModelNotFound { message, status } => {
CodexErr::new(CodexErrorDetails::ModelNotFound { message, status })
}
+18
View File
@@ -40,6 +40,8 @@ pub enum ApiError {
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("{message}")]
InvalidAudioInput { message: String },
#[error("{message}")]
ModelNotFound {
message: String,
status: Option<StatusCode>,
@@ -77,6 +79,22 @@ pub(crate) fn parse_terminal_provider_error(
let error = error?;
let code = error.get("code")?.as_str()?;
match (code, status) {
(
"audio_too_large"
| "audio_too_short"
| "invalid_audio"
| "invalid_audio_data"
| "invalid_audio_format"
| "missing_audio_sample_rate",
None | Some(StatusCode::BAD_REQUEST),
) => Some(ApiError::InvalidAudioInput {
message: error
.get("message")
.and_then(Value::as_str)
.filter(|message| !message.trim().is_empty())
.unwrap_or("Invalid audio input.")
.to_owned(),
}),
("flex_unavailable", None | Some(StatusCode::TOO_MANY_REQUESTS)) => {
Some(ApiError::FlexUnavailable)
}
@@ -16,6 +16,47 @@ use pretty_assertions::assert_eq;
use serde_json::json;
use std::time::Duration;
#[test]
fn audio_validation_codes_map_to_non_retryable_audio_input_errors_on_all_transports() {
for code in [
"audio_too_large",
"audio_too_short",
"invalid_audio",
"invalid_audio_data",
"invalid_audio_format",
"missing_audio_sample_rate",
] {
let detail = json!({"code": code, "message": "Invalid recording."});
let http_error = map_api_error(crate::error::ApiError::Transport(
crate::TransportError::Http {
status: StatusCode::BAD_REQUEST,
url: None,
headers: None,
body: Some(json!({"error": detail}).to_string()),
retry_after: None,
},
));
let mut errors = vec![http_error];
for failure in [
json!({"type": "error", "error": detail}),
json!({"type": "response.failed", "response": {"error": detail}}),
] {
let event = serde_json::from_value(failure).unwrap();
errors.push(map_api_error(
process_responses_event(event).unwrap_err().into_api_error(),
));
}
for error in errors {
assert_eq!(error.retry_delay(/*retry_count*/ 1), None);
assert!(matches!(
error.details(),
codex_protocol::error::CodexErrorDetails::InvalidAudioInput { .. }
));
assert_eq!(error.to_string(), "Invalid recording.");
}
}
}
/// Only the explicit policy capacity code changes overload attribution.
#[tokio::test(start_paused = true)]
async fn policy_capacity_classification_preserves_overload_retry_behavior() {