1pub const API_ERROR_MESSAGE_PREFIX: &str = "API Error";
6
7pub fn sanitize_html_error(text: &str) -> String {
11 let lower = text.to_lowercase();
12 if lower.contains("<!doctype html") || lower.contains("<html") {
13 if let Some(title_start) = text.find("<title>") {
15 let after_start = &text[title_start + "<title>".len()..];
16 if let Some(title_end) = after_start.find("</title>") {
17 let title = after_start[..title_end].trim().to_string();
18 if !title.is_empty() {
19 return title;
20 }
21 }
22 }
23 String::new()
25 } else {
26 text.to_string()
27 }
28}
29
30pub fn starts_with_api_error_prefix(text: &str) -> bool {
32 text.starts_with(API_ERROR_MESSAGE_PREFIX)
33 || text.starts_with(&format!("Please run /login · {}", API_ERROR_MESSAGE_PREFIX))
34}
35
36pub const PROMPT_TOO_LONG_ERROR_MESSAGE: &str = "Prompt is too long";
38
39pub fn is_prompt_too_long_message(msg: &ApiErrorMessage) -> bool {
41 if !msg.is_api_error_message {
42 return false;
43 }
44
45 let content = match &msg.content {
46 Some(c) => c,
47 None => return false,
48 };
49
50 content.starts_with(PROMPT_TOO_LONG_ERROR_MESSAGE)
52}
53
54pub fn parse_prompt_too_long_token_counts(raw_message: &str) -> (Option<u64>, Option<u64>) {
59 let lower = raw_message.to_lowercase();
62
63 if !lower.contains("prompt is too long") {
64 return (None, None);
65 }
66
67 let mut numbers: Vec<u64> = Vec::new();
69 let mut current_num = String::new();
70
71 for c in raw_message.chars() {
72 if c.is_ascii_digit() {
73 current_num.push(c);
74 } else if !current_num.is_empty() {
75 if let Ok(n) = current_num.parse() {
76 numbers.push(n);
77 }
78 current_num.clear();
79 }
80 }
81
82 if !current_num.is_empty() {
84 if let Ok(n) = current_num.parse() {
85 numbers.push(n);
86 }
87 }
88
89 if numbers.len() >= 2 {
91 if let Some(gt_pos) = raw_message.find('>') {
94 let before_gt = &raw_message[..gt_pos];
95 let after_gt = &raw_message[gt_pos..];
96
97 let mut before_nums: Vec<u64> = Vec::new();
99 let mut after_nums: Vec<u64> = Vec::new();
100
101 let mut current = String::new();
102 for c in before_gt.chars().rev() {
103 if c.is_ascii_digit() {
104 current.push(c);
105 } else if !current.is_empty() {
106 if let Ok(n) = current.chars().rev().collect::<String>().parse() {
107 before_nums.push(n);
108 }
109 current.clear();
110 }
111 }
112
113 current.clear();
114 for c in after_gt.chars() {
115 if c.is_ascii_digit() {
116 current.push(c);
117 } else if !current.is_empty() {
118 if let Ok(n) = current.parse() {
119 after_nums.push(n);
120 }
121 current.clear();
122 }
123 }
124
125 if let (Some(actual), Some(limit)) = (before_nums.first(), after_nums.first()) {
126 return (Some(*actual), Some(*limit));
127 }
128 }
129 }
130
131 if numbers.len() >= 2 {
133 return (Some(numbers[0]), Some(numbers[1]));
134 }
135
136 (None, None)
137}
138
139pub fn get_prompt_too_long_token_gap(msg: &ApiErrorMessage) -> Option<i64> {
142 if !is_prompt_too_long_message(msg) {
143 return None;
144 }
145
146 let error_details = msg.error_details.as_ref()?;
147
148 let (actual_tokens, limit_tokens) = parse_prompt_too_long_token_counts(error_details);
149
150 let actual = actual_tokens?;
151 let limit = limit_tokens?;
152
153 let gap = actual as i64 - limit as i64;
154 if gap > 0 { Some(gap) } else { None }
155}
156
157pub fn is_media_size_error(raw: &str) -> bool {
162 let lower = raw.to_lowercase();
163
164 (lower.contains("image exceeds") && lower.contains("maximum"))
165 || (lower.contains("image dimensions exceed") && lower.contains("many-image"))
166 || regex::Regex::new(r"maximum of \d+ PDF pages")
168 .map(|re| re.is_match(raw))
169 .unwrap_or(false)
170}
171
172pub fn is_media_size_error_message(msg: &ApiErrorMessage) -> bool {
174 msg.is_api_error_message
175 && msg
176 .error_details
177 .as_ref()
178 .map(|d| is_media_size_error(d))
179 .unwrap_or(false)
180}
181
182pub const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: &str = "Credit balance is too low";
184
185pub const INVALID_API_KEY_ERROR_MESSAGE: &str = "Not logged in · Please run /login";
187
188pub const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: &str = "Invalid API key · Fix external API key";
190
191pub 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";
193
194pub const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY: &str = "Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable";
196
197pub const TOKEN_REVOKED_ERROR_MESSAGE: &str = "OAuth token revoked · Please run /login";
199
200pub const CCR_AUTH_ERROR_MESSAGE: &str =
202 "Authentication error · This may be a temporary network issue, please try again";
203
204pub const REPEATED_529_ERROR_MESSAGE: &str = "Repeated 529 Overloaded errors";
206
207pub const CUSTOM_OFF_SWITCH_MESSAGE: &str =
209 "Opus is experiencing high load, please use /model to switch to Sonnet";
210
211pub const API_TIMEOUT_ERROR_MESSAGE: &str = "Request timed out";
213
214pub fn get_pdf_too_large_error_message(is_non_interactive: bool) -> String {
216 let limits = "max 1000 pages, 32MB".to_string();
218
219 if is_non_interactive {
220 format!(
221 "PDF too large ({}). Try reading the file a different way (e.g., extract text with pdftotext).",
222 limits
223 )
224 } else {
225 format!(
226 "PDF too large ({}). Double press esc to go back and try again, or use pdftotext to convert to text first.",
227 limits
228 )
229 }
230}
231
232pub fn get_pdf_password_protected_error_message(is_non_interactive: bool) -> String {
234 if is_non_interactive {
235 "PDF is password protected. Try using a CLI tool to extract or convert the PDF.".to_string()
236 } else {
237 "PDF is password protected. Please double press esc to edit your message and try again."
238 .to_string()
239 }
240}
241
242pub fn get_pdf_invalid_error_message(is_non_interactive: bool) -> String {
244 if is_non_interactive {
245 "The PDF file was not valid. Try converting it to a text first (e.g., pdftotext)."
246 .to_string()
247 } else {
248 "The PDF file was not valid. Double press esc to go back and try again with a different file.".to_string()
249 }
250}
251
252pub fn get_image_too_large_error_message(is_non_interactive: bool) -> String {
254 if is_non_interactive {
255 "Image was too large. Try resizing the image or using a different approach.".to_string()
256 } else {
257 "Image was too large. Double press esc to go back and try again with a smaller image."
258 .to_string()
259 }
260}
261
262pub fn get_request_too_large_error_message(is_non_interactive: bool) -> String {
264 let limits = "max 32MB".to_string();
265
266 if is_non_interactive {
267 format!("Request too large ({}). Try with a smaller file.", limits)
268 } else {
269 format!(
270 "Request too large ({}). Double press esc to go back and try with a smaller file.",
271 limits
272 )
273 }
274}
275
276pub const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE: &str =
278 "Your account does not have access to Claude Code. Please run /login.";
279
280pub fn get_token_revoked_error_message(is_non_interactive: bool) -> String {
282 if is_non_interactive {
283 "Your account does not have access to Claude. Please login again or contact your administrator."
284 .to_string()
285 } else {
286 TOKEN_REVOKED_ERROR_MESSAGE.to_string()
287 }
288}
289
290pub fn get_oauth_org_not_allowed_error_message(is_non_interactive: bool) -> String {
292 if is_non_interactive {
293 "Your organization does not have access to Claude. Please login again or contact your administrator."
294 .to_string()
295 } else {
296 OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE.to_string()
297 }
298}
299
300#[derive(Debug, Clone, PartialEq)]
302#[allow(non_camel_case_types)]
303pub enum ApiErrorType {
304 aborted,
305 api_timeout,
306 repeated_529,
307 capacity_off_switch,
308 rate_limit,
309 server_overload,
310 prompt_too_long,
311 pdf_too_large,
312 pdf_password_protected,
313 image_too_large,
314 tool_use_mismatch,
315 unexpected_tool_result,
316 duplicate_tool_use_id,
317 invalid_model,
318 credit_balance_low,
319 invalid_api_key,
320 token_revoked,
321 oauth_org_not_allowed,
322 auth_error,
323 bedrock_model_access,
324 server_error,
325 client_error,
326 ssl_cert_error,
327 connection_error,
328 unknown,
329}
330
331#[derive(Debug, Clone, PartialEq)]
333#[allow(non_camel_case_types)]
334pub enum SDKAssistantMessageError {
335 rate_limit,
336 authentication_failed,
337 server_error,
338 unknown,
339}
340
341#[derive(Debug, Clone)]
343pub struct ApiErrorMessage {
344 pub is_api_error_message: bool,
345 pub content: Option<String>,
346 pub error: Option<String>,
347 pub error_details: Option<String>,
348}
349
350impl Default for ApiErrorMessage {
351 fn default() -> Self {
352 Self {
353 is_api_error_message: true,
354 content: Some(API_ERROR_MESSAGE_PREFIX.to_string()),
355 error: Some("unknown".to_string()),
356 error_details: None,
357 }
358 }
359}
360
361pub fn create_assistant_api_error_message(content: &str) -> ApiErrorMessage {
363 ApiErrorMessage {
364 is_api_error_message: true,
365 content: Some(content.to_string()),
366 error: Some("unknown".to_string()),
367 error_details: None,
368 }
369}
370
371pub fn create_assistant_api_error_message_with_options(
373 content: &str,
374 error: Option<&str>,
375 error_details: Option<&str>,
376) -> ApiErrorMessage {
377 ApiErrorMessage {
378 is_api_error_message: true,
379 content: Some(content.to_string()),
380 error: error.map(String::from),
381 error_details: error_details.map(String::from),
382 }
383}
384
385pub fn is_ccr_mode() -> bool {
390 false
392}
393
394pub fn is_valid_api_message(value: &serde_json::Value) -> bool {
396 value.get("content").is_some()
397 && value.get("model").is_some()
398 && value.get("usage").is_some()
399 && value["content"].is_array()
400 && value["model"].is_string()
401 && value["usage"].is_object()
402}
403
404#[derive(Debug, Clone, Default)]
406pub struct AmazonError {
407 pub output: Option<AmazonOutput>,
408 pub version: Option<String>,
409}
410
411#[derive(Debug, Clone, Default)]
412pub struct AmazonOutput {
413 pub type_: Option<String>,
414}
415
416impl AmazonError {
417 pub fn new() -> Self {
418 Self::default()
419 }
420
421 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
422 let output = value.get("Output")?;
423 let output_type = output
424 .get("__type")
425 .and_then(|v| v.as_str())
426 .map(String::from);
427
428 Some(AmazonError {
429 output: Some(AmazonOutput { type_: output_type }),
430 version: value
431 .get("Version")
432 .and_then(|v| v.as_str())
433 .map(String::from),
434 })
435 }
436}
437
438pub fn extract_unknown_error_format(value: &serde_json::Value) -> Option<String> {
440 if !value.is_object() {
442 return None;
443 }
444
445 if let Some(output) = value.get("Output") {
447 if let Some(output_type) = output.get("__type").and_then(|v| v.as_str()) {
448 return Some(output_type.to_string());
449 }
450 }
451
452 None
453}
454
455pub fn classify_api_error(error_message: &str, status: Option<u16>) -> ApiErrorType {
458 let lower = error_message.to_lowercase();
459
460 if error_message == "Request was aborted." {
462 return ApiErrorType::aborted;
463 }
464
465 if lower.contains("timeout") {
467 return ApiErrorType::api_timeout;
468 }
469
470 if error_message.contains(REPEATED_529_ERROR_MESSAGE) {
472 return ApiErrorType::repeated_529;
473 }
474
475 if error_message.contains(CUSTOM_OFF_SWITCH_MESSAGE) {
477 return ApiErrorType::capacity_off_switch;
478 }
479
480 if status == Some(429) {
482 return ApiErrorType::rate_limit;
483 }
484
485 if status == Some(529) || error_message.contains(r#""type":"overloaded_error""#) {
487 return ApiErrorType::server_overload;
488 }
489
490 if lower.contains(&PROMPT_TOO_LONG_ERROR_MESSAGE.to_lowercase()) {
492 return ApiErrorType::prompt_too_long;
493 }
494
495 if is_media_size_error(error_message) && error_message.to_lowercase().contains("pdf") {
497 if error_message.to_lowercase().contains("password") {
498 return ApiErrorType::pdf_password_protected;
499 }
500 return ApiErrorType::pdf_too_large;
501 }
502
503 if status == Some(400)
505 && lower.contains("image")
506 && lower.contains("exceeds")
507 && lower.contains("maximum")
508 {
509 return ApiErrorType::image_too_large;
510 }
511
512 if status == Some(400)
514 && lower.contains("image dimensions exceed")
515 && lower.contains("many-image")
516 {
517 return ApiErrorType::image_too_large;
518 }
519
520 if status == Some(400)
522 && error_message.contains("`tool_use` ids were found without `tool_result`")
523 {
524 return ApiErrorType::tool_use_mismatch;
525 }
526
527 if status == Some(400)
528 && error_message.contains("unexpected `tool_use_id` found in `tool_result`")
529 {
530 return ApiErrorType::unexpected_tool_result;
531 }
532
533 if status == Some(400) && error_message.contains("`tool_use` ids must be unique") {
534 return ApiErrorType::duplicate_tool_use_id;
535 }
536
537 if status == Some(400) && lower.contains("invalid model name") {
539 return ApiErrorType::invalid_model;
540 }
541
542 if lower.contains(&CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.to_lowercase()) {
544 return ApiErrorType::credit_balance_low;
545 }
546
547 if lower.contains("x-api-key") {
549 return ApiErrorType::invalid_api_key;
550 }
551
552 if status == Some(403) && error_message.contains("OAuth token has been revoked") {
553 return ApiErrorType::token_revoked;
554 }
555
556 if (status == Some(401) || status == Some(403))
557 && error_message
558 .contains("OAuth authentication is currently not allowed for this organization")
559 {
560 return ApiErrorType::oauth_org_not_allowed;
561 }
562
563 if status == Some(401) || status == Some(403) {
565 return ApiErrorType::auth_error;
566 }
567
568 if let Some(s) = status {
575 if s >= 500 {
576 return ApiErrorType::server_error;
577 }
578 if s >= 400 {
579 return ApiErrorType::client_error;
580 }
581 }
582
583 if lower.contains("connection") || lower.contains("ssl") || lower.contains("tls") {
585 if lower.contains("ssl") || lower.contains("certificate") {
586 return ApiErrorType::ssl_cert_error;
587 }
588 return ApiErrorType::connection_error;
589 }
590
591 ApiErrorType::unknown
592}
593
594pub fn categorize_retryable_api_error(status: u16, message: &str) -> SDKAssistantMessageError {
596 if status == 529 || message.contains(r#""type":"overloaded_error""#) {
597 return SDKAssistantMessageError::rate_limit;
598 }
599 if status == 429 {
600 return SDKAssistantMessageError::rate_limit;
601 }
602 if status == 401 || status == 403 {
603 return SDKAssistantMessageError::authentication_failed;
604 }
605 if status >= 408 {
606 return SDKAssistantMessageError::server_error;
607 }
608 SDKAssistantMessageError::unknown
609}
610
611pub fn get_error_message_if_refusal(
613 stop_reason: Option<&str>,
614 model: &str,
615 is_non_interactive: bool,
616) -> Option<ApiErrorMessage> {
617 if stop_reason != Some("refusal") {
618 return None;
619 }
620
621 let base_message = if is_non_interactive {
625 format!(
626 "{}: 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.",
627 API_ERROR_MESSAGE_PREFIX
628 )
629 } else {
630 format!(
631 "{}: 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.",
632 API_ERROR_MESSAGE_PREFIX
633 )
634 };
635
636 let model_suggestion = if model != "claude-sonnet-4-20250514" {
637 " If you are seeing this refusal repeatedly, try running /model claude-sonnet-4-20250514 to switch models."
638 } else {
639 ""
640 };
641
642 Some(create_assistant_api_error_message_with_options(
643 &(base_message + model_suggestion),
644 Some("invalid_request"),
645 None,
646 ))
647}
648
649pub const NO_RESPONSE_REQUESTED: &str = "NO_RESPONSE_REQUESTED";
651
652pub fn error_to_api_message(error_msg: &str, status: Option<u16>) -> ApiErrorMessage {
656 let lower = error_msg.to_lowercase();
657
658 if error_msg == "Request was aborted." || error_msg == "User aborted the request" {
660 return create_assistant_api_error_message_with_options(
661 "Request was aborted",
662 Some("aborted"),
663 Some(error_msg),
664 );
665 }
666
667 if lower.contains("timeout") || lower.contains("timed out") {
669 return create_assistant_api_error_message_with_options(
670 API_TIMEOUT_ERROR_MESSAGE,
671 Some("unknown"),
672 Some(error_msg),
673 );
674 }
675
676 if error_msg.contains(REPEATED_529_ERROR_MESSAGE) {
678 return create_assistant_api_error_message_with_options(
679 REPEATED_529_ERROR_MESSAGE,
680 Some("server_overload"),
681 Some(error_msg),
682 );
683 }
684
685 if status == Some(429) || lower.contains("rate_limit") || lower.contains("rate limit") {
687 return create_assistant_api_error_message_with_options(
688 "Rate limit exceeded. Please try again shortly.",
689 Some("rate_limit"),
690 Some(error_msg),
691 );
692 }
693
694 if status == Some(529) || lower.contains("overloaded") {
696 return create_assistant_api_error_message_with_options(
697 "Server is overloaded. Retrying...",
698 Some("server_overload"),
699 Some(error_msg),
700 );
701 }
702
703 if lower.contains(&PROMPT_TOO_LONG_ERROR_MESSAGE.to_lowercase())
705 || lower.contains("prompt is too long")
706 || (status == Some(413) && lower.contains("too long"))
707 {
708 return create_assistant_api_error_message_with_options(
709 PROMPT_TOO_LONG_ERROR_MESSAGE,
710 Some("invalid_request"),
711 Some(error_msg),
712 );
713 }
714
715 if is_media_size_error(error_msg) && lower.contains("pdf") {
717 if lower.contains("password") {
718 return create_assistant_api_error_message_with_options(
719 &get_pdf_password_protected_error_message(false),
720 Some("invalid_request"),
721 Some(error_msg),
722 );
723 }
724 return create_assistant_api_error_message_with_options(
725 &get_pdf_too_large_error_message(false),
726 Some("invalid_request"),
727 Some(error_msg),
728 );
729 }
730
731 if (status == Some(400) && lower.contains("image") && lower.contains("exceeds") && lower.contains("maximum"))
733 || (lower.contains("image exceeds") && lower.contains("maximum"))
734 {
735 return create_assistant_api_error_message_with_options(
736 &get_image_too_large_error_message(false),
737 Some("invalid_request"),
738 Some(error_msg),
739 );
740 }
741
742 if status == Some(413) {
744 return create_assistant_api_error_message_with_options(
745 &get_request_too_large_error_message(false),
746 Some("invalid_request"),
747 Some(error_msg),
748 );
749 }
750
751 if status == Some(400) && error_msg.contains("`tool_use` ids were found without `tool_result`") {
753 return create_assistant_api_error_message_with_options(
754 &format!("{}: Tool use mismatch. Try /rewind to fix.", API_ERROR_MESSAGE_PREFIX),
755 Some("invalid_request"),
756 Some(error_msg),
757 );
758 }
759
760 if status == Some(400) && error_msg.contains("`tool_use` ids must be unique") {
762 return create_assistant_api_error_message_with_options(
763 &format!("{}: Duplicate tool use ID. Try /rewind to fix.", API_ERROR_MESSAGE_PREFIX),
764 Some("invalid_request"),
765 Some(error_msg),
766 );
767 }
768
769 if status == Some(400) && lower.contains("invalid model") {
771 return create_assistant_api_error_message_with_options(
772 "Model is not available. Try /model to switch.",
773 Some("invalid_request"),
774 Some(error_msg),
775 );
776 }
777
778 if lower.contains(&CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE.to_lowercase()) {
780 return create_assistant_api_error_message_with_options(
781 CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE,
782 Some("billing_error"),
783 Some(error_msg),
784 );
785 }
786
787 if lower.contains("x-api-key") || lower.contains("api key") && status == Some(401) {
789 return create_assistant_api_error_message_with_options(
790 INVALID_API_KEY_ERROR_MESSAGE,
791 Some("authentication_failed"),
792 Some(error_msg),
793 );
794 }
795
796 if status == Some(403) && error_msg.contains("OAuth token has been revoked") {
798 return create_assistant_api_error_message_with_options(
799 TOKEN_REVOKED_ERROR_MESSAGE,
800 Some("authentication_failed"),
801 Some(error_msg),
802 );
803 }
804
805 if (status == Some(401) || status == Some(403))
807 && error_msg.contains("OAuth authentication is currently not allowed for this organization")
808 {
809 return create_assistant_api_error_message_with_options(
810 OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE,
811 Some("authentication_failed"),
812 Some(error_msg),
813 );
814 }
815
816 if status == Some(401) || status == Some(403) {
818 return create_assistant_api_error_message_with_options(
819 "Authentication failed.",
820 Some("authentication_failed"),
821 Some(error_msg),
822 );
823 }
824
825 if lower.contains("connection") || lower.contains("ssl") || lower.contains("tls") {
827 return create_assistant_api_error_message_with_options(
828 "Connection error. Check your network and try again.",
829 Some("connection_error"),
830 Some(error_msg),
831 );
832 }
833
834 if lower.starts_with("api error") {
836 create_assistant_api_error_message_with_options(error_msg, Some("unknown"), Some(error_msg))
837 } else {
838 create_assistant_api_error_message_with_options(
839 &format!("{}: {}", API_ERROR_MESSAGE_PREFIX, error_msg),
840 Some("unknown"),
841 Some(error_msg),
842 )
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn test_starts_with_api_error_prefix() {
852 assert!(starts_with_api_error_prefix(
853 "API Error: something went wrong"
854 ));
855 assert!(starts_with_api_error_prefix(
856 "Please run /login · API Error: test"
857 ));
858 assert!(!starts_with_api_error_prefix("Something else"));
859 }
860
861 #[test]
862 fn test_is_terminal_task_status() {
863 assert!(!is_media_size_error("some random error"));
864 assert!(is_media_size_error("image exceeds 5 MB maximum"));
865 assert!(is_media_size_error(
866 "image dimensions exceed limit for many-image"
867 ));
868 assert!(is_media_size_error("maximum of 1000 PDF pages"));
869 }
870
871 #[test]
872 fn test_classify_api_error() {
873 assert_eq!(
874 classify_api_error("Request was aborted.", None),
875 ApiErrorType::aborted
876 );
877 assert_eq!(
878 classify_api_error("timeout error", None),
879 ApiErrorType::api_timeout
880 );
881 assert_eq!(
882 classify_api_error("rate limit", Some(429)),
883 ApiErrorType::rate_limit
884 );
885 assert_eq!(
886 classify_api_error("server overloaded", Some(529)),
887 ApiErrorType::server_overload
888 );
889 assert_eq!(
890 classify_api_error("Prompt is too long", None),
891 ApiErrorType::prompt_too_long
892 );
893 }
894
895 #[test]
896 fn test_categorize_retryable_api_error() {
897 assert_eq!(
898 categorize_retryable_api_error(529, "overloaded"),
899 SDKAssistantMessageError::rate_limit
900 );
901 assert_eq!(
902 categorize_retryable_api_error(429, "rate limit"),
903 SDKAssistantMessageError::rate_limit
904 );
905 assert_eq!(
906 categorize_retryable_api_error(401, "unauthorized"),
907 SDKAssistantMessageError::authentication_failed
908 );
909 assert_eq!(
910 categorize_retryable_api_error(500, "server error"),
911 SDKAssistantMessageError::server_error
912 );
913 }
914
915 #[test]
916 fn test_sanitize_html_error() {
917 let html = "<html><head><title>502 Bad Gateway</title></head><body><p>error</p></body></html>";
919 assert_eq!(sanitize_html_error(html), "502 Bad Gateway");
920
921 let html_no_title = "<html><body><p>error</p></body></html>";
923 assert_eq!(sanitize_html_error(html_no_title), "");
924
925 let doctype = "<!DOCTYPE html><html><head><title>503 Service Unavailable</title></head>";
927 assert_eq!(sanitize_html_error(doctype), "503 Service Unavailable");
928
929 let plain = "{\"error\":{\"message\":\"rate limited\"}}";
931 assert_eq!(sanitize_html_error(plain), "{\"error\":{\"message\":\"rate limited\"}}");
932
933 assert_eq!(sanitize_html_error(""), "");
935 }
936
937 #[test]
938 fn test_parse_prompt_too_long_token_counts() {
939 let (actual, limit) = parse_prompt_too_long_token_counts(
940 "prompt is too long: 137500 tokens > 135000 maximum",
941 );
942 assert_eq!(actual, Some(137500));
943 assert_eq!(limit, Some(135000));
944 }
945}