pub const API_ERROR_MESSAGE_PREFIX: &str = "API Error";
pub fn sanitize_html_error(text: &str) -> String {
let lower = text.to_lowercase();
if lower.contains("<!doctype html") || lower.contains("<html") {
if let Some(title_start) = text.find("<title>") {
let after_start = &text[title_start + "<title>".len()..];
if let Some(title_end) = after_start.find("</title>") {
let title = after_start[..title_end].trim().to_string();
if !title.is_empty() {
return title;
}
}
}
String::new()
} else {
text.to_string()
}
}
pub fn starts_with_api_error_prefix(text: &str) -> bool {
text.starts_with(API_ERROR_MESSAGE_PREFIX)
|| text.starts_with(&format!("Please run /login · {}", API_ERROR_MESSAGE_PREFIX))
}
pub const PROMPT_TOO_LONG_ERROR_MESSAGE: &str = "Prompt is too long";
pub fn is_prompt_too_long_message(msg: &ApiErrorMessage) -> bool {
if !msg.is_api_error_message {
return false;
}
let content = match &msg.content {
Some(c) => c,
None => return false,
};
content.starts_with(PROMPT_TOO_LONG_ERROR_MESSAGE)
}
pub fn parse_prompt_too_long_token_counts(raw_message: &str) -> (Option<u64>, Option<u64>) {
let lower = raw_message.to_lowercase();
if !lower.contains("prompt is too long") {
return (None, None);
}
let mut numbers: Vec<u64> = Vec::new();
let mut current_num = String::new();
for c in raw_message.chars() {
if c.is_ascii_digit() {
current_num.push(c);
} else if !current_num.is_empty() {
if let Ok(n) = current_num.parse() {
numbers.push(n);
}
current_num.clear();
}
}
if !current_num.is_empty() {
if let Ok(n) = current_num.parse() {
numbers.push(n);
}
}
if numbers.len() >= 2 {
if let Some(gt_pos) = raw_message.find('>') {
let before_gt = &raw_message[..gt_pos];
let after_gt = &raw_message[gt_pos..];
let mut before_nums: Vec<u64> = Vec::new();
let mut after_nums: Vec<u64> = Vec::new();
let mut current = String::new();
for c in before_gt.chars().rev() {
if c.is_ascii_digit() {
current.push(c);
} else if !current.is_empty() {
if let Ok(n) = current.chars().rev().collect::<String>().parse() {
before_nums.push(n);
}
current.clear();
}
}
current.clear();
for c in after_gt.chars() {
if c.is_ascii_digit() {
current.push(c);
} else if !current.is_empty() {
if let Ok(n) = current.parse() {
after_nums.push(n);
}
current.clear();
}
}
if let (Some(actual), Some(limit)) = (before_nums.first(), after_nums.first()) {
return (Some(*actual), Some(*limit));
}
}
}
if numbers.len() >= 2 {
return (Some(numbers[0]), Some(numbers[1]));
}
(None, None)
}
pub fn get_prompt_too_long_token_gap(msg: &ApiErrorMessage) -> Option<i64> {
if !is_prompt_too_long_message(msg) {
return None;
}
let error_details = msg.error_details.as_ref()?;
let (actual_tokens, limit_tokens) = parse_prompt_too_long_token_counts(error_details);
let actual = actual_tokens?;
let limit = limit_tokens?;
let gap = actual as i64 - limit as i64;
if gap > 0 { Some(gap) } else { None }
}
pub fn is_media_size_error(raw: &str) -> bool {
let lower = raw.to_lowercase();
(lower.contains("image exceeds") && lower.contains("maximum"))
|| (lower.contains("image dimensions exceed") && lower.contains("many-image"))
|| regex::Regex::new(r"maximum of \d+ PDF pages")
.map(|re| re.is_match(raw))
.unwrap_or(false)
}
pub fn is_media_size_error_message(msg: &ApiErrorMessage) -> bool {
msg.is_api_error_message
&& msg
.error_details
.as_ref()
.map(|d| is_media_size_error(d))
.unwrap_or(false)
}
pub const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: &str = "Credit balance is too low";
pub const INVALID_API_KEY_ERROR_MESSAGE: &str = "Not logged in · Please run /login";
pub const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: &str = "Invalid API key · Fix external API key";
pub const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH: &str = "Your ANTHROPIC_API_KEY belongs to a disabled organization · Unset the environment variable to use your subscription instead";
pub const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY: &str = "Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable";
pub const TOKEN_REVOKED_ERROR_MESSAGE: &str = "OAuth token revoked · Please run /login";
pub const CCR_AUTH_ERROR_MESSAGE: &str =
"Authentication error · This may be a temporary network issue, please try again";
pub const REPEATED_529_ERROR_MESSAGE: &str = "Repeated 529 Overloaded errors";
pub const CUSTOM_OFF_SWITCH_MESSAGE: &str =
"Opus is experiencing high load, please use /model to switch to Sonnet";
pub const API_TIMEOUT_ERROR_MESSAGE: &str = "Request timed out";
pub fn get_pdf_too_large_error_message(is_non_interactive: bool) -> String {
let limits = "max 1000 pages, 32MB".to_string();
if is_non_interactive {
format!(
"PDF too large ({}). Try reading the file a different way (e.g., extract text with pdftotext).",
limits
)
} else {
format!(
"PDF too large ({}). Double press esc to go back and try again, or use pdftotext to convert to text first.",
limits
)
}
}
pub fn get_pdf_password_protected_error_message(is_non_interactive: bool) -> String {
if is_non_interactive {
"PDF is password protected. Try using a CLI tool to extract or convert the PDF.".to_string()
} else {
"PDF is password protected. Please double press esc to edit your message and try again."
.to_string()
}
}
pub fn get_pdf_invalid_error_message(is_non_interactive: bool) -> String {
if is_non_interactive {
"The PDF file was not valid. Try converting it to a text first (e.g., pdftotext)."
.to_string()
} else {
"The PDF file was not valid. Double press esc to go back and try again with a different file.".to_string()
}
}
pub fn get_image_too_large_error_message(is_non_interactive: bool) -> String {
if is_non_interactive {
"Image was too large. Try resizing the image or using a different approach.".to_string()
} else {
"Image was too large. Double press esc to go back and try again with a smaller image."
.to_string()
}
}
pub fn get_request_too_large_error_message(is_non_interactive: bool) -> String {
let limits = "max 32MB".to_string();
if is_non_interactive {
format!("Request too large ({}). Try with a smaller file.", limits)
} else {
format!(
"Request too large ({}). Double press esc to go back and try with a smaller file.",
limits
)
}
}
pub const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE: &str =
"Your account does not have access to Claude Code. Please run /login.";
pub fn get_token_revoked_error_message(is_non_interactive: bool) -> String {
if is_non_interactive {
"Your account does not have access to Claude. Please login again or contact your administrator."
.to_string()
} else {
TOKEN_REVOKED_ERROR_MESSAGE.to_string()
}
}
pub fn get_oauth_org_not_allowed_error_message(is_non_interactive: bool) -> String {
if is_non_interactive {
"Your organization does not have access to Claude. Please login again or contact your administrator."
.to_string()
} else {
OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE.to_string()
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub enum ApiErrorType {
aborted,
api_timeout,
repeated_529,
capacity_off_switch,
rate_limit,
server_overload,
prompt_too_long,
pdf_too_large,
pdf_password_protected,
image_too_large,
tool_use_mismatch,
unexpected_tool_result,
duplicate_tool_use_id,
invalid_model,
credit_balance_low,
invalid_api_key,
token_revoked,
oauth_org_not_allowed,
auth_error,
bedrock_model_access,
server_error,
client_error,
ssl_cert_error,
connection_error,
unknown,
}
#[derive(Debug, Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub enum SDKAssistantMessageError {
rate_limit,
authentication_failed,
server_error,
unknown,
}
#[derive(Debug, Clone)]
pub struct ApiErrorMessage {
pub is_api_error_message: bool,
pub content: Option<String>,
pub error: Option<String>,
pub error_details: Option<String>,
}
impl Default for ApiErrorMessage {
fn default() -> Self {
Self {
is_api_error_message: true,
content: Some(API_ERROR_MESSAGE_PREFIX.to_string()),
error: Some("unknown".to_string()),
error_details: None,
}
}
}
pub fn create_assistant_api_error_message(content: &str) -> ApiErrorMessage {
ApiErrorMessage {
is_api_error_message: true,
content: Some(content.to_string()),
error: Some("unknown".to_string()),
error_details: None,
}
}
pub fn create_assistant_api_error_message_with_options(
content: &str,
error: Option<&str>,
error_details: Option<&str>,
) -> ApiErrorMessage {
ApiErrorMessage {
is_api_error_message: true,
content: Some(content.to_string()),
error: error.map(String::from),
error_details: error_details.map(String::from),
}
}
pub fn is_ccr_mode() -> bool {
false
}
pub fn is_valid_api_message(value: &serde_json::Value) -> bool {
value.get("content").is_some()
&& value.get("model").is_some()
&& value.get("usage").is_some()
&& value["content"].is_array()
&& value["model"].is_string()
&& value["usage"].is_object()
}
#[derive(Debug, Clone, Default)]
pub struct AmazonError {
pub output: Option<AmazonOutput>,
pub version: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AmazonOutput {
pub type_: Option<String>,
}
impl AmazonError {
pub fn new() -> Self {
Self::default()
}
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
let output = value.get("Output")?;
let output_type = output
.get("__type")
.and_then(|v| v.as_str())
.map(String::from);
Some(AmazonError {
output: Some(AmazonOutput { type_: output_type }),
version: value
.get("Version")
.and_then(|v| v.as_str())
.map(String::from),
})
}
}
pub fn extract_unknown_error_format(value: &serde_json::Value) -> Option<String> {
if !value.is_object() {
return None;
}
if let Some(output) = value.get("Output") {
if let Some(output_type) = output.get("__type").and_then(|v| v.as_str()) {
return Some(output_type.to_string());
}
}
None
}
pub fn classify_api_error(error_message: &str, status: Option<u16>) -> ApiErrorType {
let lower = error_message.to_lowercase();
if error_message == "Request was aborted." {
return ApiErrorType::aborted;
}
if lower.contains("timeout") {
return ApiErrorType::api_timeout;
}
if error_message.contains(REPEATED_529_ERROR_MESSAGE) {
return ApiErrorType::repeated_529;
}
if error_message.contains(CUSTOM_OFF_SWITCH_MESSAGE) {
return ApiErrorType::capacity_off_switch;
}
if status == Some(429) {
return ApiErrorType::rate_limit;
}
if status == Some(529) || error_message.contains(r#""type":"overloaded_error""#) {
return ApiErrorType::server_overload;
}
if lower.contains(&PROMPT_TOO_LONG_ERROR_MESSAGE.to_lowercase()) {
return ApiErrorType::prompt_too_long;
}
if is_media_size_error(error_message) && error_message.to_lowercase().contains("pdf") {
if error_message.to_lowercase().contains("password") {
return ApiErrorType::pdf_password_protected;
}
return ApiErrorType::pdf_too_large;
}
if status == Some(400)
&& lower.contains("image")
&& lower.contains("exceeds")
&& lower.contains("maximum")
{
return ApiErrorType::image_too_large;
}
if status == Some(400)
&& lower.contains("image dimensions exceed")
&& lower.contains("many-image")
{
return ApiErrorType::image_too_large;
}
if status == Some(400)
&& error_message.contains("`tool_use` ids were found without `tool_result`")
{
return ApiErrorType::tool_use_mismatch;
}
if status == Some(400)
&& error_message.contains("unexpected `tool_use_id` found in `tool_result`")
{
return ApiErrorType::unexpected_tool_result;
}
if status == Some(400) && error_message.contains("`tool_use` ids must be unique") {
return ApiErrorType::duplicate_tool_use_id;
}
if status == Some(400) && lower.contains("invalid model name") {
return ApiErrorType::invalid_model;
}
if lower.contains(&CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.to_lowercase()) {
return ApiErrorType::credit_balance_low;
}
if lower.contains("x-api-key") {
return ApiErrorType::invalid_api_key;
}
if status == Some(403) && error_message.contains("OAuth token has been revoked") {
return ApiErrorType::token_revoked;
}
if (status == Some(401) || status == Some(403))
&& error_message
.contains("OAuth authentication is currently not allowed for this organization")
{
return ApiErrorType::oauth_org_not_allowed;
}
if status == Some(401) || status == Some(403) {
return ApiErrorType::auth_error;
}
if let Some(s) = status {
if s >= 500 {
return ApiErrorType::server_error;
}
if s >= 400 {
return ApiErrorType::client_error;
}
}
if lower.contains("connection") || lower.contains("ssl") || lower.contains("tls") {
if lower.contains("ssl") || lower.contains("certificate") {
return ApiErrorType::ssl_cert_error;
}
return ApiErrorType::connection_error;
}
ApiErrorType::unknown
}
pub fn categorize_retryable_api_error(status: u16, message: &str) -> SDKAssistantMessageError {
if status == 529 || message.contains(r#""type":"overloaded_error""#) {
return SDKAssistantMessageError::rate_limit;
}
if status == 429 {
return SDKAssistantMessageError::rate_limit;
}
if status == 401 || status == 403 {
return SDKAssistantMessageError::authentication_failed;
}
if status >= 408 {
return SDKAssistantMessageError::server_error;
}
SDKAssistantMessageError::unknown
}
pub fn get_error_message_if_refusal(
stop_reason: Option<&str>,
model: &str,
is_non_interactive: bool,
) -> Option<ApiErrorMessage> {
if stop_reason != Some("refusal") {
return None;
}
let base_message = if is_non_interactive {
format!(
"{}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Try rephrasing the request or attempting a different approach.",
API_ERROR_MESSAGE_PREFIX
)
} else {
format!(
"{}: Claude Code is unable to respond to this request, which appears to violate our Usage Policy (https://www.anthropic.com/legal/aup). Please double press esc to edit your last message or start a new session for Claude Code to assist with a different task.",
API_ERROR_MESSAGE_PREFIX
)
};
let model_suggestion = if model != "claude-sonnet-4-20250514" {
" If you are seeing this refusal repeatedly, try running /model claude-sonnet-4-20250514 to switch models."
} else {
""
};
Some(create_assistant_api_error_message_with_options(
&(base_message + model_suggestion),
Some("invalid_request"),
None,
))
}
pub const NO_RESPONSE_REQUESTED: &str = "NO_RESPONSE_REQUESTED";
pub fn error_to_api_message(error_msg: &str, status: Option<u16>) -> ApiErrorMessage {
let lower = error_msg.to_lowercase();
if error_msg == "Request was aborted." || error_msg == "User aborted the request" {
return create_assistant_api_error_message_with_options(
"Request was aborted",
Some("aborted"),
Some(error_msg),
);
}
if lower.contains("timeout") || lower.contains("timed out") {
return create_assistant_api_error_message_with_options(
API_TIMEOUT_ERROR_MESSAGE,
Some("unknown"),
Some(error_msg),
);
}
if error_msg.contains(REPEATED_529_ERROR_MESSAGE) {
return create_assistant_api_error_message_with_options(
REPEATED_529_ERROR_MESSAGE,
Some("server_overload"),
Some(error_msg),
);
}
if status == Some(429) || lower.contains("rate_limit") || lower.contains("rate limit") {
return create_assistant_api_error_message_with_options(
"Rate limit exceeded. Please try again shortly.",
Some("rate_limit"),
Some(error_msg),
);
}
if status == Some(529) || lower.contains("overloaded") {
return create_assistant_api_error_message_with_options(
"Server is overloaded. Retrying...",
Some("server_overload"),
Some(error_msg),
);
}
if lower.contains(&PROMPT_TOO_LONG_ERROR_MESSAGE.to_lowercase())
|| lower.contains("prompt is too long")
|| (status == Some(413) && lower.contains("too long"))
{
return create_assistant_api_error_message_with_options(
PROMPT_TOO_LONG_ERROR_MESSAGE,
Some("invalid_request"),
Some(error_msg),
);
}
if is_media_size_error(error_msg) && lower.contains("pdf") {
if lower.contains("password") {
return create_assistant_api_error_message_with_options(
&get_pdf_password_protected_error_message(false),
Some("invalid_request"),
Some(error_msg),
);
}
return create_assistant_api_error_message_with_options(
&get_pdf_too_large_error_message(false),
Some("invalid_request"),
Some(error_msg),
);
}
if (status == Some(400) && lower.contains("image") && lower.contains("exceeds") && lower.contains("maximum"))
|| (lower.contains("image exceeds") && lower.contains("maximum"))
{
return create_assistant_api_error_message_with_options(
&get_image_too_large_error_message(false),
Some("invalid_request"),
Some(error_msg),
);
}
if status == Some(413) {
return create_assistant_api_error_message_with_options(
&get_request_too_large_error_message(false),
Some("invalid_request"),
Some(error_msg),
);
}
if status == Some(400) && error_msg.contains("`tool_use` ids were found without `tool_result`") {
return create_assistant_api_error_message_with_options(
&format!("{}: Tool use mismatch. Try /rewind to fix.", API_ERROR_MESSAGE_PREFIX),
Some("invalid_request"),
Some(error_msg),
);
}
if status == Some(400) && error_msg.contains("`tool_use` ids must be unique") {
return create_assistant_api_error_message_with_options(
&format!("{}: Duplicate tool use ID. Try /rewind to fix.", API_ERROR_MESSAGE_PREFIX),
Some("invalid_request"),
Some(error_msg),
);
}
if status == Some(400) && lower.contains("invalid model") {
return create_assistant_api_error_message_with_options(
"Model is not available. Try /model to switch.",
Some("invalid_request"),
Some(error_msg),
);
}
if lower.contains(&CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.to_lowercase()) {
return create_assistant_api_error_message_with_options(
CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE,
Some("billing_error"),
Some(error_msg),
);
}
if lower.contains("x-api-key") || lower.contains("api key") && status == Some(401) {
return create_assistant_api_error_message_with_options(
INVALID_API_KEY_ERROR_MESSAGE,
Some("authentication_failed"),
Some(error_msg),
);
}
if status == Some(403) && error_msg.contains("OAuth token has been revoked") {
return create_assistant_api_error_message_with_options(
TOKEN_REVOKED_ERROR_MESSAGE,
Some("authentication_failed"),
Some(error_msg),
);
}
if (status == Some(401) || status == Some(403))
&& error_msg.contains("OAuth authentication is currently not allowed for this organization")
{
return create_assistant_api_error_message_with_options(
OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE,
Some("authentication_failed"),
Some(error_msg),
);
}
if status == Some(401) || status == Some(403) {
return create_assistant_api_error_message_with_options(
"Authentication failed.",
Some("authentication_failed"),
Some(error_msg),
);
}
if lower.contains("connection") || lower.contains("ssl") || lower.contains("tls") {
return create_assistant_api_error_message_with_options(
"Connection error. Check your network and try again.",
Some("connection_error"),
Some(error_msg),
);
}
if lower.starts_with("api error") {
create_assistant_api_error_message_with_options(error_msg, Some("unknown"), Some(error_msg))
} else {
create_assistant_api_error_message_with_options(
&format!("{}: {}", API_ERROR_MESSAGE_PREFIX, error_msg),
Some("unknown"),
Some(error_msg),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_starts_with_api_error_prefix() {
assert!(starts_with_api_error_prefix(
"API Error: something went wrong"
));
assert!(starts_with_api_error_prefix(
"Please run /login · API Error: test"
));
assert!(!starts_with_api_error_prefix("Something else"));
}
#[test]
fn test_is_terminal_task_status() {
assert!(!is_media_size_error("some random error"));
assert!(is_media_size_error("image exceeds 5 MB maximum"));
assert!(is_media_size_error(
"image dimensions exceed limit for many-image"
));
assert!(is_media_size_error("maximum of 1000 PDF pages"));
}
#[test]
fn test_classify_api_error() {
assert_eq!(
classify_api_error("Request was aborted.", None),
ApiErrorType::aborted
);
assert_eq!(
classify_api_error("timeout error", None),
ApiErrorType::api_timeout
);
assert_eq!(
classify_api_error("rate limit", Some(429)),
ApiErrorType::rate_limit
);
assert_eq!(
classify_api_error("server overloaded", Some(529)),
ApiErrorType::server_overload
);
assert_eq!(
classify_api_error("Prompt is too long", None),
ApiErrorType::prompt_too_long
);
}
#[test]
fn test_categorize_retryable_api_error() {
assert_eq!(
categorize_retryable_api_error(529, "overloaded"),
SDKAssistantMessageError::rate_limit
);
assert_eq!(
categorize_retryable_api_error(429, "rate limit"),
SDKAssistantMessageError::rate_limit
);
assert_eq!(
categorize_retryable_api_error(401, "unauthorized"),
SDKAssistantMessageError::authentication_failed
);
assert_eq!(
categorize_retryable_api_error(500, "server error"),
SDKAssistantMessageError::server_error
);
}
#[test]
fn test_sanitize_html_error() {
let html = "<html><head><title>502 Bad Gateway</title></head><body><p>error</p></body></html>";
assert_eq!(sanitize_html_error(html), "502 Bad Gateway");
let html_no_title = "<html><body><p>error</p></body></html>";
assert_eq!(sanitize_html_error(html_no_title), "");
let doctype = "<!DOCTYPE html><html><head><title>503 Service Unavailable</title></head>";
assert_eq!(sanitize_html_error(doctype), "503 Service Unavailable");
let plain = "{\"error\":{\"message\":\"rate limited\"}}";
assert_eq!(sanitize_html_error(plain), "{\"error\":{\"message\":\"rate limited\"}}");
assert_eq!(sanitize_html_error(""), "");
}
#[test]
fn test_parse_prompt_too_long_token_counts() {
let (actual, limit) = parse_prompt_too_long_token_counts(
"prompt is too long: 137500 tokens > 135000 maximum",
);
assert_eq!(actual, Some(137500));
assert_eq!(limit, Some(135000));
}
}