use std::sync::Arc;
use futures_core::Stream;
use lellm_core::{ChatRequest, LlmError, Message};
use lellm_provider::{ProviderEvent, ProviderStream, ResolvedModel};
use std::pin::Pin;
use std::task::{Context, Poll};
use super::config::ToolUseConfig;
use super::fallback::{FallbackAction, FallbackContext, FallbackStrategy};
use super::retry::BackoffStrategy;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamState {
NotStarted,
HeadersReceived,
FirstChunkSent,
Finished,
}
impl StreamState {
pub fn can_retry(&self) -> bool {
matches!(self, Self::NotStarted | Self::HeadersReceived)
}
}
#[derive(Clone)]
pub struct LlmInvoker {
model: ResolvedModel,
fallback: Arc<dyn FallbackStrategy>,
max_retries: usize,
backoff: BackoffStrategy,
}
impl LlmInvoker {
pub fn new(
model: ResolvedModel,
fallback: Arc<dyn FallbackStrategy>,
max_retries: usize,
backoff: BackoffStrategy,
) -> Self {
Self {
model,
fallback,
max_retries,
backoff,
}
}
pub fn from_config(
model: ResolvedModel,
config: &ToolUseConfig,
fallback: Arc<dyn FallbackStrategy>,
) -> Self {
Self::new(
model,
fallback,
config.retry_policy.max_attempts() as usize,
config.retry_policy.backoff().clone(),
)
}
pub fn model(&self) -> &ResolvedModel {
&self.model
}
pub async fn invoke_stream(
&self,
req: &ChatRequest,
messages: &[Message],
iteration: usize,
) -> Result<RetryAwareStream, LlmError> {
let mut attempt = 1;
let mut stream_state = StreamState::NotStarted;
loop {
match self.model.provider.stream(req).await {
Ok(stream) => {
return Ok(RetryAwareStream::new(stream, attempt));
}
Err(ref err) => {
tracing::warn!(
attempt = attempt,
error = %err,
stream_state = ?stream_state,
"LLM stream invocation failed"
);
if !stream_state.can_retry() {
return Err(err.clone());
}
let ctx = FallbackContext {
error: err,
attempt,
iterations: iteration,
conversation: Arc::from(messages),
};
match self.fallback.handle(&ctx).await {
FallbackAction::Retry => {
if attempt >= self.max_retries {
tracing::warn!(
attempt,
max_retries = self.max_retries,
"max retries reached, aborting"
);
return Err(err.clone());
}
let delay = self.backoff.delay(attempt as u32);
tracing::info!(
attempt = attempt + 1,
max_retries = self.max_retries,
delay_ms = delay.as_millis(),
"retrying LLM invocation"
);
tokio::time::sleep(delay).await;
attempt += 1;
stream_state = StreamState::HeadersReceived;
}
FallbackAction::Abort => {
return Err(err.clone());
}
}
}
}
}
}
pub async fn invoke(
&self,
req: &ChatRequest,
messages: &[Message],
iteration: usize,
) -> Result<lellm_core::ChatResponse, LlmError> {
let mut attempt = 1;
loop {
match self.model.provider.call(req).await {
Ok(response) => return Ok(response),
Err(ref err) => {
tracing::warn!(
attempt = attempt,
error = %err,
"LLM invocation failed"
);
let ctx = FallbackContext {
error: err,
attempt,
iterations: iteration,
conversation: Arc::from(messages),
};
match self.fallback.handle(&ctx).await {
FallbackAction::Retry => {
if attempt >= self.max_retries {
return Err(err.clone());
}
let delay = self.backoff.delay(attempt as u32);
tokio::time::sleep(delay).await;
attempt += 1;
}
FallbackAction::Abort => return Err(err.clone()),
}
}
}
}
}
}
pub struct RetryAwareStream {
inner: Pin<Box<dyn Stream<Item = Result<ProviderEvent, LlmError>> + Send>>,
has_sent_data: bool,
attempt: usize,
}
impl RetryAwareStream {
fn new(stream: ProviderStream, attempt: usize) -> Self {
Self {
inner: stream,
has_sent_data: false,
attempt,
}
}
pub fn stream_state(&self) -> StreamState {
if self.has_sent_data {
StreamState::FirstChunkSent
} else {
StreamState::HeadersReceived
}
}
pub fn attempt(&self) -> usize {
self.attempt
}
}
impl Stream for RetryAwareStream {
type Item = Result<ProviderEvent, LlmError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let result = Pin::new(&mut self.inner).poll_next(cx);
if let Poll::Ready(Some(Ok(ref event))) = result {
if matches!(
event,
ProviderEvent::Token { .. } | ProviderEvent::ThinkingDelta { .. }
) {
self.has_sent_data = true;
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_state_can_retry() {
assert!(StreamState::NotStarted.can_retry());
assert!(StreamState::HeadersReceived.can_retry());
assert!(!StreamState::FirstChunkSent.can_retry());
assert!(!StreamState::Finished.can_retry());
}
}