1pub const API_ERROR_MESSAGE_PREFIX: &str = "API Error";
6
7pub fn starts_with_api_error_prefix(text: &str) -> bool {
9 text.starts_with(API_ERROR_MESSAGE_PREFIX)
10 || text.starts_with(&format!("Please run /login · {}", API_ERROR_MESSAGE_PREFIX))
11}
12
13pub const PROMPT_TOO_LONG_ERROR_MESSAGE: &str = "Prompt is too long";
15
16pub fn is_prompt_too_long_message(msg: &ApiErrorMessage) -> bool {
18 if !msg.is_api_error_message {
19 return false;
20 }
21
22 let content = match &msg.content {
23 Some(c) => c,
24 None => return false,
25 };
26
27 content.starts_with(PROMPT_TOO_LONG_ERROR_MESSAGE)
29}
30
31pub fn parse_prompt_too_long_token_counts(raw_message: &str) -> (Option<u64>, Option<u64>) {
36 let lower = raw_message.to_lowercase();
39
40 if !lower.contains("prompt is too long") {
41 return (None, None);
42 }
43
44 let mut numbers: Vec<u64> = Vec::new();
46 let mut current_num = String::new();
47
48 for c in raw_message.chars() {
49 if c.is_ascii_digit() {
50 current_num.push(c);
51 } else if !current_num.is_empty() {
52 if let Ok(n) = current_num.parse() {
53 numbers.push(n);
54 }
55 current_num.clear();
56 }
57 }
58
59 if !current_num.is_empty() {
61 if let Ok(n) = current_num.parse() {
62 numbers.push(n);
63 }
64 }
65
66 if numbers.len() >= 2 {
68 if let Some(gt_pos) = raw_message.find('>') {
71 let before_gt = &raw_message[..gt_pos];
72 let after_gt = &raw_message[gt_pos..];
73
74 let mut before_nums: Vec<u64> = Vec::new();
76 let mut after_nums: Vec<u64> = Vec::new();
77
78 let mut current = String::new();
79 for c in before_gt.chars().rev() {
80 if c.is_ascii_digit() {
81 current.push(c);
82 } else if !current.is_empty() {
83 if let Ok(n) = current.chars().rev().collect::<String>().parse() {
84 before_nums.push(n);
85 }
86 current.clear();
87 }
88 }
89
90 current.clear();
91 for c in after_gt.chars() {
92 if c.is_ascii_digit() {
93 current.push(c);
94 } else if !current.is_empty() {
95 if let Ok(n) = current.parse() {
96 after_nums.push(n);
97 }
98 current.clear();
99 }
100 }
101
102 if let (Some(actual), Some(limit)) = (before_nums.first(), after_nums.first()) {
103 return (Some(*actual), Some(*limit));
104 }
105 }
106 }
107
108 if numbers.len() >= 2 {
110 return (Some(numbers[0]), Some(numbers[1]));
111 }
112
113 (None, None)
114}
115
116pub fn get_prompt_too_long_token_gap(msg: &ApiErrorMessage) -> Option<i64> {
119 if !is_prompt_too_long_message(msg) {
120 return None;
121 }
122
123 let error_details = msg.error_details.as_ref()?;
124
125 let (actual_tokens, limit_tokens) = parse_prompt_too_long_token_counts(error_details);
126
127 let actual = actual_tokens?;
128 let limit = limit_tokens?;
129
130 let gap = actual as i64 - limit as i64;
131 if gap > 0 {
132 Some(gap)
133 } else {
134 None
135 }
136}
137
138pub fn is_media_size_error(raw: &str) -> bool {
143 let lower = raw.to_lowercase();
144
145 (lower.contains("image exceeds") && lower.contains("maximum"))
146 || (lower.contains("image dimensions exceed") && lower.contains("many-image"))
147 || regex::Regex::new(r"maximum of \d+ PDF pages")
149 .map(|re| re.is_match(raw))
150 .unwrap_or(false)
151}
152
153pub fn is_media_size_error_message(msg: &ApiErrorMessage) -> bool {
155 msg.is_api_error_message
156 && msg
157 .error_details
158 .as_ref()
159 .map(|d| is_media_size_error(d))
160 .unwrap_or(false)
161}
162
163pub const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: &str = "Credit balance is too low";
165
166pub const INVALID_API_KEY_ERROR_MESSAGE: &str = "Not logged in · Please run /login";
168
169pub const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: &str = "Invalid API key · Fix external API key";
171
172pub const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH: &str =
174 "Your ANTHROPIC_API_KEY belongs to a disabled organization · Unset the environment variable to use your subscription instead";
175
176pub const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY: &str =
178 "Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable";
179
180pub const TOKEN_REVOKED_ERROR_MESSAGE: &str = "OAuth token revoked · Please run /login";
182
183pub const CCR_AUTH_ERROR_MESSAGE: &str =
185 "Authentication error · This may be a temporary network issue, please try again";
186
187pub const REPEATED_529_ERROR_MESSAGE: &str = "Repeated 529 Overloaded errors";
189
190pub const CUSTOM_OFF_SWITCH_MESSAGE: &str =
192 "Opus is experiencing high load, please use /model to switch to Sonnet";
193
194pub const API_TIMEOUT_ERROR_MESSAGE: &str = "Request timed out";
196
197pub fn get_pdf_too_large_error_message(is_non_interactive: bool) -> String {
199 let limits = "max 1000 pages, 32MB".to_string();
201
202 if is_non_interactive {
203 format!(
204 "PDF too large ({}). Try reading the file a different way (e.g., extract text with pdftotext).",
205 limits
206 )
207 } else {
208 format!(
209 "PDF too large ({}). Double press esc to go back and try again, or use pdftotext to convert to text first.",
210 limits
211 )
212 }
213}
214
215pub fn get_pdf_password_protected_error_message(is_non_interactive: bool) -> String {
217 if is_non_interactive {
218 "PDF is password protected. Try using a CLI tool to extract or convert the PDF.".to_string()
219 } else {
220 "PDF is password protected. Please double press esc to edit your message and try again."
221 .to_string()
222 }
223}
224
225pub fn get_pdf_invalid_error_message(is_non_interactive: bool) -> String {
227 if is_non_interactive {
228 "The PDF file was not valid. Try converting it to a text first (e.g., pdftotext)."
229 .to_string()
230 } else {
231 "The PDF file was not valid. Double press esc to go back and try again with a different file.".to_string()
232 }
233}
234
235pub fn get_image_too_large_error_message(is_non_interactive: bool) -> String {
237 if is_non_interactive {
238 "Image was too large. Try resizing the image or using a different approach.".to_string()
239 } else {
240 "Image was too large. Double press esc to go back and try again with a smaller image."
241 .to_string()
242 }
243}
244
245pub fn get_request_too_large_error_message(is_non_interactive: bool) -> String {
247 let limits = "max 32MB".to_string();
248
249 if is_non_interactive {
250 format!("Request too large ({}). Try with a smaller file.", limits)
251 } else {
252 format!(
253 "Request too large ({}). Double press esc to go back and try with a smaller file.",
254 limits
255 )
256 }
257}
258
259pub const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE: &str =
261 "Your account does not have access to Claude Code. Please run /login.";
262
263pub fn get_token_revoked_error_message(is_non_interactive: bool) -> String {
265 if is_non_interactive {
266 "Your account does not have access to Claude. Please login again or contact your administrator."
267 .to_string()
268 } else {
269 TOKEN_REVOKED_ERROR_MESSAGE.to_string()
270 }
271}
272
273pub fn get_oauth_org_not_allowed_error_message(is_non_interactive: bool) -> String {
275 if is_non_interactive {
276 "Your organization does not have access to Claude. Please login again or contact your administrator."
277 .to_string()
278 } else {
279 OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE.to_string()
280 }
281}
282
283#[derive(Debug, Clone, PartialEq)]
285#[allow(non_camel_case_types)]
286pub enum ApiErrorType {
287 aborted,
288 api_timeout,
289 repeated_529,
290 capacity_off_switch,
291 rate_limit,
292 server_overload,
293 prompt_too_long,
294 pdf_too_large,
295 pdf_password_protected,
296 image_too_large,
297 tool_use_mismatch,
298 unexpected_tool_result,
299 duplicate_tool_use_id,
300 invalid_model,
301 credit_balance_low,
302 invalid_api_key,
303 token_revoked,
304 oauth_org_not_allowed,
305 auth_error,
306 bedrock_model_access,
307 server_error,
308 client_error,
309 ssl_cert_error,
310 connection_error,
311 unknown,
312}
313
314#[derive(Debug, Clone, PartialEq)]
316#[allow(non_camel_case_types)]
317pub enum SDKAssistantMessageError {
318 rate_limit,
319 authentication_failed,
320 server_error,
321 unknown,
322}
323
324#[derive(Debug, Clone)]
326pub struct ApiErrorMessage {
327 pub is_api_error_message: bool,
328 pub content: Option<String>,
329 pub error: Option<String>,
330 pub error_details: Option<String>,
331}
332
333impl Default for ApiErrorMessage {
334 fn default() -> Self {
335 Self {
336 is_api_error_message: true,
337 content: Some(API_ERROR_MESSAGE_PREFIX.to_string()),
338 error: Some("unknown".to_string()),
339 error_details: None,
340 }
341 }
342}
343
344pub fn create_assistant_api_error_message(content: &str) -> ApiErrorMessage {
346 ApiErrorMessage {
347 is_api_error_message: true,
348 content: Some(content.to_string()),
349 error: Some("unknown".to_string()),
350 error_details: None,
351 }
352}
353
354pub fn create_assistant_api_error_message_with_options(
356 content: &str,
357 error: Option<&str>,
358 error_details: Option<&str>,
359) -> ApiErrorMessage {
360 ApiErrorMessage {
361 is_api_error_message: true,
362 content: Some(content.to_string()),
363 error: error.map(String::from),
364 error_details: error_details.map(String::from),
365 }
366}
367
368pub fn is_ccr_mode() -> bool {
373 false
375}
376
377pub fn is_valid_api_message(value: &serde_json::Value) -> bool {
379 value.get("content").is_some()
380 && value.get("model").is_some()
381 && value.get("usage").is_some()
382 && value["content"].is_array()
383 && value["model"].is_string()
384 && value["usage"].is_object()
385}
386
387#[derive(Debug, Clone, Default)]
389pub struct AmazonError {
390 pub output: Option<AmazonOutput>,
391 pub version: Option<String>,
392}
393
394#[derive(Debug, Clone, Default)]
395pub struct AmazonOutput {
396 pub type_: Option<String>,
397}
398
399impl AmazonError {
400 pub fn new() -> Self {
401 Self::default()
402 }
403
404 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
405 let output = value.get("Output")?;
406 let output_type = output
407 .get("__type")
408 .and_then(|v| v.as_str())
409 .map(String::from);
410
411 Some(AmazonError {
412 output: Some(AmazonOutput { type_: output_type }),
413 version: value
414 .get("Version")
415 .and_then(|v| v.as_str())
416 .map(String::from),
417 })
418 }
419}
420
421pub fn extract_unknown_error_format(value: &serde_json::Value) -> Option<String> {
423 if !value.is_object() {
425 return None;
426 }
427
428 if let Some(output) = value.get("Output") {
430 if let Some(output_type) = output.get("__type").and_then(|v| v.as_str()) {
431 return Some(output_type.to_string());
432 }
433 }
434
435 None
436}
437
438pub fn classify_api_error(error_message: &str, status: Option<u16>) -> ApiErrorType {
441 let lower = error_message.to_lowercase();
442
443 if error_message == "Request was aborted." {
445 return ApiErrorType::aborted;
446 }
447
448 if lower.contains("timeout") {
450 return ApiErrorType::api_timeout;
451 }
452
453 if error_message.contains(REPEATED_529_ERROR_MESSAGE) {
455 return ApiErrorType::repeated_529;
456 }
457
458 if error_message.contains(CUSTOM_OFF_SWITCH_MESSAGE) {
460 return ApiErrorType::capacity_off_switch;
461 }
462
463 if status == Some(429) {
465 return ApiErrorType::rate_limit;
466 }
467
468 if status == Some(529) || error_message.contains(r#""type":"overloaded_error""#) {
470 return ApiErrorType::server_overload;
471 }
472
473 if lower.contains(&PROMPT_TOO_LONG_ERROR_MESSAGE.to_lowercase()) {
475 return ApiErrorType::prompt_too_long;
476 }
477
478 if is_media_size_error(error_message) && error_message.to_lowercase().contains("pdf") {
480 if error_message.to_lowercase().contains("password") {
481 return ApiErrorType::pdf_password_protected;
482 }
483 return ApiErrorType::pdf_too_large;
484 }
485
486 if status == Some(400)
488 && lower.contains("image")
489 && lower.contains("exceeds")
490 && lower.contains("maximum")
491 {
492 return ApiErrorType::image_too_large;
493 }
494
495 if status == Some(400)
497 && lower.contains("image dimensions exceed")
498 && lower.contains("many-image")
499 {
500 return ApiErrorType::image_too_large;
501 }
502
503 if status == Some(400)
505 && error_message.contains("`tool_use` ids were found without `tool_result`")
506 {
507 return ApiErrorType::tool_use_mismatch;
508 }
509
510 if status == Some(400)
511 && error_message.contains("unexpected `tool_use_id` found in `tool_result`")
512 {
513 return ApiErrorType::unexpected_tool_result;
514 }
515
516 if status == Some(400) && error_message.contains("`tool_use` ids must be unique") {
517 return ApiErrorType::duplicate_tool_use_id;
518 }
519
520 if status == Some(400) && lower.contains("invalid model name") {
522 return ApiErrorType::invalid_model;
523 }
524
525 if lower.contains(&CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.to_lowercase()) {
527 return ApiErrorType::credit_balance_low;
528 }
529
530 if lower.contains("x-api-key") {
532 return ApiErrorType::invalid_api_key;
533 }
534
535 if status == Some(403) && error_message.contains("OAuth token has been revoked") {
536 return ApiErrorType::token_revoked;
537 }
538
539 if (status == Some(401) || status == Some(403))
540 && error_message
541 .contains("OAuth authentication is currently not allowed for this organization")
542 {
543 return ApiErrorType::oauth_org_not_allowed;
544 }
545
546 if status == Some(401) || status == Some(403) {
548 return ApiErrorType::auth_error;
549 }
550
551 if let Some(s) = status {
558 if s >= 500 {
559 return ApiErrorType::server_error;
560 }
561 if s >= 400 {
562 return ApiErrorType::client_error;
563 }
564 }
565
566 if lower.contains("connection") || lower.contains("ssl") || lower.contains("tls") {
568 if lower.contains("ssl") || lower.contains("certificate") {
569 return ApiErrorType::ssl_cert_error;
570 }
571 return ApiErrorType::connection_error;
572 }
573
574 ApiErrorType::unknown
575}
576
577pub fn categorize_retryable_api_error(status: u16, message: &str) -> SDKAssistantMessageError {
579 if status == 529 || message.contains(r#""type":"overloaded_error""#) {
580 return SDKAssistantMessageError::rate_limit;
581 }
582 if status == 429 {
583 return SDKAssistantMessageError::rate_limit;
584 }
585 if status == 401 || status == 403 {
586 return SDKAssistantMessageError::authentication_failed;
587 }
588 if status >= 408 {
589 return SDKAssistantMessageError::server_error;
590 }
591 SDKAssistantMessageError::unknown
592}
593
594pub fn get_error_message_if_refusal(
596 stop_reason: Option<&str>,
597 model: &str,
598 is_non_interactive: bool,
599) -> Option<ApiErrorMessage> {
600 if stop_reason != Some("refusal") {
601 return None;
602 }
603
604 let base_message = if is_non_interactive {
608 format!(
609 "{}: 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.",
610 API_ERROR_MESSAGE_PREFIX
611 )
612 } else {
613 format!(
614 "{}: 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.",
615 API_ERROR_MESSAGE_PREFIX
616 )
617 };
618
619 let model_suggestion = if model != "claude-sonnet-4-20250514" {
620 " If you are seeing this refusal repeatedly, try running /model claude-sonnet-4-20250514 to switch models."
621 } else {
622 ""
623 };
624
625 Some(create_assistant_api_error_message_with_options(
626 &(base_message + model_suggestion),
627 Some("invalid_request"),
628 None,
629 ))
630}
631
632pub const NO_RESPONSE_REQUESTED: &str = "NO_RESPONSE_REQUESTED";
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn test_starts_with_api_error_prefix() {
641 assert!(starts_with_api_error_prefix(
642 "API Error: something went wrong"
643 ));
644 assert!(starts_with_api_error_prefix(
645 "Please run /login · API Error: test"
646 ));
647 assert!(!starts_with_api_error_prefix("Something else"));
648 }
649
650 #[test]
651 fn test_is_terminal_task_status() {
652 assert!(!is_media_size_error("some random error"));
653 assert!(is_media_size_error("image exceeds 5 MB maximum"));
654 assert!(is_media_size_error(
655 "image dimensions exceed limit for many-image"
656 ));
657 assert!(is_media_size_error("maximum of 1000 PDF pages"));
658 }
659
660 #[test]
661 fn test_classify_api_error() {
662 assert_eq!(
663 classify_api_error("Request was aborted.", None),
664 ApiErrorType::aborted
665 );
666 assert_eq!(
667 classify_api_error("timeout error", None),
668 ApiErrorType::api_timeout
669 );
670 assert_eq!(
671 classify_api_error("rate limit", Some(429)),
672 ApiErrorType::rate_limit
673 );
674 assert_eq!(
675 classify_api_error("server overloaded", Some(529)),
676 ApiErrorType::server_overload
677 );
678 assert_eq!(
679 classify_api_error("Prompt is too long", None),
680 ApiErrorType::prompt_too_long
681 );
682 }
683
684 #[test]
685 fn test_categorize_retryable_api_error() {
686 assert_eq!(
687 categorize_retryable_api_error(529, "overloaded"),
688 SDKAssistantMessageError::rate_limit
689 );
690 assert_eq!(
691 categorize_retryable_api_error(429, "rate limit"),
692 SDKAssistantMessageError::rate_limit
693 );
694 assert_eq!(
695 categorize_retryable_api_error(401, "unauthorized"),
696 SDKAssistantMessageError::authentication_failed
697 );
698 assert_eq!(
699 categorize_retryable_api_error(500, "server error"),
700 SDKAssistantMessageError::server_error
701 );
702 }
703
704 #[test]
705 fn test_parse_prompt_too_long_token_counts() {
706 let (actual, limit) = parse_prompt_too_long_token_counts(
707 "prompt is too long: 137500 tokens > 135000 maximum",
708 );
709 assert_eq!(actual, Some(137500));
710 assert_eq!(limit, Some(135000));
711 }
712}