use chrono::DateTime; use chrono::Utc; use codex_api::AuthProvider as ApiAuthProvider; use codex_api::TransportError; use codex_api::error::ApiError; use codex_api::rate_limits::parse_rate_limit; use http::HeaderMap; use serde::Deserialize; use crate::auth::CodexAuth; use crate::error::CodexErr; use crate::error::ModelCapError; use crate::error::RetryLimitReachedError; use crate::error::UnexpectedResponseError; use crate::error::UsageLimitReachedError; use crate::model_provider_info::ModelProviderInfo; use crate::token_data::PlanType; pub(crate) 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::Api { status, message } => CodexErr::UnexpectedStatus(UnexpectedResponseError { status, body: message, url: None, request_id: None, }), ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message), ApiError::Transport(transport) => match transport { TransportError::Http { status, url, headers, body, } => { let body_text = body.unwrap_or_default(); if status == http::StatusCode::BAD_REQUEST { 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 Some(model) = headers .as_ref() .and_then(|map| map.get(MODEL_CAP_MODEL_HEADER)) .and_then(|value| value.to_str().ok()) .map(str::to_string) { let reset_after_seconds = headers .as_ref() .and_then(|map| map.get(MODEL_CAP_RESET_AFTER_HEADER)) .and_then(|value| value.to_str().ok()) .and_then(|value| value.parse::().ok()); return CodexErr::ModelCap(ModelCapError { model, reset_after_seconds, }); } if let Ok(err) = serde_json::from_str::(&body_text) { if err.error.error_type.as_deref() == Some("usage_limit_reached") { let rate_limits = headers.as_ref().and_then(parse_rate_limit); 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, }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; } } CodexErr::RetryLimit(RetryLimitReachedError { status, request_id: extract_request_id(headers.as_ref()), }) } else { CodexErr::UnexpectedStatus(UnexpectedResponseError { status, body: body_text, url, request_id: extract_request_id(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 MODEL_CAP_MODEL_HEADER: &str = "x-codex-model-cap-model"; const MODEL_CAP_RESET_AFTER_HEADER: &str = "x-codex-model-cap-reset-after-seconds"; #[cfg(test)] mod tests { use super::*; use codex_api::TransportError; use http::HeaderMap; use http::StatusCode; #[test] fn map_api_error_maps_model_cap_headers() { let mut headers = HeaderMap::new(); headers.insert( MODEL_CAP_MODEL_HEADER, http::HeaderValue::from_static("boomslang"), ); headers.insert( MODEL_CAP_RESET_AFTER_HEADER, http::HeaderValue::from_static("120"), ); let err = map_api_error(ApiError::Transport(TransportError::Http { status: StatusCode::TOO_MANY_REQUESTS, url: Some("http://example.com/v1/responses".to_string()), headers: Some(headers), body: Some(String::new()), })); let CodexErr::ModelCap(model_cap) = err else { panic!("expected CodexErr::ModelCap, got {err:?}"); }; assert_eq!(model_cap.model, "boomslang"); assert_eq!(model_cap.reset_after_seconds, Some(120)); } } fn extract_request_id(headers: Option<&HeaderMap>) -> Option { headers.and_then(|map| { ["cf-ray", "x-request-id", "x-oai-request-id"] .iter() .find_map(|name| { map.get(*name) .and_then(|v| v.to_str().ok()) .map(str::to_string) }) }) } pub(crate) fn auth_provider_from_auth( auth: Option, provider: &ModelProviderInfo, ) -> crate::error::Result { if let Some(api_key) = provider.api_key()? { return Ok(CoreAuthProvider { token: Some(api_key), account_id: None, }); } if let Some(token) = provider.experimental_bearer_token.clone() { return Ok(CoreAuthProvider { token: Some(token), account_id: None, }); } if let Some(auth) = auth { let token = auth.get_token()?; Ok(CoreAuthProvider { token: Some(token), account_id: auth.get_account_id(), }) } else { Ok(CoreAuthProvider { token: None, account_id: None, }) } } #[derive(Debug, Deserialize)] struct UsageErrorResponse { error: UsageErrorBody, } #[derive(Debug, Deserialize)] struct UsageErrorBody { #[serde(rename = "type")] error_type: Option, plan_type: Option, resets_at: Option, } #[derive(Clone, Default)] pub(crate) struct CoreAuthProvider { token: Option, account_id: Option, } impl ApiAuthProvider for CoreAuthProvider { fn bearer_token(&self) -> Option { self.token.clone() } fn account_id(&self) -> Option { self.account_id.clone() } }