1use crate::error::{LiteLLMError, Result};
2use reqwest::header::HeaderMap;
3use reqwest::StatusCode;
4use std::time::Duration;
5use tokio::time::sleep;
6
7pub const MAX_SSE_BUFFER_SIZE: usize = 16 * 1024 * 1024;
9
10pub const DEFAULT_MAX_RETRIES: u32 = 3;
12pub const DEFAULT_INITIAL_BACKOFF_MS: u64 = 1000;
13pub const DEFAULT_MAX_BACKOFF_MS: u64 = 30000;
14pub const DEFAULT_BACKOFF_MULTIPLIER: f64 = 2.0;
15
16#[derive(Debug, Clone)]
18pub struct RetryConfig {
19 pub max_retries: u32,
21 pub initial_backoff_ms: u64,
23 pub max_backoff_ms: u64,
25 pub backoff_multiplier: f64,
27}
28
29impl Default for RetryConfig {
30 fn default() -> Self {
31 Self {
32 max_retries: DEFAULT_MAX_RETRIES,
33 initial_backoff_ms: DEFAULT_INITIAL_BACKOFF_MS,
34 max_backoff_ms: DEFAULT_MAX_BACKOFF_MS,
35 backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
36 }
37 }
38}
39
40impl RetryConfig {
41 fn backoff_duration(&self, attempt: u32) -> Duration {
43 let backoff_ms =
44 (self.initial_backoff_ms as f64) * self.backoff_multiplier.powi(attempt as i32);
45 let clamped_ms = backoff_ms.min(self.max_backoff_ms as f64) as u64;
46 Duration::from_millis(clamped_ms)
47 }
48}
49
50fn is_retryable_status(status: StatusCode) -> bool {
52 matches!(
53 status,
54 StatusCode::TOO_MANY_REQUESTS
55 | StatusCode::SERVICE_UNAVAILABLE
56 | StatusCode::GATEWAY_TIMEOUT
57 | StatusCode::BAD_GATEWAY
58 | StatusCode::REQUEST_TIMEOUT
59 )
60}
61
62pub async fn send_json<T: serde::de::DeserializeOwned>(
66 req: reqwest::RequestBuilder,
67) -> Result<(T, HeaderMap)> {
68 send_json_with_retry(req, &RetryConfig::default()).await
69}
70
71#[allow(unused_variables)]
76pub async fn send_json_with_retry<T: serde::de::DeserializeOwned>(
77 req: reqwest::RequestBuilder,
78 retry_config: &RetryConfig,
79) -> Result<(T, HeaderMap)> {
80 send_json_once(req).await
85}
86
87pub async fn send_json_once<T: serde::de::DeserializeOwned>(
89 req: reqwest::RequestBuilder,
90) -> Result<(T, HeaderMap)> {
91 let resp = req.send().await.map_err(LiteLLMError::from)?;
92
93 let status = resp.status();
94 let headers = resp.headers().clone();
95
96 if !status.is_success() {
97 let text = resp.text().await.map_err(LiteLLMError::from)?;
98 let trimmed = text.lines().take(20).collect::<Vec<_>>().join("\n");
99 return Err(LiteLLMError::http(format!(
100 "http {}: {}",
101 status.as_u16(),
102 trimmed
103 )));
104 }
105
106 let parsed = resp.json().await.map_err(map_json_error)?;
107 Ok((parsed, headers))
108}
109
110pub async fn with_retry<T, F, Fut>(
114 retry_config: &RetryConfig,
115 mut build_request: F,
116) -> Result<(T, HeaderMap)>
117where
118 T: serde::de::DeserializeOwned,
119 F: FnMut() -> Fut,
120 Fut: std::future::Future<Output = Result<reqwest::RequestBuilder>>,
121{
122 let mut last_error = None;
123
124 for attempt in 0..=retry_config.max_retries {
125 let req = build_request().await?;
126 let resp = req.send().await;
127
128 match resp {
129 Ok(response) => {
130 let status = response.status();
131 let headers = response.headers().clone();
132
133 if status.is_success() {
134 let parsed = response.json().await.map_err(map_json_error)?;
135 return Ok((parsed, headers));
136 }
137
138 let text = response.text().await.map_err(LiteLLMError::from)?;
139
140 if is_retryable_status(status) && attempt < retry_config.max_retries {
142 let backoff = retry_config.backoff_duration(attempt);
143 sleep(backoff).await;
144 last_error = Some(LiteLLMError::http(format!(
145 "http {}: {}",
146 status.as_u16(),
147 text.lines().take(5).collect::<Vec<_>>().join("\n")
148 )));
149 continue;
150 }
151
152 let trimmed = text.lines().take(20).collect::<Vec<_>>().join("\n");
154 return Err(LiteLLMError::http(format!(
155 "http {}: {}",
156 status.as_u16(),
157 trimmed
158 )));
159 }
160 Err(e) => {
161 if attempt < retry_config.max_retries {
163 let backoff = retry_config.backoff_duration(attempt);
164 sleep(backoff).await;
165 last_error = Some(LiteLLMError::from(e));
166 continue;
167 }
168 return Err(LiteLLMError::from(e));
169 }
170 }
171 }
172
173 Err(last_error.unwrap_or_else(|| LiteLLMError::http("max retries exceeded")))
174}
175
176fn map_json_error(err: reqwest::Error) -> LiteLLMError {
177 if err.is_timeout() {
178 let message = if err.is_decode() {
179 "request timed out while reading response body"
180 } else {
181 "request timed out"
182 };
183
184 return LiteLLMError::Http {
185 message: message.into(),
186 source: Some(Box::new(err)),
187 };
188 }
189
190 LiteLLMError::Parse(err.to_string())
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde::Deserialize;
197 use tokio::io::{AsyncReadExt, AsyncWriteExt};
198 use tokio::net::TcpListener;
199
200 #[test]
201 fn retry_config_backoff_calculation() {
202 let config = RetryConfig {
203 max_retries: 3,
204 initial_backoff_ms: 1000,
205 max_backoff_ms: 10000,
206 backoff_multiplier: 2.0,
207 };
208
209 assert_eq!(config.backoff_duration(0), Duration::from_millis(1000));
210 assert_eq!(config.backoff_duration(1), Duration::from_millis(2000));
211 assert_eq!(config.backoff_duration(2), Duration::from_millis(4000));
212 assert_eq!(config.backoff_duration(3), Duration::from_millis(8000));
213 assert_eq!(config.backoff_duration(4), Duration::from_millis(10000));
215 }
216
217 #[test]
218 fn retryable_status_codes() {
219 assert!(is_retryable_status(StatusCode::TOO_MANY_REQUESTS));
220 assert!(is_retryable_status(StatusCode::SERVICE_UNAVAILABLE));
221 assert!(is_retryable_status(StatusCode::GATEWAY_TIMEOUT));
222 assert!(is_retryable_status(StatusCode::BAD_GATEWAY));
223 assert!(is_retryable_status(StatusCode::REQUEST_TIMEOUT));
224
225 assert!(!is_retryable_status(StatusCode::OK));
226 assert!(!is_retryable_status(StatusCode::BAD_REQUEST));
227 assert!(!is_retryable_status(StatusCode::UNAUTHORIZED));
228 assert!(!is_retryable_status(StatusCode::NOT_FOUND));
229 assert!(!is_retryable_status(StatusCode::INTERNAL_SERVER_ERROR));
230 }
231
232 #[derive(Debug, Deserialize)]
233 struct TestPayload {
234 #[serde(rename = "value")]
235 _value: String,
236 }
237
238 #[tokio::test]
239 async fn send_json_maps_body_read_timeouts_to_http_errors() {
240 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
241 let addr = listener.local_addr().unwrap();
242
243 tokio::spawn(async move {
244 let (mut stream, _) = listener.accept().await.unwrap();
245 let mut request_buf = [0_u8; 1024];
246 let _ = stream.read(&mut request_buf).await;
247
248 stream
249 .write_all(
250 b"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: 14\r\n\r\n",
251 )
252 .await
253 .unwrap();
254
255 sleep(Duration::from_millis(100)).await;
256 let _ = stream.write_all(br#"{"value":"ok"}"#).await;
257 });
258
259 let client = reqwest::Client::builder()
260 .timeout(Duration::from_millis(50))
261 .build()
262 .unwrap();
263
264 let err = send_json_once::<TestPayload>(client.get(format!("http://{addr}/")))
265 .await
266 .unwrap_err();
267
268 match err {
269 LiteLLMError::Http { message, source } => {
270 assert!(
271 message.contains("timed out"),
272 "unexpected message: {message}"
273 );
274 assert!(
275 source.is_some(),
276 "timeout errors should retain the reqwest source"
277 );
278 }
279 other => panic!("expected http timeout error, got {other:?}"),
280 }
281 }
282}