lash_core/llm/
transport.rs1pub use lash_sansio::llm::types::ProviderFailureKind;
7
8#[derive(Debug, thiserror::Error, Clone)]
9#[error("{message}")]
10pub struct ProviderFailure {
11 pub kind: ProviderFailureKind,
12 pub message: String,
13 pub retryable: bool,
14 pub status: Option<u16>,
15 pub raw: Option<String>,
16 pub code: Option<String>,
17 pub terminal_reason: lash_sansio::llm::types::LlmTerminalReason,
18 pub headers: Vec<(String, String)>,
19 pub retry_after: Option<std::time::Duration>,
20 pub request_body: Option<String>,
21}
22
23impl ProviderFailure {
24 pub fn new(message: impl Into<String>) -> Self {
25 Self {
26 kind: ProviderFailureKind::Unknown,
27 message: message.into(),
28 retryable: false,
29 status: None,
30 raw: None,
31 code: None,
32 terminal_reason: lash_sansio::llm::types::LlmTerminalReason::ProviderError,
33 headers: Vec::new(),
34 retry_after: None,
35 request_body: None,
36 }
37 }
38
39 pub fn with_kind(mut self, kind: ProviderFailureKind) -> Self {
40 self.kind = kind;
41 self
42 }
43
44 pub fn retryable(mut self, retryable: bool) -> Self {
45 self.retryable = retryable;
46 self
47 }
48
49 pub fn with_status(mut self, status: u16) -> Self {
50 self.status = Some(status);
51 if self.code.is_none() {
52 self.code = Some(status.to_string());
53 }
54 self
55 }
56
57 pub fn with_raw(mut self, raw: impl Into<String>) -> Self {
58 self.raw = Some(raw.into());
59 self
60 }
61
62 pub fn with_code(mut self, code: impl Into<String>) -> Self {
63 self.code = Some(code.into());
64 self
65 }
66
67 pub fn with_terminal_reason(
68 mut self,
69 reason: lash_sansio::llm::types::LlmTerminalReason,
70 ) -> Self {
71 self.terminal_reason = reason;
72 self
73 }
74
75 pub fn with_headers<I, K, V>(mut self, headers: I) -> Self
76 where
77 I: IntoIterator<Item = (K, V)>,
78 K: Into<String>,
79 V: Into<String>,
80 {
81 self.headers = headers
82 .into_iter()
83 .map(|(name, value)| (name.into(), value.into()))
84 .collect();
85 self.retry_after = retry_after_from_headers(&self.headers);
86 self
87 }
88
89 pub fn with_retry_after(mut self, retry_after: std::time::Duration) -> Self {
90 self.retry_after = Some(retry_after);
91 self
92 }
93
94 pub fn with_request_body(mut self, request_body: impl Into<String>) -> Self {
95 self.request_body = Some(request_body.into());
96 self
97 }
98}
99
100pub fn retry_after_from_headers(headers: &[(String, String)]) -> Option<std::time::Duration> {
101 let value = headers
102 .iter()
103 .find(|(name, _)| name.eq_ignore_ascii_case("retry-after"))?
104 .1
105 .trim();
106 if let Ok(seconds) = value.parse::<u64>() {
107 return Some(std::time::Duration::from_secs(seconds));
108 }
109 None
110}
111
112pub type LlmTransportError = ProviderFailure;
113
114#[expect(
123 clippy::result_large_err,
124 reason = "provider transport errors are a public typed API and carry request/response diagnostics"
125)]
126pub fn validate_image_attachments(
127 req: &lash_sansio::llm::types::LlmRequest,
128 accepted_mimes: &[&str],
129 provider_name: &str,
130) -> Result<(), LlmTransportError> {
131 for (idx, att) in req.attachments.iter().enumerate() {
132 let mime = att.mime.trim().to_ascii_lowercase();
133 let normalized = if mime == "image/jpg" {
134 "image/jpeg"
135 } else {
136 mime.as_str()
137 };
138 if !accepted_mimes.contains(&normalized) {
139 return Err(ProviderFailure::new(format!(
140 "{provider_name} does not accept image attachments of type `{}` (attachment index {idx}); accepted: {}",
141 att.mime,
142 accepted_mimes.join(", "),
143 ))
144 .with_kind(ProviderFailureKind::Validation)
145 .with_code("unsupported_image_format"));
146 }
147 }
148 Ok(())
149}