Download codex-rs/protocol/src/error.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 31.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/error.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/protocol/src/error.rs
-
curl -L -o error.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/error.rs
31.2 kB
| use crate::ThreadId; | |
| use crate::auth::KnownPlan; | |
| use crate::auth::PlanType; | |
| pub use crate::auth::RefreshTokenFailedError; | |
| pub use crate::auth::RefreshTokenFailedReason; | |
| use crate::exec_output::ExecToolCallOutput; | |
| use crate::network_policy::NetworkPolicyDecisionPayload; | |
| use crate::protocol::CodexErrorInfo; | |
| use crate::protocol::ErrorEvent; | |
| use crate::protocol::MisalignmentErrorDetails; | |
| use crate::protocol::RateLimitReachedType; | |
| use crate::protocol::RateLimitSnapshot; | |
| use crate::protocol::TruncationPolicy; | |
| use chrono::DateTime; | |
| use chrono::Datelike; | |
| use chrono::Local; | |
| use chrono::Utc; | |
| use codex_async_utils::CancelErr; | |
| use codex_http_client::HttpError; | |
| use codex_utils_string::truncate_middle_chars; | |
| use codex_utils_string::truncate_middle_with_token_budget; | |
| use http::StatusCode; | |
| use serde_json; | |
| use std::fmt; | |
| use std::io; | |
| use std::time::Duration; | |
| use strum_macros::EnumDiscriminants; | |
| use thiserror::Error; | |
| use tokio::task::JoinError; | |
| pub type Result<T> = std::result::Result<T, CodexErr>; | |
| /// Limit UI error messages to a reasonable size while keeping useful context. | |
| const ERROR_MESSAGE_UI_MAX_BYTES: usize = 2 * 1024; | |
| pub enum SandboxErr { | |
| /// Error from sandbox execution | |
| Denied { | |
| output: Box<ExecToolCallOutput>, | |
| network_policy_decision: Option<NetworkPolicyDecisionPayload>, | |
| }, | |
| /// Error from linux seccomp filter setup | |
| SeccompInstall( seccompiler::Error), | |
| /// Error from linux seccomp backend | |
| SeccompBackend( seccompiler::BackendError), | |
| /// Command timed out | |
| Timeout { output: Box<ExecToolCallOutput> }, | |
| /// Command was killed by a signal | |
| Signal(i32), | |
| /// Error from linux landlock | |
| LandlockRestrict, | |
| } | |
| pub struct CodexErr { | |
| details: CodexErrorDetails, | |
| retry_delay: Option<Duration>, | |
| } | |
| /// The semantic category and diagnostic payload for a [`CodexErr`]. | |
| pub enum CodexErrorDetails { | |
| TurnAborted, | |
| SessionBudgetExceeded, | |
| /// Returned by ResponsesClient when the SSE stream disconnects or errors out **after** the HTTP | |
| /// handshake has succeeded but **before** it finished emitting `response.completed`. | |
| /// | |
| /// The Session loop treats this as a transient error and will automatically retry the turn. | |
| Stream(String), | |
| /// A retryable upstream rate limit received inside the response stream. | |
| RateLimitExceeded(String), | |
| // The iOS input-limit classifier matches this message's ASCII prefix. | |
| ContextWindowExceeded, | |
| ThreadNotFound(ThreadId), | |
| AgentLimitReached { max_threads: usize }, | |
| SessionConfiguredNotFirstEvent, | |
| /// Returned by run_command_stream when the spawned child process timed out (10s). | |
| Timeout, | |
| RequestTimeout, | |
| /// Returned by run_command_stream when the child could not be spawned (its stdout/stderr pipes | |
| /// could not be captured). Analogous to the previous `CodexError::Spawn` variant. | |
| Spawn, | |
| /// Returned by run_command_stream when the user pressed Ctrl-C (SIGINT). Session uses this to | |
| /// surface a polite FunctionCallOutput back to the model instead of crashing the CLI. | |
| Interrupted, | |
| /// Unexpected HTTP status code. | |
| UnexpectedStatus(UnexpectedResponseError), | |
| /// Invalid request. | |
| InvalidRequest(String), | |
| /// Multiple registered tools share the same effective name. | |
| ToolCollision(String), | |
| /// Invalid image. | |
| InvalidImageRequest(), | |
| UsageLimitReached(UsageLimitReachedError), | |
| ServerOverloaded, | |
| CyberPolicy { message: String }, | |
| MisalignmentPolicyViolation { | |
| message: String, | |
| misalignment: Option<MisalignmentErrorDetails>, | |
| }, | |
| ResponseStreamFailed(ResponseStreamFailed), | |
| ConnectionFailed(ConnectionFailedError), | |
| QuotaExceeded, | |
| UsageNotIncluded, | |
| InternalServerError, | |
| /// Retry limit exceeded. | |
| RetryLimit(RetryLimitReachedError), | |
| /// Agent loop died unexpectedly | |
| InternalAgentDied, | |
| /// Sandbox error | |
| Sandbox( SandboxErr), | |
| LandlockSandboxExecutableNotProvided, | |
| UnsupportedOperation(String), | |
| RefreshTokenFailed(RefreshTokenFailedError), | |
| Fatal(String), | |
| // ----------------------------------------------------------------- | |
| // Automatic conversions for common external error types | |
| // ----------------------------------------------------------------- | |
| Io( io::Error), | |
| Json( serde_json::Error), | |
| LandlockRuleset( landlock::RulesetError), | |
| LandlockPathFd( landlock::PathFdError), | |
| TokioJoin( JoinError), | |
| EnvVar(EnvVarError), | |
| } | |
| impl From<&CodexErr> for CodexErrKind { | |
| fn from(error: &CodexErr) -> Self { | |
| error.details().into() | |
| } | |
| } | |
| impl fmt::Debug for CodexErr { | |
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| match &self.details { | |
| CodexErrorDetails::Stream(message) => formatter | |
| .debug_tuple("Stream") | |
| .field(message) | |
| .field(&self.retry_delay) | |
| .finish(), | |
| details => fmt::Debug::fmt(details, formatter), | |
| } | |
| } | |
| } | |
| impl fmt::Display for CodexErr { | |
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| fmt::Display::fmt(&self.details, formatter) | |
| } | |
| } | |
| impl std::error::Error for CodexErr { | |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
| self.details.source() | |
| } | |
| } | |
| impl From<CodexErrorDetails> for CodexErr { | |
| fn from(details: CodexErrorDetails) -> Self { | |
| Self { | |
| details, | |
| retry_delay: None, | |
| } | |
| } | |
| } | |
| impl From<CancelErr> for CodexErr { | |
| fn from(error: CancelErr) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<SandboxErr> for CodexErr { | |
| fn from(error: SandboxErr) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<io::Error> for CodexErr { | |
| fn from(error: io::Error) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<serde_json::Error> for CodexErr { | |
| fn from(error: serde_json::Error) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<JoinError> for CodexErr { | |
| fn from(error: JoinError) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<landlock::RulesetError> for CodexErr { | |
| fn from(error: landlock::RulesetError) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<landlock::PathFdError> for CodexErr { | |
| fn from(error: landlock::PathFdError) -> Self { | |
| CodexErrorDetails::from(error).into() | |
| } | |
| } | |
| impl From<CancelErr> for CodexErrorDetails { | |
| fn from(_: CancelErr) -> Self { | |
| CodexErrorDetails::TurnAborted | |
| } | |
| } | |
| // TODO(anp): Remove this compatibility macro once callers construct | |
| // `CodexErrorDetails` directly. | |
| macro_rules! codex_err_unit_constructors { | |
| ($($variant:ident),* $(,)?) => { | |
| $( | |
| pub const $variant: Self = Self { | |
| details: CodexErrorDetails::$variant, | |
| retry_delay: None, | |
| }; | |
| )* | |
| }; | |
| } | |
| // TODO(anp): Remove this compatibility macro once callers construct | |
| // `CodexErrorDetails` directly. | |
| macro_rules! codex_err_tuple_constructors { | |
| ($($()* $variant:ident($value:ident: $value_type:ty)),* $(,)?) => { | |
| $( | |
| $()* | |
| pub fn $variant($value: $value_type) -> Self { | |
| CodexErrorDetails::$variant($value).into() | |
| } | |
| )* | |
| }; | |
| } | |
| impl CodexErr { | |
| codex_err_unit_constructors!( | |
| TurnAborted, | |
| SessionBudgetExceeded, | |
| ContextWindowExceeded, | |
| SessionConfiguredNotFirstEvent, | |
| Timeout, | |
| RequestTimeout, | |
| Spawn, | |
| Interrupted, | |
| ServerOverloaded, | |
| QuotaExceeded, | |
| UsageNotIncluded, | |
| InternalServerError, | |
| InternalAgentDied, | |
| LandlockSandboxExecutableNotProvided, | |
| ); | |
| codex_err_tuple_constructors!( | |
| Stream(message: String), | |
| ThreadNotFound(thread_id: ThreadId), | |
| UnexpectedStatus(error: UnexpectedResponseError), | |
| InvalidRequest(message: String), | |
| UsageLimitReached(error: UsageLimitReachedError), | |
| ResponseStreamFailed(error: ResponseStreamFailed), | |
| ConnectionFailed(error: ConnectionFailedError), | |
| RetryLimit(error: RetryLimitReachedError), | |
| Sandbox(error: SandboxErr), | |
| UnsupportedOperation(message: String), | |
| RefreshTokenFailed(error: RefreshTokenFailedError), | |
| Fatal(message: String), | |
| Io(error: io::Error), | |
| Json(error: serde_json::Error), | |
| LandlockRuleset(error: landlock::RulesetError), | |
| LandlockPathFd(error: landlock::PathFdError), | |
| TokioJoin(error: JoinError), | |
| EnvVar(error: EnvVarError), | |
| ); | |
| // TODO(anp): Remove this compatibility constructor once callers construct | |
| // `CodexErrorDetails` directly. | |
| pub fn InvalidImageRequest() -> Self { | |
| CodexErrorDetails::InvalidImageRequest().into() | |
| } | |
| /// Creates an error with no server-provided retry delay. | |
| pub fn new(details: CodexErrorDetails) -> Self { | |
| details.into() | |
| } | |
| /// Returns the semantic failure and its diagnostic payload. | |
| pub fn details(&self) -> &CodexErrorDetails { | |
| &self.details | |
| } | |
| pub fn is_retryable(&self) -> bool { | |
| match self.details() { | |
| CodexErrorDetails::TurnAborted | |
| | CodexErrorDetails::SessionBudgetExceeded | |
| | CodexErrorDetails::Interrupted | |
| | CodexErrorDetails::EnvVar(_) | |
| | CodexErrorDetails::Fatal(_) | |
| | CodexErrorDetails::UsageNotIncluded | |
| | CodexErrorDetails::QuotaExceeded | |
| | CodexErrorDetails::InvalidImageRequest() | |
| | CodexErrorDetails::InvalidRequest(_) | |
| | CodexErrorDetails::ToolCollision(_) | |
| | CodexErrorDetails::RefreshTokenFailed(_) | |
| | CodexErrorDetails::UnsupportedOperation(_) | |
| | CodexErrorDetails::Sandbox(_) | |
| | CodexErrorDetails::LandlockSandboxExecutableNotProvided | |
| | CodexErrorDetails::RetryLimit(_) | |
| | CodexErrorDetails::ContextWindowExceeded | |
| | CodexErrorDetails::ThreadNotFound(_) | |
| | CodexErrorDetails::AgentLimitReached { .. } | |
| | CodexErrorDetails::Spawn | |
| | CodexErrorDetails::SessionConfiguredNotFirstEvent | |
| | CodexErrorDetails::UsageLimitReached(_) | |
| | CodexErrorDetails::ServerOverloaded | |
| | CodexErrorDetails::CyberPolicy { .. } | |
| | CodexErrorDetails::MisalignmentPolicyViolation { .. } => false, | |
| CodexErrorDetails::Stream(..) | |
| | CodexErrorDetails::RateLimitExceeded(_) | |
| | CodexErrorDetails::Timeout | |
| | CodexErrorDetails::RequestTimeout | |
| | CodexErrorDetails::UnexpectedStatus(_) | |
| | CodexErrorDetails::ResponseStreamFailed(_) | |
| | CodexErrorDetails::ConnectionFailed(_) | |
| | CodexErrorDetails::InternalServerError | |
| | CodexErrorDetails::InternalAgentDied | |
| | CodexErrorDetails::Io(_) | |
| | CodexErrorDetails::Json(_) | |
| | CodexErrorDetails::TokioJoin(_) => true, | |
| CodexErrorDetails::LandlockRuleset(_) | CodexErrorDetails::LandlockPathFd(_) => false, | |
| } | |
| } | |
| pub fn retry_delay(&self) -> Option<Duration> { | |
| self.retry_delay | |
| } | |
| pub fn with_retry_delay(mut self, retry_delay: Duration) -> Self { | |
| self.retry_delay = Some(retry_delay); | |
| self | |
| } | |
| /// Minimal shim so that existing `e.downcast_ref::<CodexErr>()` checks continue to compile | |
| /// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of | |
| /// `anyhow::Error::downcast_ref` but works directly on our concrete error type. | |
| pub fn downcast_ref<T: std::any::Any>(&self) -> Option<&T> { | |
| (self as &dyn std::any::Any).downcast_ref::<T>() | |
| } | |
| /// Translate core error to client-facing protocol error. | |
| pub fn to_codex_protocol_error(&self) -> CodexErrorInfo { | |
| match &self.details { | |
| CodexErrorDetails::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded, | |
| CodexErrorDetails::SessionBudgetExceeded => CodexErrorInfo::SessionBudgetExceeded, | |
| CodexErrorDetails::RateLimitExceeded(_) => CodexErrorInfo::RateLimitExceeded, | |
| CodexErrorDetails::UsageLimitReached(_) | |
| | CodexErrorDetails::QuotaExceeded | |
| | CodexErrorDetails::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded, | |
| CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded, | |
| CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy, | |
| CodexErrorDetails::MisalignmentPolicyViolation { .. } => { | |
| CodexErrorInfo::MisalignmentPolicyViolation | |
| } | |
| CodexErrorDetails::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts { | |
| http_status_code: self.http_status_code_value(), | |
| }, | |
| CodexErrorDetails::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed { | |
| http_status_code: self.http_status_code_value(), | |
| }, | |
| CodexErrorDetails::ResponseStreamFailed(_) => { | |
| CodexErrorInfo::ResponseStreamConnectionFailed { | |
| http_status_code: self.http_status_code_value(), | |
| } | |
| } | |
| CodexErrorDetails::RefreshTokenFailed(_) => CodexErrorInfo::Unauthorized, | |
| CodexErrorDetails::SessionConfiguredNotFirstEvent | |
| | CodexErrorDetails::InternalServerError | |
| | CodexErrorDetails::InternalAgentDied => CodexErrorInfo::InternalServerError, | |
| CodexErrorDetails::UnsupportedOperation(_) | |
| | CodexErrorDetails::ThreadNotFound(_) | |
| | CodexErrorDetails::AgentLimitReached { .. } => CodexErrorInfo::BadRequest, | |
| CodexErrorDetails::Sandbox(_) => CodexErrorInfo::SandboxError, | |
| _ => CodexErrorInfo::Other, | |
| } | |
| } | |
| pub fn to_error_event(&self, message_prefix: Option<String>) -> ErrorEvent { | |
| let error_message = self.to_string(); | |
| let message: String = match message_prefix { | |
| Some(prefix) => format!("{prefix}: {error_message}"), | |
| None => error_message, | |
| }; | |
| ErrorEvent { | |
| message, | |
| codex_error_info: Some(self.to_codex_protocol_error()), | |
| misalignment: match &self.details { | |
| CodexErrorDetails::MisalignmentPolicyViolation { misalignment, .. } => { | |
| misalignment.clone() | |
| } | |
| _ => None, | |
| }, | |
| } | |
| } | |
| pub fn http_status_code_value(&self) -> Option<u16> { | |
| let http_status_code = match &self.details { | |
| CodexErrorDetails::RetryLimit(err) => Some(err.status), | |
| CodexErrorDetails::UnexpectedStatus(err) => Some(err.status), | |
| CodexErrorDetails::ConnectionFailed(err) => err.source.status(), | |
| CodexErrorDetails::ResponseStreamFailed(err) => err.source.status(), | |
| _ => None, | |
| }; | |
| http_status_code.as_ref().map(StatusCode::as_u16) | |
| } | |
| } | |
| pub struct ConnectionFailedError { | |
| pub source: HttpError, | |
| } | |
| impl std::fmt::Display for ConnectionFailedError { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| write!(f, "Connection failed: {}", self.source) | |
| } | |
| } | |
| pub struct ResponseStreamFailed { | |
| pub source: HttpError, | |
| pub request_id: Option<String>, | |
| } | |
| impl std::fmt::Display for ResponseStreamFailed { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| write!( | |
| f, | |
| "Error while reading the server response: {}{}", | |
| self.source, | |
| self.request_id | |
| .as_ref() | |
| .map(|id| format!(", request id: {id}")) | |
| .unwrap_or_default() | |
| ) | |
| } | |
| } | |
| pub struct UnexpectedResponseError { | |
| pub status: StatusCode, | |
| pub body: String, | |
| pub user_message: Option<String>, | |
| pub url: Option<String>, | |
| pub cf_ray: Option<String>, | |
| pub request_id: Option<String>, | |
| pub identity_authorization_error: Option<String>, | |
| pub identity_error_code: Option<String>, | |
| } | |
| const UNEXPECTED_RESPONSE_BODY_MAX_BYTES: usize = 1000; | |
| impl UnexpectedResponseError { | |
| fn display_body(&self) -> String { | |
| if let Some(message) = self.extract_error_message() { | |
| return message; | |
| } | |
| let trimmed_body = self.body.trim(); | |
| if trimmed_body.is_empty() { | |
| return "Unknown error".to_string(); | |
| } | |
| truncate_with_ellipsis(trimmed_body, UNEXPECTED_RESPONSE_BODY_MAX_BYTES) | |
| } | |
| fn extract_error_message(&self) -> Option<String> { | |
| let json = serde_json::from_str::<serde_json::Value>(&self.body).ok()?; | |
| let message = json | |
| .get("error") | |
| .and_then(|error| error.get("message")) | |
| .and_then(serde_json::Value::as_str)?; | |
| let message = message.trim(); | |
| if message.is_empty() { | |
| None | |
| } else { | |
| Some(message.to_string()) | |
| } | |
| } | |
| } | |
| impl std::fmt::Display for UnexpectedResponseError { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| let mut message = if let Some(user_message) = &self.user_message { | |
| user_message.clone() | |
| } else { | |
| let status = self.status; | |
| let body = self.display_body(); | |
| format!("unexpected status {status}: {body}") | |
| }; | |
| if let Some(url) = &self.url { | |
| message.push_str(&format!(", url: {url}")); | |
| } | |
| if let Some(cf_ray) = &self.cf_ray { | |
| message.push_str(&format!(", cf-ray: {cf_ray}")); | |
| } | |
| if let Some(id) = &self.request_id { | |
| message.push_str(&format!(", request id: {id}")); | |
| } | |
| if let Some(auth_error) = &self.identity_authorization_error { | |
| message.push_str(&format!(", auth error: {auth_error}")); | |
| } | |
| if let Some(error_code) = &self.identity_error_code { | |
| message.push_str(&format!(", auth error code: {error_code}")); | |
| } | |
| write!(f, "{message}") | |
| } | |
| } | |
| impl std::error::Error for UnexpectedResponseError {} | |
| fn truncate_with_ellipsis(text: &str, max_bytes: usize) -> String { | |
| if text.len() <= max_bytes { | |
| return text.to_string(); | |
| } | |
| let mut cut = max_bytes; | |
| while !text.is_char_boundary(cut) { | |
| cut = cut.saturating_sub(1); | |
| } | |
| let mut truncated = text[..cut].to_string(); | |
| truncated.push_str("..."); | |
| truncated | |
| } | |
| fn truncate_text(content: &str, policy: TruncationPolicy) -> String { | |
| match policy { | |
| TruncationPolicy::Bytes(bytes) => truncate_middle_chars(content, bytes), | |
| TruncationPolicy::Tokens(tokens) => truncate_middle_with_token_budget(content, tokens).0, | |
| } | |
| } | |
| pub struct RetryLimitReachedError { | |
| pub status: StatusCode, | |
| pub request_id: Option<String>, | |
| } | |
| impl std::fmt::Display for RetryLimitReachedError { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| write!( | |
| f, | |
| "exceeded retry limit, last status: {}{}", | |
| self.status, | |
| self.request_id | |
| .as_ref() | |
| .map(|id| format!(", request id: {id}")) | |
| .unwrap_or_default() | |
| ) | |
| } | |
| } | |
| pub struct UsageLimitReachedError { | |
| pub plan_type: Option<PlanType>, | |
| pub resets_at: Option<DateTime<Utc>>, | |
| pub rate_limits: Option<Box<RateLimitSnapshot>>, | |
| pub promo_message: Option<String>, | |
| pub rate_limit_reached_type: Option<RateLimitReachedType>, | |
| } | |
| impl std::fmt::Display for UsageLimitReachedError { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| // Reserve is a fallback for exhausted ordinary usage, so keep the standard | |
| // promo/plan recovery copy below instead of suggesting another model. | |
| if let Some(limit_name) = self | |
| .rate_limits | |
| .as_ref() | |
| .and_then(|snapshot| snapshot.limit_name.as_deref()) | |
| .map(str::trim) | |
| .filter(|name| !name.is_empty()) | |
| && !limit_name.eq_ignore_ascii_case("codex") | |
| && !limit_name.eq_ignore_ascii_case("gpt-reserve") | |
| { | |
| return write!( | |
| f, | |
| "You’ve hit your usage limit for {limit_name}. Switch to another model now,{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ); | |
| } | |
| if let Some(rate_limit_reached_type) = self.rate_limit_reached_type { | |
| match rate_limit_reached_type { | |
| RateLimitReachedType::WorkspaceOwnerCreditsDepleted => { | |
| return write!( | |
| f, | |
| "Your workspace is out of credits. Add credits to continue." | |
| ); | |
| } | |
| RateLimitReachedType::WorkspaceMemberCreditsDepleted => { | |
| return write!( | |
| f, | |
| "Your workspace is out of credits. Ask your workspace owner to refill in order to continue." | |
| ); | |
| } | |
| RateLimitReachedType::WorkspaceOwnerUsageLimitReached => { | |
| return write!( | |
| f, | |
| "You hit your spend cap set in your workspace. Increase your spend cap to continue." | |
| ); | |
| } | |
| RateLimitReachedType::WorkspaceMemberUsageLimitReached => { | |
| return write!( | |
| f, | |
| "You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue." | |
| ); | |
| } | |
| RateLimitReachedType::RateLimitReached => { | |
| // Generic limits intentionally use the existing promo or plan copy below. | |
| } | |
| } | |
| } | |
| if let Some(promo_message) = &self.promo_message { | |
| return write!( | |
| f, | |
| "You’ve hit your usage limit. {promo_message},{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ); | |
| } | |
| let message = match self.plan_type.as_ref() { | |
| Some(PlanType::Known(KnownPlan::Plus)) => format!( | |
| "You’ve hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ), | |
| Some(PlanType::Known( | |
| KnownPlan::Team | |
| | KnownPlan::SelfServeBusinessProLite | |
| | KnownPlan::SelfServeBusinessUsageBased | |
| | KnownPlan::Business | |
| | KnownPlan::Ent26 | |
| | KnownPlan::EnterpriseCbpAutomation | |
| | KnownPlan::EnterpriseCbpUsageBased, | |
| )) => { | |
| format!( | |
| "You’ve hit your usage limit. To get more access now, send a request to your admin{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ) | |
| } | |
| Some(PlanType::Known(KnownPlan::Free)) | Some(PlanType::Known(KnownPlan::Go)) => { | |
| format!( | |
| "You’ve hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus),{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ) | |
| } | |
| Some(PlanType::Known(KnownPlan::Pro | KnownPlan::ProLite)) => format!( | |
| "You’ve hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits{}", | |
| retry_suffix_after_or(self.resets_at.as_ref()) | |
| ), | |
| Some(PlanType::Known( | |
| KnownPlan::Enterprise | KnownPlan::Edu | KnownPlan::EduPlus | KnownPlan::EduPro, | |
| )) => format!( | |
| "You’ve hit your usage limit.{}", | |
| retry_suffix(self.resets_at.as_ref()) | |
| ), | |
| Some(PlanType::Unknown(_)) | None => format!( | |
| "You’ve hit your usage limit.{}", | |
| retry_suffix(self.resets_at.as_ref()) | |
| ), | |
| }; | |
| write!(f, "{message}") | |
| } | |
| } | |
| fn retry_suffix(resets_at: Option<&DateTime<Utc>>) -> String { | |
| if let Some(resets_at) = resets_at { | |
| let formatted = format_retry_timestamp(resets_at); | |
| format!(" Try again at {formatted}.") | |
| } else { | |
| " Try again later.".to_string() | |
| } | |
| } | |
| fn retry_suffix_after_or(resets_at: Option<&DateTime<Utc>>) -> String { | |
| if let Some(resets_at) = resets_at { | |
| let formatted = format_retry_timestamp(resets_at); | |
| format!(" or try again at {formatted}.") | |
| } else { | |
| " or try again later.".to_string() | |
| } | |
| } | |
| fn format_retry_timestamp(resets_at: &DateTime<Utc>) -> String { | |
| let local_reset = resets_at.with_timezone(&Local); | |
| let local_now = now_for_retry().with_timezone(&Local); | |
| if local_reset.date_naive() == local_now.date_naive() { | |
| local_reset.format("%-I:%M %p").to_string() | |
| } else { | |
| let suffix = day_suffix(local_reset.day()); | |
| local_reset | |
| .format(&format!("%b %-d{suffix}, %Y %-I:%M %p")) | |
| .to_string() | |
| } | |
| } | |
| fn day_suffix(day: u32) -> &'static str { | |
| match day { | |
| 11..=13 => "th", | |
| _ => match day % 10 { | |
| 1 => "st", | |
| 2 => "nd", // codespell:ignore | |
| 3 => "rd", | |
| _ => "th", | |
| }, | |
| } | |
| } | |
| thread_local! { | |
| static NOW_OVERRIDE: std::cell::RefCell<Option<DateTime<Utc>>> = | |
| const { std::cell::RefCell::new(None) }; | |
| } | |
| fn now_for_retry() -> DateTime<Utc> { | |
| { | |
| if let Some(now) = NOW_OVERRIDE.with(|cell| *cell.borrow()) { | |
| return now; | |
| } | |
| } | |
| Utc::now() | |
| } | |
| pub struct EnvVarError { | |
| /// Name of the environment variable that is missing. | |
| pub var: String, | |
| /// Optional instructions to help the user get a valid value for the | |
| /// variable and set it. | |
| pub instructions: Option<String>, | |
| } | |
| impl std::fmt::Display for EnvVarError { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| write!(f, "Missing environment variable: `{}`.", self.var)?; | |
| if let Some(instructions) = &self.instructions { | |
| write!(f, " {instructions}")?; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| pub fn get_error_message_ui(e: &CodexErr) -> String { | |
| let message = match e.details() { | |
| CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => { | |
| let aggregated = output.aggregated_output.text.trim(); | |
| if !aggregated.is_empty() { | |
| output.aggregated_output.text.clone() | |
| } else { | |
| let stderr = output.stderr.text.trim(); | |
| let stdout = output.stdout.text.trim(); | |
| match (stderr.is_empty(), stdout.is_empty()) { | |
| (false, false) => format!("{stderr}\n{stdout}"), | |
| (false, true) => output.stderr.text.clone(), | |
| (true, false) => output.stdout.text.clone(), | |
| (true, true) => format!( | |
| "command failed inside sandbox with exit code {}", | |
| output.exit_code | |
| ), | |
| } | |
| } | |
| } | |
| // Timeouts are not sandbox errors from a UX perspective; present them plainly. | |
| CodexErrorDetails::Sandbox(SandboxErr::Timeout { output }) => { | |
| format!( | |
| "error: command timed out after {} ms", | |
| output.duration.as_millis() | |
| ) | |
| } | |
| _ => e.to_string(), | |
| }; | |
| truncate_text( | |
| &message, | |
| TruncationPolicy::Bytes(ERROR_MESSAGE_UI_MAX_BYTES), | |
| ) | |
| } | |
| mod tests; | |