llm_client.rs

  1//! Shared LLM client abstraction for Anthropic and OpenAI.
  2//!
  3//! This module provides a unified interface for making LLM requests,
  4//! supporting both synchronous and batch modes.
  5
  6use crate::BatchProvider;
  7use crate::anthropic_client::AnthropicClient;
  8use crate::openai_client::OpenAiClient;
  9use crate::paths::LLM_CACHE_DB;
 10use anyhow::Result;
 11
 12/// A unified LLM client that wraps either Anthropic or OpenAI.
 13pub enum LlmClient {
 14    Anthropic(AnthropicClient),
 15    OpenAi(OpenAiClient),
 16}
 17
 18impl LlmClient {
 19    /// Create a new LLM client for the given backend.
 20    ///
 21    /// If `batched` is true, requests will be queued for batch processing.
 22    /// Otherwise, requests are made synchronously.
 23    pub fn new(backend: BatchProvider, batched: bool) -> Result<Self> {
 24        match backend {
 25            BatchProvider::Anthropic => {
 26                if batched {
 27                    Ok(LlmClient::Anthropic(AnthropicClient::batch(&LLM_CACHE_DB)?))
 28                } else {
 29                    Ok(LlmClient::Anthropic(AnthropicClient::plain()?))
 30                }
 31            }
 32            BatchProvider::Openai => {
 33                if batched {
 34                    Ok(LlmClient::OpenAi(OpenAiClient::batch(&LLM_CACHE_DB)?))
 35                } else {
 36                    Ok(LlmClient::OpenAi(OpenAiClient::plain()?))
 37                }
 38            }
 39        }
 40    }
 41
 42    /// Generate a response from the LLM.
 43    ///
 44    /// Returns `Ok(None)` if the request was queued for batch processing
 45    /// and results are not yet available.
 46    pub async fn generate(
 47        &self,
 48        model: &str,
 49        max_tokens: u64,
 50        prompt: &str,
 51    ) -> Result<Option<String>> {
 52        match self {
 53            LlmClient::Anthropic(client) => {
 54                let messages = vec![anthropic::Message {
 55                    role: anthropic::Role::User,
 56                    content: vec![anthropic::RequestContent::Text {
 57                        text: prompt.to_string(),
 58                        cache_control: None,
 59                    }],
 60                }];
 61                let response = client.generate(model, max_tokens, messages, None).await?;
 62                Ok(response.map(|r| {
 63                    r.content
 64                        .iter()
 65                        .filter_map(|c| match c {
 66                            anthropic::ResponseContent::Text { text } => Some(text.as_str()),
 67                            _ => None,
 68                        })
 69                        .collect::<Vec<_>>()
 70                        .join("")
 71                }))
 72            }
 73            LlmClient::OpenAi(client) => {
 74                let messages = vec![open_ai::RequestMessage::User {
 75                    content: open_ai::MessageContent::Plain(prompt.to_string()),
 76                }];
 77                let response = client.generate(model, max_tokens, messages, None).await?;
 78                Ok(response.map(|r| {
 79                    r.choices
 80                        .into_iter()
 81                        .filter_map(|choice| match choice.message {
 82                            open_ai::RequestMessage::Assistant { content, .. } => {
 83                                content.map(|c| match c {
 84                                    open_ai::MessageContent::Plain(text) => text,
 85                                    open_ai::MessageContent::Multipart(parts) => parts
 86                                        .into_iter()
 87                                        .filter_map(|p| match p {
 88                                            open_ai::MessagePart::Text { text } => Some(text),
 89                                            _ => None,
 90                                        })
 91                                        .collect::<Vec<_>>()
 92                                        .join(""),
 93                                })
 94                            }
 95                            _ => None,
 96                        })
 97                        .collect::<Vec<_>>()
 98                        .join("")
 99                }))
100            }
101        }
102    }
103
104    /// Sync pending batches - upload queued requests and download completed results.
105    pub async fn sync_batches(&self) -> Result<()> {
106        match self {
107            LlmClient::Anthropic(client) => client.sync_batches().await,
108            LlmClient::OpenAi(client) => client.sync_batches().await,
109        }
110    }
111}
112
113/// Get the model name for a given backend.
114pub fn model_for_backend(backend: BatchProvider) -> &'static str {
115    match backend {
116        BatchProvider::Anthropic => "claude-sonnet-4-5",
117        BatchProvider::Openai => "gpt-5.2",
118    }
119}