1use std::fmt;
2use std::time::Duration;
3
4pub const MAX_ERROR_BODY_DISPLAY_BYTES: usize = 512;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum GuardrailPhase {
10 Input,
11 Output,
12}
13
14#[derive(Clone)]
16pub enum LLMError {
17 HttpError(String),
19 AuthError {
21 message: String,
22 status_code: Option<u16>,
23 response_body: Option<Box<str>>,
24 },
25 RateLimitError {
27 status_code: u16,
28 message: String,
29 response_body: Box<str>,
30 retry_after: Option<Duration>,
31 provider_code: Option<Box<str>>,
32 },
33 HttpStatusError {
35 status_code: u16,
36 message: String,
37 response_body: Box<str>,
38 retry_after: Option<Duration>,
39 provider_code: Option<Box<str>>,
40 },
41 InvalidRequest {
43 message: String,
44 status_code: Option<u16>,
45 response_body: Option<Box<str>>,
46 },
47 ProviderError(String),
49 ResponseFormatError {
51 message: String,
52 raw_response: String,
53 },
54 Generic(String),
56 JsonError(String),
58 ToolConfigError(String),
60 NoToolSupport(String),
62 GuardrailBlocked {
64 phase: GuardrailPhase,
65 guard: Box<str>,
66 rule_id: Box<str>,
67 category: Box<str>,
68 severity: Box<str>,
69 message: Box<str>,
70 },
71 GuardrailExecutionFailed { guard: String, message: String },
73}
74
75impl LLMError {
76 pub fn missing_api_key(message: impl Into<String>) -> Self {
78 Self::AuthError {
79 message: message.into(),
80 status_code: None,
81 response_body: None,
82 }
83 }
84
85 pub fn invalid_request(message: impl Into<String>) -> Self {
87 Self::InvalidRequest {
88 message: message.into(),
89 status_code: None,
90 response_body: None,
91 }
92 }
93
94 pub fn is_retryable(&self) -> bool {
96 is_retryable(self)
97 }
98
99 pub fn http_status_code(&self) -> Option<u16> {
101 match self {
102 Self::AuthError { status_code, .. } => *status_code,
103 Self::RateLimitError { status_code, .. } => Some(*status_code),
104 Self::HttpStatusError { status_code, .. } => Some(*status_code),
105 Self::InvalidRequest { status_code, .. } => *status_code,
106 _ => None,
107 }
108 }
109
110 pub fn response_body(&self) -> Option<&str> {
112 match self {
113 Self::AuthError { response_body, .. } => response_body.as_deref(),
114 Self::RateLimitError { response_body, .. } => Some(response_body),
115 Self::HttpStatusError { response_body, .. } => Some(response_body),
116 Self::InvalidRequest { response_body, .. } => response_body.as_deref(),
117 Self::ResponseFormatError { raw_response, .. } => Some(raw_response),
118 _ => None,
119 }
120 }
121
122 pub fn is_transport_retryable(&self) -> bool {
124 match self {
125 Self::HttpError(msg) => is_transport_retryable_message(msg),
126 _ => false,
127 }
128 }
129}
130
131pub fn is_transport_retryable_message(message: &str) -> bool {
133 let m = message.to_ascii_lowercase();
134 m.starts_with("request timed out:")
135 || m.starts_with("connection failed:")
136 || m.contains("connection reset")
137 || m.contains("broken pipe")
138 || m.contains("dns error")
139 || m.contains("dns lookup")
140 || m.contains("name or service not known")
141}
142
143pub fn truncate_for_display(body: &str) -> String {
145 if body.len() <= MAX_ERROR_BODY_DISPLAY_BYTES {
146 return body.to_string();
147 }
148
149 let mut end = MAX_ERROR_BODY_DISPLAY_BYTES;
150 while end > 0 && !body.is_char_boundary(end) {
151 end -= 1;
152 }
153
154 format!(
155 "{}... [truncated, {} bytes total]",
156 &body[..end],
157 body.len()
158 )
159}
160
161fn write_truncated_body(f: &mut fmt::Formatter<'_>, label: &str, body: &str) -> fmt::Result {
162 write!(f, ". {label}: {}", truncate_for_display(body))
163}
164
165fn debug_optional_body(body: &Option<Box<str>>) -> Option<String> {
166 body.as_deref().map(truncate_for_display)
167}
168
169impl fmt::Debug for LLMError {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::HttpError(message) => f.debug_tuple("HttpError").field(message).finish(),
173 Self::AuthError {
174 message,
175 status_code,
176 response_body,
177 } => f
178 .debug_struct("AuthError")
179 .field("message", message)
180 .field("status_code", status_code)
181 .field("response_body", &debug_optional_body(response_body))
182 .finish(),
183 Self::RateLimitError {
184 status_code,
185 message,
186 response_body,
187 retry_after,
188 provider_code,
189 } => f
190 .debug_struct("RateLimitError")
191 .field("status_code", status_code)
192 .field("message", message)
193 .field("response_body", &truncate_for_display(response_body))
194 .field("retry_after", retry_after)
195 .field("provider_code", provider_code)
196 .finish(),
197 Self::HttpStatusError {
198 status_code,
199 message,
200 response_body,
201 retry_after,
202 provider_code,
203 } => f
204 .debug_struct("HttpStatusError")
205 .field("status_code", status_code)
206 .field("message", message)
207 .field("response_body", &truncate_for_display(response_body))
208 .field("retry_after", retry_after)
209 .field("provider_code", provider_code)
210 .finish(),
211 Self::InvalidRequest {
212 message,
213 status_code,
214 response_body,
215 } => f
216 .debug_struct("InvalidRequest")
217 .field("message", message)
218 .field("status_code", status_code)
219 .field("response_body", &debug_optional_body(response_body))
220 .finish(),
221 Self::ProviderError(message) => f.debug_tuple("ProviderError").field(message).finish(),
222 Self::ResponseFormatError {
223 message,
224 raw_response,
225 } => f
226 .debug_struct("ResponseFormatError")
227 .field("message", message)
228 .field("raw_response", &truncate_for_display(raw_response))
229 .finish(),
230 Self::Generic(message) => f.debug_tuple("Generic").field(message).finish(),
231 Self::JsonError(message) => f.debug_tuple("JsonError").field(message).finish(),
232 Self::ToolConfigError(message) => {
233 f.debug_tuple("ToolConfigError").field(message).finish()
234 }
235 Self::NoToolSupport(message) => f.debug_tuple("NoToolSupport").field(message).finish(),
236 Self::GuardrailBlocked {
237 phase,
238 guard,
239 rule_id,
240 category,
241 severity,
242 message,
243 } => f
244 .debug_struct("GuardrailBlocked")
245 .field("phase", phase)
246 .field("guard", guard)
247 .field("rule_id", rule_id)
248 .field("category", category)
249 .field("severity", severity)
250 .field("message", message)
251 .finish(),
252 Self::GuardrailExecutionFailed { guard, message } => f
253 .debug_struct("GuardrailExecutionFailed")
254 .field("guard", guard)
255 .field("message", message)
256 .finish(),
257 }
258 }
259}
260
261pub fn is_http_status_retryable(status_code: u16) -> bool {
263 matches!(status_code, 408 | 500..=599)
264}
265
266pub fn is_retryable(err: &LLMError) -> bool {
268 match err {
269 LLMError::RateLimitError { .. } => true,
270 LLMError::HttpStatusError { status_code, .. } => is_http_status_retryable(*status_code),
271 LLMError::HttpError(msg) => is_transport_retryable_message(msg),
272 LLMError::Generic(_)
273 | LLMError::AuthError { .. }
274 | LLMError::InvalidRequest { .. }
275 | LLMError::GuardrailBlocked { .. }
276 | LLMError::GuardrailExecutionFailed { .. }
277 | LLMError::ResponseFormatError { .. }
278 | LLMError::JsonError(_)
279 | LLMError::ToolConfigError(_)
280 | LLMError::NoToolSupport(_)
281 | LLMError::ProviderError(_) => false,
282 }
283}
284
285pub fn is_fallbackable(err: &LLMError) -> bool {
292 match err {
293 LLMError::RateLimitError { .. } => true,
294 LLMError::HttpStatusError { status_code, .. } => is_http_status_retryable(*status_code),
295 LLMError::HttpError(_) => true,
296 LLMError::ProviderError(_)
297 | LLMError::ResponseFormatError { .. }
298 | LLMError::NoToolSupport(_)
299 | LLMError::Generic(_) => true,
300 LLMError::AuthError { .. }
301 | LLMError::InvalidRequest { .. }
302 | LLMError::JsonError(_)
303 | LLMError::ToolConfigError(_)
304 | LLMError::GuardrailBlocked { .. }
305 | LLMError::GuardrailExecutionFailed { .. } => false,
306 }
307}
308
309fn write_status_prefixed_error(
310 f: &mut fmt::Formatter<'_>,
311 label: &str,
312 message: &str,
313 status_code: Option<u16>,
314 response_body: Option<&str>,
315) -> fmt::Result {
316 if let Some(status) = status_code {
317 write!(f, "{label} ({status}): {message}")?;
318 } else {
319 write!(f, "{label}: {message}")?;
320 }
321 if let Some(body) = response_body {
322 write_truncated_body(f, "Response", body)?;
323 }
324 Ok(())
325}
326
327fn display_auth_error(
328 f: &mut fmt::Formatter<'_>,
329 message: &str,
330 status_code: Option<u16>,
331 response_body: Option<&str>,
332) -> fmt::Result {
333 write_status_prefixed_error(f, "Auth Error", message, status_code, response_body)
334}
335
336fn display_rate_limit_error(
337 f: &mut fmt::Formatter<'_>,
338 status_code: u16,
339 message: &str,
340 response_body: &str,
341 retry_after: Option<Duration>,
342 provider_code: Option<&str>,
343) -> fmt::Result {
344 write!(f, "Rate Limit Error ({status_code}): {message}")?;
345 write_truncated_body(f, "Response", response_body)?;
346 if let Some(code) = provider_code {
347 write!(f, ". Provider code: {code}")?;
348 }
349 if let Some(retry_after) = retry_after {
350 write!(f, ". Retry-After: {}s", retry_after.as_secs())?;
351 }
352 Ok(())
353}
354
355fn display_http_status_error(
356 f: &mut fmt::Formatter<'_>,
357 status_code: u16,
358 message: &str,
359 response_body: &str,
360 retry_after: Option<Duration>,
361 provider_code: Option<&str>,
362) -> fmt::Result {
363 write!(f, "HTTP Status Error ({status_code}): {message}")?;
364 write_truncated_body(f, "Response", response_body)?;
365 if let Some(code) = provider_code {
366 write!(f, ". Provider code: {code}")?;
367 }
368 if let Some(retry_after) = retry_after {
369 write!(f, ". Retry-After: {}s", retry_after.as_secs())?;
370 }
371 Ok(())
372}
373
374fn display_invalid_request(
375 f: &mut fmt::Formatter<'_>,
376 message: &str,
377 status_code: Option<u16>,
378 response_body: Option<&str>,
379) -> fmt::Result {
380 write_status_prefixed_error(f, "Invalid Request", message, status_code, response_body)
381}
382
383fn display_response_format_error(
384 f: &mut fmt::Formatter<'_>,
385 message: &str,
386 raw_response: &str,
387) -> fmt::Result {
388 write!(f, "Response Format Error: {message}")?;
389 write_truncated_body(f, "Raw response", raw_response)
390}
391
392fn display_guardrail_blocked(
393 f: &mut fmt::Formatter<'_>,
394 phase: GuardrailPhase,
395 guard: &str,
396 rule_id: &str,
397 category: &str,
398 severity: &str,
399 message: &str,
400) -> fmt::Result {
401 let phase = match phase {
402 GuardrailPhase::Input => "input",
403 GuardrailPhase::Output => "output",
404 };
405 write!(
406 f,
407 "guardrail blocked {phase}: guard={guard}, rule={rule_id}, category={category}, severity={severity}, message={message}"
408 )
409}
410
411impl fmt::Display for LLMError {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 match self {
414 LLMError::HttpError(e) => write!(f, "HTTP Error: {e}"),
415 LLMError::AuthError {
416 message,
417 status_code,
418 response_body,
419 } => display_auth_error(f, message, *status_code, response_body.as_deref()),
420 LLMError::RateLimitError {
421 status_code,
422 message,
423 response_body,
424 retry_after,
425 provider_code,
426 } => display_rate_limit_error(
427 f,
428 *status_code,
429 message,
430 response_body,
431 *retry_after,
432 provider_code.as_deref(),
433 ),
434 LLMError::HttpStatusError {
435 status_code,
436 message,
437 response_body,
438 retry_after,
439 provider_code,
440 } => display_http_status_error(
441 f,
442 *status_code,
443 message,
444 response_body,
445 *retry_after,
446 provider_code.as_deref(),
447 ),
448 LLMError::InvalidRequest {
449 message,
450 status_code,
451 response_body,
452 } => display_invalid_request(f, message, *status_code, response_body.as_deref()),
453 LLMError::ProviderError(e) => write!(f, "Provider Error: {e}"),
454 LLMError::Generic(e) => write!(f, "Generic Error : {e}"),
455 LLMError::ResponseFormatError {
456 message,
457 raw_response,
458 } => display_response_format_error(f, message, raw_response),
459 LLMError::JsonError(e) => write!(f, "JSON Parse Error: {e}"),
460 LLMError::ToolConfigError(e) => write!(f, "Tool Configuration Error: {e}"),
461 LLMError::NoToolSupport(e) => write!(f, "No Tool Support: {e}"),
462 LLMError::GuardrailBlocked {
463 phase,
464 guard,
465 rule_id,
466 category,
467 severity,
468 message,
469 } => display_guardrail_blocked(f, *phase, guard, rule_id, category, severity, message),
470 LLMError::GuardrailExecutionFailed { guard, message } => write!(
471 f,
472 "guardrail execution failed: guard={guard}, error={message}"
473 ),
474 }
475 }
476}
477
478impl std::error::Error for LLMError {}
479
480#[cfg(not(target_arch = "wasm32"))]
482impl From<reqwest::Error> for LLMError {
483 fn from(err: reqwest::Error) -> Self {
484 if err.is_timeout() {
485 LLMError::HttpError(format!("request timed out: {err}"))
486 } else if err.is_connect() {
487 LLMError::HttpError(format!("connection failed: {err}"))
488 } else {
489 LLMError::HttpError(err.to_string())
490 }
491 }
492}
493
494impl From<serde_json::Error> for LLMError {
495 fn from(err: serde_json::Error) -> Self {
496 LLMError::JsonError(format!(
497 "{} at line {} column {}",
498 err,
499 err.line(),
500 err.column()
501 ))
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use serde_json::Error as JsonError;
509
510 #[test]
511 fn test_truncate_for_display_short_body_unchanged() {
512 assert_eq!(truncate_for_display("short"), "short");
513 }
514
515 #[test]
516 fn test_truncate_for_display_long_body() {
517 let body = "x".repeat(MAX_ERROR_BODY_DISPLAY_BYTES + 10);
518 let truncated = truncate_for_display(&body);
519 assert!(truncated.contains("truncated"));
520 assert!(truncated.starts_with(&"x".repeat(MAX_ERROR_BODY_DISPLAY_BYTES)));
521 }
522
523 #[test]
524 fn test_is_transport_retryable_message_prefixed_forms() {
525 assert!(is_transport_retryable_message(
526 "request timed out: operation timed out"
527 ));
528 assert!(is_transport_retryable_message(
529 "connection failed: tcp connect error"
530 ));
531 assert!(!is_transport_retryable_message(
532 "HTTP Status Error (400): bad request"
533 ));
534 }
535
536 #[test]
537 fn test_llm_error_display_auth_error_with_status_truncates_body() {
538 let body = "secret".repeat(200);
539 let error = LLMError::AuthError {
540 message: "Unauthorized".to_string(),
541 status_code: Some(401),
542 response_body: Some(body.clone().into_boxed_str()),
543 };
544 let display = error.to_string();
545 assert!(display.contains("401"));
546 assert!(display.contains("truncated"));
547 assert_eq!(error.response_body(), Some(body.as_str()));
548 }
549
550 #[test]
551 fn test_llm_error_display_rate_limit_error_includes_status() {
552 let error = LLMError::RateLimitError {
553 status_code: 529,
554 message: "Overloaded".to_string(),
555 response_body: "overload".into(),
556 retry_after: Some(Duration::from_secs(30)),
557 provider_code: Some("overloaded".into()),
558 };
559 let display = error.to_string();
560 assert!(display.contains("529"));
561 assert!(display.contains("overloaded"));
562 assert_eq!(error.http_status_code(), Some(529));
563 }
564
565 #[test]
566 fn test_invalid_request_preserves_response_body() {
567 let err = LLMError::InvalidRequest {
568 message: "bad request".into(),
569 status_code: Some(400),
570 response_body: Some(r#"{"error":"details"}"#.into()),
571 };
572 assert_eq!(err.http_status_code(), Some(400));
573 assert_eq!(err.response_body(), Some(r#"{"error":"details"}"#));
574 }
575
576 #[test]
577 fn test_is_retryable_matrix() {
578 assert!(
579 LLMError::RateLimitError {
580 status_code: 429,
581 message: "limit".into(),
582 response_body: "body".into(),
583 retry_after: None,
584 provider_code: None,
585 }
586 .is_retryable()
587 );
588 assert!(
589 LLMError::HttpStatusError {
590 status_code: 503,
591 message: "down".into(),
592 response_body: "body".into(),
593 retry_after: None,
594 provider_code: None,
595 }
596 .is_retryable()
597 );
598 assert!(
599 !LLMError::HttpStatusError {
600 status_code: 400,
601 message: "bad".into(),
602 response_body: "body".into(),
603 retry_after: None,
604 provider_code: None,
605 }
606 .is_retryable()
607 );
608 assert!(!LLMError::Generic("unsupported".into()).is_retryable());
609 assert!(LLMError::HttpError("request timed out: elapsed".into()).is_retryable());
610 }
611
612 #[test]
613 fn test_llm_error_debug_truncates_response_body() {
614 let body = "secret".repeat(MAX_ERROR_BODY_DISPLAY_BYTES + 10);
615 let error = LLMError::RateLimitError {
616 status_code: 429,
617 message: "limit".into(),
618 response_body: body.clone().into_boxed_str(),
619 retry_after: None,
620 provider_code: None,
621 };
622 let debug = format!("{error:?}");
623 assert!(debug.contains("truncated"));
624 assert!(!debug.contains(&body));
625 }
626
627 #[test]
628 fn test_from_serde_json_error() {
629 let json_str = r#"{"invalid": json}"#;
630 let json_error: JsonError =
631 serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
632
633 let llm_error: LLMError = json_error.into();
634
635 match llm_error {
636 LLMError::JsonError(msg) => {
637 assert!(msg.contains("line"));
638 assert!(msg.contains("column"));
639 }
640 _ => panic!("Expected JsonError"),
641 }
642 }
643
644 #[test]
645 fn test_http_status_code_and_response_body_accessors() {
646 let auth = LLMError::AuthError {
647 message: "denied".into(),
648 status_code: Some(403),
649 response_body: Some("body".into()),
650 };
651 assert_eq!(auth.http_status_code(), Some(403));
652 assert_eq!(auth.response_body(), Some("body"));
653
654 let rate_limit = LLMError::RateLimitError {
655 status_code: 429,
656 message: "limit".into(),
657 response_body: "payload".into(),
658 retry_after: None,
659 provider_code: None,
660 };
661 assert_eq!(rate_limit.response_body(), Some("payload"));
662
663 let http_status = LLMError::HttpStatusError {
664 status_code: 502,
665 message: "bad gateway".into(),
666 response_body: "html".into(),
667 retry_after: None,
668 provider_code: None,
669 };
670 assert_eq!(http_status.http_status_code(), Some(502));
671 assert_eq!(http_status.response_body(), Some("html"));
672
673 let format_error = LLMError::ResponseFormatError {
674 message: "invalid json".into(),
675 raw_response: "not-json".into(),
676 };
677 assert_eq!(format_error.response_body(), Some("not-json"));
678 assert_eq!(LLMError::Generic("x".into()).http_status_code(), None);
679 assert_eq!(LLMError::JsonError("parse".into()).response_body(), None);
680 }
681
682 #[test]
683 fn test_is_transport_retryable_method() {
684 assert!(LLMError::HttpError("request timed out: elapsed".into()).is_transport_retryable());
685 assert!(!LLMError::Generic("timeout".into()).is_transport_retryable());
686 }
687
688 #[test]
689 fn test_truncate_for_display_respects_utf8_boundary() {
690 let body = format!("{}€{}", "a".repeat(511), "z".repeat(20));
691 let truncated = truncate_for_display(&body);
692 assert!(truncated.contains("truncated"));
693 assert!(std::str::from_utf8(truncated.as_bytes()).is_ok());
694 }
695
696 #[test]
697 fn test_is_fallbackable_matrix() {
698 assert!(is_fallbackable(&LLMError::HttpError("down".into())));
699 assert!(is_fallbackable(&LLMError::RateLimitError {
700 status_code: 429,
701 message: "limit".into(),
702 response_body: "body".into(),
703 retry_after: None,
704 provider_code: None,
705 }));
706 assert!(!is_fallbackable(&LLMError::missing_api_key("missing")));
707 assert!(!is_fallbackable(&LLMError::GuardrailBlocked {
708 phase: GuardrailPhase::Input,
709 guard: "g".into(),
710 rule_id: "r".into(),
711 category: "c".into(),
712 severity: "high".into(),
713 message: "blocked".into(),
714 }));
715 }
716
717 #[test]
718 fn test_llm_error_debug_covers_struct_variants() {
719 let cases: Vec<LLMError> = vec![
720 LLMError::HttpError("transport".into()),
721 LLMError::AuthError {
722 message: "denied".into(),
723 status_code: Some(401),
724 response_body: Some("secret".into()),
725 },
726 LLMError::HttpStatusError {
727 status_code: 500,
728 message: "fail".into(),
729 response_body: "body".into(),
730 retry_after: None,
731 provider_code: Some("internal".into()),
732 },
733 LLMError::InvalidRequest {
734 message: "bad".into(),
735 status_code: Some(422),
736 response_body: Some("details".into()),
737 },
738 LLMError::ProviderError("provider".into()),
739 LLMError::ResponseFormatError {
740 message: "parse".into(),
741 raw_response: "raw".into(),
742 },
743 LLMError::Generic("generic".into()),
744 LLMError::JsonError("json".into()),
745 LLMError::ToolConfigError("tool".into()),
746 LLMError::NoToolSupport("tools".into()),
747 LLMError::GuardrailBlocked {
748 phase: GuardrailPhase::Output,
749 guard: "guard".into(),
750 rule_id: "rule".into(),
751 category: "cat".into(),
752 severity: "low".into(),
753 message: "msg".into(),
754 },
755 LLMError::GuardrailExecutionFailed {
756 guard: "guard".into(),
757 message: "runtime".into(),
758 },
759 ];
760
761 for error in cases {
762 let debug = format!("{error:?}");
763 assert!(!debug.is_empty());
764 }
765 }
766
767 #[test]
768 fn test_llm_error_display_covers_remaining_variants() {
769 assert!(
770 LLMError::HttpError("transport".into())
771 .to_string()
772 .contains("HTTP Error")
773 );
774
775 let status = LLMError::HttpStatusError {
776 status_code: 500,
777 message: "fail".into(),
778 response_body: "body".into(),
779 retry_after: Some(Duration::from_secs(120)),
780 provider_code: Some("INTERNAL".into()),
781 };
782 let status_display = status.to_string();
783 assert!(status_display.contains("Provider code: INTERNAL"));
784 assert!(status_display.contains("Retry-After: 120s"));
785
786 let format_error = LLMError::ResponseFormatError {
787 message: "bad json".into(),
788 raw_response: "not-json".into(),
789 };
790 assert!(format_error.to_string().contains("Response Format Error"));
791 assert!(format_error.to_string().contains("Raw response"));
792
793 let input_block = LLMError::GuardrailBlocked {
794 phase: GuardrailPhase::Input,
795 guard: "g".into(),
796 rule_id: "r".into(),
797 category: "c".into(),
798 severity: "high".into(),
799 message: "blocked".into(),
800 };
801 assert!(input_block.to_string().contains("guardrail blocked input"));
802
803 let output_block = LLMError::GuardrailBlocked {
804 phase: GuardrailPhase::Output,
805 guard: "g".into(),
806 rule_id: "r".into(),
807 category: "c".into(),
808 severity: "high".into(),
809 message: "blocked".into(),
810 };
811 assert!(
812 output_block
813 .to_string()
814 .contains("guardrail blocked output")
815 );
816
817 let execution_failed = LLMError::GuardrailExecutionFailed {
818 guard: "g".into(),
819 message: "runtime".into(),
820 };
821 assert!(
822 execution_failed
823 .to_string()
824 .contains("guardrail execution failed")
825 );
826 }
827
828 #[cfg(not(target_arch = "wasm32"))]
829 #[tokio::test]
830 async fn test_from_reqwest_connection_error() {
831 let client = reqwest::Client::new();
832 let err = client
833 .get("http://127.0.0.1:1")
834 .send()
835 .await
836 .expect_err("connection should fail");
837
838 let llm_err = LLMError::from(err);
839 match llm_err {
840 LLMError::HttpError(message) => {
841 assert!(message.starts_with("connection failed:"));
842 }
843 other => panic!("unexpected error: {other:?}"),
844 }
845 }
846}