Enable uploaded audio with durable rejection recovery (#59234)

## Why

Uploaded audio was rejected even for audio-capable models. Invalid or expired recordings also need to be excluded from later requests so users can continue the conversation.

## What changed

- Accept uploaded audio when the selected model supports audio; otherwise, preserve valid sibling input and report the unsupported attachment before inference.
- Track successful audio decoding by model and provider, and attribute provider rejections against the exact submitted request. Preserve attachments when attribution is ambiguous.
- Persist confirmed rejected file IDs across compaction and resume, omitting them from future requests without rewriting retained user history.
- Check audio availability before remote compaction and retry once without an unambiguously rejected recording. Include guidance to remove or replace affected attachments in audio errors.

## Testing

Add unit and scenario coverage for uploaded audio across follow-up turns, compaction, cold and paginated resume, model switching, ambiguous rejections, expired files, and queued or interrupted input.

GitOrigin-RevId: e701901e7b1c0865c9f2a5918197215f549b06c0
This commit is contained in:
Hung
2026-11-03 21:24:16 +00:00
committed by copyberry
parent 5667598c8d
commit f1b0721c88
52 changed files with 2454 additions and 90 deletions
+7 -3
View File
@@ -86,9 +86,13 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
})
}
ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
ApiError::InvalidAudioInput { message } => {
CodexErr::new(CodexErrorDetails::InvalidAudioInput { message })
}
ApiError::InvalidAudioInput {
message,
per_recording,
} => CodexErr::new(CodexErrorDetails::InvalidAudioInput {
message,
per_recording,
}),
ApiError::ModelNotFound { message, status } => {
CodexErr::new(CodexErrorDetails::ModelNotFound { message, status })
}
+6 -1
View File
@@ -42,7 +42,11 @@ pub enum ApiError {
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("{message}")]
InvalidAudioInput { message: String },
InvalidAudioInput {
message: String,
/// True when the provider rejected an individual clip, independent of request size.
per_recording: bool,
},
#[error("{message}")]
ModelNotFound {
message: String,
@@ -93,6 +97,7 @@ pub(crate) fn parse_terminal_provider_error(
| "missing_audio_sample_rate",
None | Some(StatusCode::BAD_REQUEST),
) => Some(ApiError::InvalidAudioInput {
per_recording: code != "audio_too_large",
message: error
.get("message")
.and_then(Value::as_str)