use std::{
collections::HashMap,
pin::Pin,
sync::{Arc, Mutex},
time::Duration,
};
use async_trait::async_trait;
use futures::{Stream, StreamExt};
use serde_json::{json, Value};
use tokio::time::Instant;
use crate::{
language_models::{
box_llm_events, llm::LLM, options::CallOptions, result_events, GenerateResult, LLMError,
LLMEvent, LLMEventStream,
},
schemas::{Message, StreamData},
};
#[async_trait]
pub trait LLMCache: Send + Sync {
async fn get(&self, key: &str) -> Option<GenerateResult>;
async fn put(&self, key: &str, value: GenerateResult);
}
struct Entry {
value: GenerateResult,
inserted: Instant,
last_used: u64,
}
#[derive(Default)]
struct InMemoryState {
entries: HashMap<String, Entry>,
clock: u64,
}
#[derive(Default)]
pub struct InMemoryLLMCache {
state: Mutex<InMemoryState>,
capacity: Option<usize>,
ttl: Option<Duration>,
}
impl InMemoryLLMCache {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(mut self, capacity: usize) -> Self {
self.capacity = Some(capacity);
self
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn len(&self) -> usize {
self.state.lock().unwrap().entries.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
self.state.lock().unwrap().entries.clear();
}
fn expired(&self, entry: &Entry, now: Instant) -> bool {
self.ttl
.is_some_and(|ttl| now.duration_since(entry.inserted) >= ttl)
}
}
#[async_trait]
impl LLMCache for InMemoryLLMCache {
async fn get(&self, key: &str) -> Option<GenerateResult> {
let now = Instant::now();
let mut state = self.state.lock().unwrap();
state.clock += 1;
let clock = state.clock;
let expired = self.expired(state.entries.get(key)?, now);
if expired {
state.entries.remove(key);
return None;
}
let entry = state.entries.get_mut(key)?;
entry.last_used = clock;
Some(entry.value.clone())
}
async fn put(&self, key: &str, value: GenerateResult) {
let now = Instant::now();
let mut state = self.state.lock().unwrap();
state.clock += 1;
let clock = state.clock;
if self.capacity == Some(0) {
return;
}
if let Some(capacity) = self.capacity {
if !state.entries.contains_key(key) {
state.entries.retain(|_, e| !self.expired(e, now));
while state.entries.len() >= capacity {
let oldest = state
.entries
.iter()
.min_by_key(|(_, e)| e.last_used)
.map(|(k, _)| k.clone());
match oldest {
Some(k) => state.entries.remove(&k),
None => break,
};
}
}
}
state.entries.insert(
key.to_string(),
Entry {
value,
inserted: now,
last_used: clock,
},
);
}
}
pub struct CachedLLM {
llm: Box<dyn LLM>,
cache: Arc<dyn LLMCache>,
namespace: String,
options: CallOptions,
}
impl Clone for CachedLLM {
fn clone(&self) -> Self {
Self {
llm: self.llm.clone_box(),
cache: self.cache.clone(),
namespace: self.namespace.clone(),
options: self.options.clone(),
}
}
}
impl CachedLLM {
pub fn new<L: LLM + 'static>(llm: L, cache: Arc<dyn LLMCache>) -> Self {
Self {
llm: Box::new(llm),
cache,
namespace: std::any::type_name::<L>().to_string(),
options: CallOptions::new(),
}
}
pub fn with_namespace<S: Into<String>>(mut self, namespace: S) -> Self {
self.namespace = namespace.into();
self
}
pub fn cache_key(&self, messages: &[Message]) -> Result<String, LLMError> {
let key = json!({
"namespace": self.namespace,
"messages": messages,
"options": options_fingerprint(&self.options),
});
Ok(serde_json::to_string(&key)?)
}
async fn lookup(&self, key: &str) -> Option<GenerateResult> {
let mut hit = self.cache.get(key).await?;
hit.tokens = None;
Some(hit)
}
}
fn options_fingerprint(o: &CallOptions) -> Value {
json!({
"candidate_count": o.candidate_count,
"max_tokens": o.max_tokens,
"temperature": o.temperature,
"stop_words": o.stop_words,
"top_k": o.top_k,
"top_p": o.top_p,
"seed": o.seed,
"min_length": o.min_length,
"max_length": o.max_length,
"n": o.n,
"repetition_penalty": o.repetition_penalty,
"frequency_penalty": o.frequency_penalty,
"presence_penalty": o.presence_penalty,
"functions": o.functions.as_ref().map(|fs| {
fs.iter()
.map(|f| json!([f.name, f.description, f.parameters]))
.collect::<Vec<_>>()
}),
"function_call_behavior": o.function_call_behavior.as_ref().map(|b| format!("{b:?}")),
"response_format": o.response_format.as_ref().map(|r| format!("{r:?}")),
})
}
#[async_trait]
impl LLM for CachedLLM {
async fn generate(&self, messages: &[Message]) -> Result<GenerateResult, LLMError> {
let key = self.cache_key(messages)?;
if let Some(hit) = self.lookup(&key).await {
if let Some(func) = &self.options.streaming_func {
let mut func = func.lock().await;
func(hit.generation.clone())
.await
.map_err(|_| LLMError::OtherError("streaming_func failed".into()))?;
}
return Ok(hit);
}
let result = self.llm.generate(messages).await?;
self.cache.put(&key, result.clone()).await;
Ok(result)
}
async fn stream(
&self,
messages: &[Message],
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
let key = self.cache_key(messages)?;
if let Some(hit) = self.lookup(&key).await {
let chunk = StreamData::new(json!(hit.generation), None, hit.generation);
return Ok(Box::pin(futures::stream::iter([Ok(chunk)])));
}
self.llm.stream(messages).await
}
fn generate_stream<'a>(&'a self, messages: &'a [Message]) -> LLMEventStream<'a> {
box_llm_events(async_stream::try_stream! {
let key = self.cache_key(messages)?;
if let Some(hit) = self.lookup(&key).await {
for event in result_events(hit) {
yield event;
}
return;
}
let mut events = self.llm.generate_stream(messages);
while let Some(event) = events.next().await {
let event = event?;
if let LLMEvent::Done(result) = &event {
self.cache.put(&key, result.clone()).await;
}
yield event;
}
})
}
fn add_options(&mut self, options: CallOptions) {
self.options.merge_options(options.clone());
self.llm.add_options(options);
}
fn messages_to_string(&self, messages: &[Message]) -> String {
self.llm.messages_to_string(messages)
}
}
#[cfg(test)]
mod tests;