Files
codex/codex-rs/codex-api/src/error.rs
T
Won Park 8bd5a136ff Add recovery guidance to content-filter retries (#49119)
## Why

Responses stopped by `content_filter` previously followed generic stream-error retry handling without guidance about the block. Retried requests need instructions to explain the limitation and offer a permitted alternative.

## What changed

- Recognize content-filter failures as a distinct error while preserving retryability and the existing public error representation.
- Append developer guidance to the conversation after each sampling block, retaining delivered output and using the existing retry handling.
- Support a model catalog override through `model_messages.content_filter_guidance`. Missing, null, blank, or values exceeding 512 UTF-8 bytes fall back to bundled guidance.
- The bundled guidance tells the model to avoid reproducing or working around blocked content, briefly explain the limitation, offer a permitted alternative, and continue unrelated authorized work.

## Testing

Add a scenario and request-history snapshot covering repeated blocks, catalog guidance on each retry, retained partial output, and eventual completion. Add error-mapping assertions for retryability, public error classification, and error text.

GitOrigin-RevId: ba0c201b3a5a3acf5cfa971332027240e0690225
2026-09-29 01:14:41 +00:00

66 lines
2.0 KiB
Rust

use crate::rate_limits::RateLimitError;
use codex_client::TransportError;
use codex_http_client::RetryAfter;
use codex_protocol::protocol::MisalignmentErrorDetails;
use http::StatusCode;
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error(transparent)]
Transport(#[from] TransportError),
#[error("api error {status}: {message}")]
Api { status: StatusCode, message: String },
#[error("stream error: {0}")]
Stream(String),
#[error("stream error: Incomplete response returned, reason: content_filter")]
ContentFilter,
#[error("context window exceeded")]
ContextWindowExceeded,
#[error("quota exceeded")]
QuotaExceeded,
#[error("usage not included")]
UsageNotIncluded,
#[error("retryable error: {message}")]
Retryable {
message: String,
retry_after: Option<RetryAfter>,
},
#[error("rate limit exceeded: {message}")]
RateLimitExceeded {
message: String,
retry_after: Option<RetryAfter>,
},
#[error("rate limit: {0}")]
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}")]
BioPolicy { message: String },
#[error("misalignment policy violation: {message}")]
MisalignmentPolicyViolation {
message: String,
misalignment: Option<MisalignmentErrorDetails>,
},
#[error("Flex capacity unavailable.")]
FlexUnavailable,
#[error("server overloaded")]
ServerOverloaded { retry_after: Option<RetryAfter> },
}
impl From<RateLimitError> for ApiError {
fn from(err: RateLimitError) -> Self {
Self::RateLimit(err.to_string())
}
}
pub(crate) fn parse_flex_unavailable(error: &Value) -> Option<ApiError> {
(error.get("code").and_then(Value::as_str) == Some("flex_unavailable"))
.then_some(ApiError::FlexUnavailable)
}