use crate::TransportError; use crate::error::ApiError; use crate::rate_limits::parse_promo_message; use crate::rate_limits::parse_rate_limit_for_limit; use base64::Engine; use chrono::DateTime; use chrono::Utc; use codex_protocol::auth::PlanType; use codex_protocol::error::CodexErr; use codex_protocol::error::RetryLimitReachedError; use codex_protocol::error::UnexpectedResponseError; use codex_protocol::error::UsageLimitReachedError; use http::HeaderMap; use serde::Deserialize; use serde_json::Value; pub fn map_api_error(err: ApiError) -> CodexErr { match err { ApiError::ContextWindowExceeded => CodexErr::ContextWindowExceeded, ApiError::QuotaExceeded => CodexErr::QuotaExceeded, ApiError::UsageNotIncluded => CodexErr::UsageNotIncluded, ApiError::Retryable { message, delay } => CodexErr::Stream(message, delay), ApiError::Stream(msg) => CodexErr::Stream(msg, None), ApiError::ServerOverloaded => CodexErr::ServerOverloaded, ApiError::Api { status, message } => CodexErr::UnexpectedStatus(UnexpectedResponseError { status, body: message, url: None, cf_ray: None, request_id: None, identity_authorization_error: None, identity_error_code: None, }), ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message), ApiError::CyberPolicy { message } => CodexErr::CyberPolicy { message }, ApiError::Transport(transport) => match transport { TransportError::Http { status, url, headers, body, } => { let body_text = body.unwrap_or_default(); if status == http::StatusCode::SERVICE_UNAVAILABLE && let Ok(value) = serde_json::from_str::(&body_text) && matches!( value .get("error") .and_then(|error| error.get("code")) .and_then(serde_json::Value::as_str), Some("server_is_overloaded" | "slow_down") ) { return CodexErr::ServerOverloaded; } if status == http::StatusCode::BAD_REQUEST { if let Ok(parsed) = serde_json::from_str::(&body_text) && let Some(error) = parsed.get("error") && error.get("code").and_then(Value::as_str) == Some(CYBER_POLICY_ERROR_CODE) { let message = error .get("message") .and_then(Value::as_str) .filter(|message| !message.trim().is_empty()) .map(str::to_string) .unwrap_or_else(|| CYBER_POLICY_FALLBACK_MESSAGE.to_string()); CodexErr::CyberPolicy { message } } else if body_text .contains("The image data you provided does not represent a valid image") { CodexErr::InvalidImageRequest() } else { CodexErr::InvalidRequest(body_text) } } else if status == http::StatusCode::INTERNAL_SERVER_ERROR { CodexErr::InternalServerError } else if status == http::StatusCode::TOO_MANY_REQUESTS { if let Ok(err) = serde_json::from_str::(&body_text) { if err.error.error_type.as_deref() == Some("usage_limit_reached") { let limit_id = extract_header(headers.as_ref(), ACTIVE_LIMIT_HEADER); let rate_limits = headers.as_ref().and_then(|map| { parse_rate_limit_for_limit(map, limit_id.as_deref()) }); let promo_message = headers.as_ref().and_then(parse_promo_message); let resets_at = err .error .resets_at .and_then(|seconds| DateTime::::from_timestamp(seconds, 0)); return CodexErr::UsageLimitReached(UsageLimitReachedError { plan_type: err.error.plan_type, resets_at, rate_limits: rate_limits.map(Box::new), promo_message, }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; } } CodexErr::RetryLimit(RetryLimitReachedError { status, request_id: extract_request_tracking_id(headers.as_ref()), }) } else { CodexErr::UnexpectedStatus(UnexpectedResponseError { status, body: body_text, url, cf_ray: extract_header(headers.as_ref(), CF_RAY_HEADER), request_id: extract_request_id(headers.as_ref()), identity_authorization_error: extract_header( headers.as_ref(), X_OPENAI_AUTHORIZATION_ERROR_HEADER, ), identity_error_code: extract_x_error_json_code(headers.as_ref()), }) } } TransportError::RetryLimit => CodexErr::RetryLimit(RetryLimitReachedError { status: http::StatusCode::INTERNAL_SERVER_ERROR, request_id: None, }), TransportError::Timeout => CodexErr::Timeout, TransportError::Network(msg) | TransportError::Build(msg) => { CodexErr::Stream(msg, None) } }, ApiError::RateLimit(msg) => CodexErr::Stream(msg, None), } } const ACTIVE_LIMIT_HEADER: &str = "x-codex-active-limit"; const REQUEST_ID_HEADER: &str = "x-request-id"; 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 CYBER_POLICY_ERROR_CODE: &str = "cyber_policy"; const CYBER_POLICY_FALLBACK_MESSAGE: &str = "This request has been flagged for possible cybersecurity risk."; #[cfg(test)] #[path = "api_bridge_tests.rs"] mod tests; fn extract_request_tracking_id(headers: Option<&HeaderMap>) -> Option { extract_request_id(headers).or_else(|| extract_header(headers, CF_RAY_HEADER)) } fn extract_request_id(headers: Option<&HeaderMap>) -> Option { extract_header(headers, REQUEST_ID_HEADER) .or_else(|| extract_header(headers, OAI_REQUEST_ID_HEADER)) } fn extract_header(headers: Option<&HeaderMap>, name: &str) -> Option { headers.and_then(|map| { map.get(name) .and_then(|value| value.to_str().ok()) .map(str::to_string) }) } fn extract_x_error_json_code(headers: Option<&HeaderMap>) -> Option { let encoded = extract_header(headers, X_ERROR_JSON_HEADER)?; let decoded = base64::engine::general_purpose::STANDARD .decode(encoded) .ok()?; let parsed = serde_json::from_slice::(&decoded).ok()?; parsed .get("error") .and_then(|error| error.get("code")) .and_then(Value::as_str) .map(str::to_string) } #[derive(Debug, Deserialize)] struct UsageErrorResponse { error: UsageErrorBody, } #[derive(Debug, Deserialize)] struct UsageErrorBody { #[serde(rename = "type")] error_type: Option, plan_type: Option, resets_at: Option, }