1use std::time::Duration;
8
9use lc_core::language_models::{BaseChatModel, LLMResult};
10use lc_core::runnables::RunnableConfig;
11use lc_schema::Message;
12
13#[derive(Debug, Clone)]
15pub struct RetryConfig {
16 pub max_retries: usize,
18 pub base_delay: Duration,
20 pub max_delay: Duration,
22}
23
24impl Default for RetryConfig {
25 fn default() -> Self {
26 Self {
27 max_retries: 3,
28 base_delay: Duration::from_secs(1),
29 max_delay: Duration::from_secs(30),
30 }
31 }
32}
33
34pub(crate) async fn retry_chat<M>(
40 llm: &M,
41 messages: Vec<Message>,
42 config: Option<RunnableConfig>,
43 retry: &RetryConfig,
44) -> Result<LLMResult, M::Error>
45where
46 M: BaseChatModel + ?Sized,
47{
48 let mut attempt = 0usize;
49 loop {
50 match llm.chat(messages.clone(), config.clone()).await {
51 Ok(result) => return Ok(result),
52 Err(e) if attempt < retry.max_retries => {
53 let shift = 1u32.checked_shl(attempt as u32).unwrap_or(u32::MAX);
55 let delay = retry.base_delay.saturating_mul(shift).min(retry.max_delay);
56 log::warn!(
57 "LLM call failed (attempt {}), retrying in {:?}: {}",
58 attempt + 1,
59 delay,
60 e
61 );
62 tokio::time::sleep(delay).await;
63 attempt += 1;
64 }
65 Err(e) => return Err(e),
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use async_trait::async_trait;
74 use lc_core::language_models::{BaseLanguageModel, StreamChunk};
75 use lc_core::runnables::Runnable;
76 use std::sync::atomic::{AtomicUsize, Ordering};
77
78 struct FlakyChat {
80 calls: AtomicUsize,
81 failures_before_success: usize,
82 }
83
84 impl FlakyChat {
85 fn new(failures_before_success: usize) -> Self {
86 Self {
87 calls: AtomicUsize::new(0),
88 failures_before_success,
89 }
90 }
91 }
92
93 #[derive(Debug, thiserror::Error)]
94 #[error("flaky chat error")]
95 struct FlakyError;
96
97 #[async_trait]
98 impl Runnable<Vec<Message>, LLMResult> for FlakyChat {
99 type Error = FlakyError;
100
101 async fn invoke(
102 &self,
103 _input: Vec<Message>,
104 _config: Option<RunnableConfig>,
105 ) -> Result<LLMResult, Self::Error> {
106 unreachable!()
107 }
108 }
109
110 #[async_trait]
111 impl BaseLanguageModel<Vec<Message>, LLMResult> for FlakyChat {
112 fn model_name(&self) -> &str {
113 "flaky"
114 }
115
116 fn get_num_tokens(&self, text: &str) -> usize {
117 text.split_whitespace().count()
118 }
119
120 fn with_temperature(self, _temp: f32) -> Self
121 where
122 Self: Sized,
123 {
124 self
125 }
126
127 fn with_max_tokens(self, _max: usize) -> Self
128 where
129 Self: Sized,
130 {
131 self
132 }
133 }
134
135 #[async_trait]
136 impl BaseChatModel for FlakyChat {
137 async fn chat(
138 &self,
139 _messages: Vec<Message>,
140 _config: Option<RunnableConfig>,
141 ) -> Result<LLMResult, Self::Error> {
142 let call = self.calls.fetch_add(1, Ordering::SeqCst);
143 if call < self.failures_before_success {
144 Err(FlakyError)
145 } else {
146 Ok(LLMResult {
147 content: "ok".to_string(),
148 model: "flaky".to_string(),
149 token_usage: None,
150 tool_calls: None,
151 thinking_content: None,
152 })
153 }
154 }
155
156 async fn stream_chat(
157 &self,
158 _messages: Vec<Message>,
159 _config: Option<RunnableConfig>,
160 ) -> Result<
161 std::pin::Pin<
162 Box<dyn futures_util::Stream<Item = Result<StreamChunk, Self::Error>> + Send>,
163 >,
164 Self::Error,
165 > {
166 unreachable!()
167 }
168 }
169
170 #[test]
171 fn retry_config_defaults() {
172 let cfg = RetryConfig::default();
173 assert_eq!(cfg.max_retries, 3);
174 assert_eq!(cfg.base_delay, Duration::from_secs(1));
175 assert_eq!(cfg.max_delay, Duration::from_secs(30));
176 }
177
178 #[tokio::test]
179 async fn retry_succeeds_after_transient_failures() {
180 let llm = FlakyChat::new(2); let cfg = RetryConfig {
182 max_retries: 3,
183 base_delay: Duration::from_millis(1),
184 max_delay: Duration::from_millis(5),
185 };
186 let result = retry_chat(&llm, vec![Message::human("hi")], None, &cfg).await;
187 assert!(result.is_ok());
188 assert_eq!(llm.calls.load(Ordering::SeqCst), 3);
189 }
190
191 #[tokio::test]
192 async fn retry_exhausts_and_returns_last_error() {
193 let llm = FlakyChat::new(10); let cfg = RetryConfig {
195 max_retries: 2,
196 base_delay: Duration::from_millis(1),
197 max_delay: Duration::from_millis(5),
198 };
199 let result = retry_chat(&llm, vec![Message::human("hi")], None, &cfg).await;
200 assert!(result.is_err());
201 assert_eq!(llm.calls.load(Ordering::SeqCst), 3);
203 }
204}