//! OpenAI 路由共享错误响应。 use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde_json::{json, Value}; use crate::{ dispatch::errors::{DispatchFailureClass, ResponseDispatchError}, upstream::openai::protocol::sse::response_failed_sse_event as encode_response_failed_sse_event, upstream::openai::transport::CodexClientError, }; const NO_ACTIVE_UPSTREAM_ACCOUNT_MESSAGE: &str = "No active upstream account is available"; const NO_AVAILABLE_RESPONSES_ACCOUNTS_MESSAGE: &str = "No available accounts. All accounts are expired or rate-limited."; const UPSTREAM_CODEX_REQUEST_FAILED_MESSAGE: &str = "Upstream Codex request failed"; const INVALID_UPSTREAM_CODEX_RESPONSE_MESSAGE: &str = "Invalid upstream Codex response"; const UPSTREAM_CODEX_RESPONSE_FAILED_MESSAGE: &str = "Upstream Codex response failed"; const HISTORY_UNAVAILABLE_MESSAGE: &str = "Previous response context is unavailable. Start a new conversation or resend the complete input without previous_response_id."; const CONTINUATION_BUSY_MESSAGE: &str = "The previous response connection is busy. Retry the request."; #[derive(Clone, Copy)] enum ResponseDispatchMessageStyle { Standard, ResponsesStream, } struct DispatchHttpError { status: StatusCode, message: String, } struct OpenAiErrorDetails { status: StatusCode, message: String, error_type: &'static str, code: &'static str, } #[derive(Debug, Clone, Copy)] struct OpenAiErrorKind { error_type: &'static str, code: &'static str, } const NO_AVAILABLE_ACCOUNTS_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "server_error", code: "no_available_accounts", }; const UPSTREAM_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "server_error", code: "upstream_error", }; const INSUFFICIENT_QUOTA_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "insufficient_quota", code: "insufficient_quota", }; const RATE_LIMIT_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "rate_limit_error", code: "rate_limit_exceeded", }; const AUTHENTICATION_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "invalid_request_error", code: "invalid_api_key", }; const MODEL_NOT_FOUND_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "invalid_request_error", code: "model_not_found", }; const HISTORY_UNAVAILABLE_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "invalid_request_error", code: "previous_response_unavailable", }; const CONTINUATION_BUSY_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "server_error", code: "continuation_connection_busy", }; const INVALID_UPSTREAM_RESPONSE_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "server_error", code: "invalid_upstream_response", }; const CODEX_API_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "server_error", code: "codex_api_error", }; const CODEX_CLIENT_ERROR: OpenAiErrorKind = OpenAiErrorKind { error_type: "invalid_request_error", code: "codex_client_error", }; /// OpenAI 兼容错误响应。 pub fn openai_error_response( status: StatusCode, message: &str, error_type: &str, code: &str, ) -> (StatusCode, Json) { ( status, Json(json!({ "error": { "message": message, "type": error_type, "code": code } })), ) } pub fn responses_dispatch_error_response(error: ResponseDispatchError) -> Response { responses_dispatch_error_response_ref(&error) } pub fn responses_dispatch_error_response_ref(error: &ResponseDispatchError) -> Response { if let ResponseDispatchError::Upstream(CodexClientError::Upstream { status, body, .. }) = error { if status.is_client_error() { if let Ok(body) = serde_json::from_str::(body) { return (*status, Json(body)).into_response(); } } } match error { ResponseDispatchError::NoActiveAccount => { responses_no_available_accounts_response().into_response() } error => response_dispatch_openai_error_response(error), } } pub fn responses_stream_dispatch_failed_sse_event(error: &ResponseDispatchError) -> String { let http_error = response_dispatch_http_error(error, ResponseDispatchMessageStyle::ResponsesStream); let kind = response_dispatch_error_kind(error); responses_failed_sse_event(kind, &http_error.message) } fn responses_no_available_accounts_response() -> (StatusCode, Json) { let kind = NO_AVAILABLE_ACCOUNTS_ERROR; ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "error": { "type": kind.error_type, "code": kind.code, "message": NO_AVAILABLE_RESPONSES_ACCOUNTS_MESSAGE, } })), ) } fn response_dispatch_openai_error_response(error: &ResponseDispatchError) -> Response { let error = response_dispatch_openai_error(error); openai_error_response(error.status, &error.message, error.error_type, error.code) .into_response() } fn responses_failed_sse_event(kind: OpenAiErrorKind, message: &str) -> String { encode_response_failed_sse_event(kind.error_type, kind.code, message) } /// 缺失 client API key。 pub fn missing_client_api_key_response() -> (StatusCode, Json) { openai_error_response( StatusCode::UNAUTHORIZED, "Missing client API key", "invalid_request_error", "invalid_api_key", ) } /// 模型不存在。 pub fn model_not_found_response() -> (StatusCode, Json) { openai_error_response( StatusCode::NOT_FOUND, "Model not found", "invalid_request_error", "model_not_found", ) } /// Responses 请求无效。 pub fn invalid_responses_request_response() -> (StatusCode, Json) { invalid_openai_request_response("Invalid responses request") } fn invalid_openai_request_response(message: &str) -> (StatusCode, Json) { openai_error_response( StatusCode::BAD_REQUEST, message, "invalid_request_error", "invalid_request", ) } fn response_dispatch_openai_error(error: &ResponseDispatchError) -> OpenAiErrorDetails { let DispatchHttpError { status, message } = response_dispatch_http_error(error, ResponseDispatchMessageStyle::Standard); let kind = response_dispatch_error_kind(error); OpenAiErrorDetails { status, message, error_type: kind.error_type, code: kind.code, } } fn response_dispatch_error_kind(error: &ResponseDispatchError) -> OpenAiErrorKind { dispatch_failure_openai_error_kind( error.metadata().failure_class, status_from_u16(error.client_http_status_code(), StatusCode::BAD_GATEWAY), ) } fn response_dispatch_http_error( error: &ResponseDispatchError, message_style: ResponseDispatchMessageStyle, ) -> DispatchHttpError { DispatchHttpError { status: status_from_u16(error.client_http_status_code(), StatusCode::BAD_GATEWAY), message: response_dispatch_message(error, message_style), } } fn responses_error_kind_for_status(status: StatusCode) -> OpenAiErrorKind { if status == StatusCode::TOO_MANY_REQUESTS { RATE_LIMIT_ERROR } else if status == StatusCode::PAYMENT_REQUIRED { INSUFFICIENT_QUOTA_ERROR } else if matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) { AUTHENTICATION_ERROR } else if status.is_client_error() { CODEX_CLIENT_ERROR } else { CODEX_API_ERROR } } fn response_dispatch_message( error: &ResponseDispatchError, style: ResponseDispatchMessageStyle, ) -> String { let metadata = error.metadata(); if let Some(exhausted) = error.exhausted_account() { return exhausted_dispatch_message( exhausted.count, exhausted.kind.message_reason(), exhausted.upstream_error, matches!( (metadata.failure_class, style), ( DispatchFailureClass::CloudflarePathBlocked | DispatchFailureClass::ModelUnsupported, ResponseDispatchMessageStyle::ResponsesStream, ) ), ); } dispatch_failure_message( metadata.failure_class, match style { ResponseDispatchMessageStyle::Standard => DispatchFailureMessageStyle::Standard, ResponseDispatchMessageStyle::ResponsesStream => { DispatchFailureMessageStyle::ResponsesStream } }, ) } #[derive(Clone, Copy)] enum DispatchFailureMessageStyle { Standard, ResponsesStream, } fn dispatch_failure_message( failure_class: DispatchFailureClass, _style: DispatchFailureMessageStyle, ) -> String { match failure_class { DispatchFailureClass::NoAvailableAccounts => NO_ACTIVE_UPSTREAM_ACCOUNT_MESSAGE.to_owned(), DispatchFailureClass::Upstream => UPSTREAM_CODEX_REQUEST_FAILED_MESSAGE.to_owned(), DispatchFailureClass::InvalidSse | DispatchFailureClass::MissingCompleted | DispatchFailureClass::EmptyUpstreamResponse => { INVALID_UPSTREAM_CODEX_RESPONSE_MESSAGE.to_owned() } DispatchFailureClass::ResponseFailed => UPSTREAM_CODEX_RESPONSE_FAILED_MESSAGE.to_owned(), DispatchFailureClass::HistoryUnavailable => HISTORY_UNAVAILABLE_MESSAGE.to_owned(), DispatchFailureClass::ContinuationBusy => CONTINUATION_BUSY_MESSAGE.to_owned(), _ => UPSTREAM_CODEX_REQUEST_FAILED_MESSAGE.to_owned(), } } fn dispatch_failure_openai_error_kind( failure_class: DispatchFailureClass, status: StatusCode, ) -> OpenAiErrorKind { match failure_class { DispatchFailureClass::NoAvailableAccounts => NO_AVAILABLE_ACCOUNTS_ERROR, DispatchFailureClass::QuotaExhausted => INSUFFICIENT_QUOTA_ERROR, DispatchFailureClass::RateLimited => RATE_LIMIT_ERROR, DispatchFailureClass::Expired | DispatchFailureClass::Disabled | DispatchFailureClass::Banned => AUTHENTICATION_ERROR, DispatchFailureClass::ModelUnsupported => MODEL_NOT_FOUND_ERROR, DispatchFailureClass::HistoryUnavailable => HISTORY_UNAVAILABLE_ERROR, DispatchFailureClass::ContinuationBusy => CONTINUATION_BUSY_ERROR, DispatchFailureClass::InvalidSse | DispatchFailureClass::MissingCompleted | DispatchFailureClass::EmptyUpstreamResponse => INVALID_UPSTREAM_RESPONSE_ERROR, DispatchFailureClass::ResponseFailed => responses_error_kind_for_status(status), DispatchFailureClass::Upstream if status.is_client_error() => { responses_error_kind_for_status(status) } DispatchFailureClass::Upstream | DispatchFailureClass::UpstreamUnavailable => { UPSTREAM_ERROR } DispatchFailureClass::CloudflareChallenge | DispatchFailureClass::CloudflarePathBlocked => { UPSTREAM_ERROR } } } fn exhausted_dispatch_message( count: usize, reason: &str, upstream_error: &str, no_accounts_prefix: bool, ) -> String { if no_accounts_prefix { format!( "No accounts available. All accounts exhausted ({count} {reason}). Codex upstream error: {upstream_error}" ) } else { format!("All accounts exhausted ({count} {reason}). Codex upstream error: {upstream_error}") } } fn status_from_u16(status: u16, fallback: StatusCode) -> StatusCode { StatusCode::from_u16(status).unwrap_or(fallback) }