mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
* refactor(rust): align the cache crates with Python and activate every backend The cache port had drifted: lifecycle and Redis-only operations sat on `BaseCache`, counters were pinned to `f64`, each semantic backend defined its own embedder and prompt handling, and only the in-memory backend could be selected natively. - Split `disconnect` and `test_connection` out of `BaseCache` into optional capabilities, implemented only where the Python class defines them, and give every Redis-only operation its own capability trait. - Decouple counters from the stored value type, so one backend can serve both responses and counters as Python's `RedisCache` does. - Share one `Embedder` and prompt contract in `litellm_cache::semantic`, and make the Redis and Valkey semantic backends generic over their codec. - Port the Python operations that were missing: `async_refresh_ttl`, `async_rpush_and_trim`, `async_set_cache_pipeline_with_ttls`, the DualCache pipeline, sadd, bulk delete and TTL reads, and the semantic-similarity write-back. - Take the HTTP client from the host pool in the GCS, S3 and Azure backends. - Activate all nine backends through the Rust catalog, whose rules all stay `PYTHON_ONLY`, and route the `Cache` facade's storage calls to the native runtime when one is selected. - Give every crate the same layout, move all tests to `tests/` on rstest, and add the shared `litellm-cache-testing` contract suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: freeze native cache request kwargs and batch entries for type discipline Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: declare semantic lookup methods in the native stub Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(rust): align the cache crates with Python and activate every backend The cache port had drifted: lifecycle and Redis-only operations sat on `BaseCache`, counters were pinned to `f64`, each semantic backend defined its own embedder and prompt handling, and only the in-memory backend could be selected natively. - Split `disconnect` and `test_connection` out of `BaseCache` into optional capabilities, implemented only where the Python class defines them, and give every Redis-only operation its own capability trait. - Decouple counters from the stored value type, so one backend can serve both responses and counters as Python's `RedisCache` does. - Share one `Embedder` and prompt contract in `litellm_cache::semantic`, and make the Redis and Valkey semantic backends generic over their codec. - Port the Python operations that were missing: `async_refresh_ttl`, `async_rpush_and_trim`, `async_set_cache_pipeline_with_ttls`, the DualCache pipeline, sadd, bulk delete and TTL reads, and the semantic-similarity write-back. - Take the HTTP client from the host pool in the GCS, S3 and Azure backends. - Activate all nine backends through the Rust catalog, whose rules all stay `PYTHON_ONLY`, and route the `Cache` facade's storage calls to the native runtime when one is selected. - Give every crate the same layout, move all tests to `tests/` on rstest, and add the shared `litellm-cache-testing` contract suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: freeze native cache request kwargs and batch entries for type discipline Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: declare semantic lookup methods in the native stub Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(rust): opt the native Messages and tokenizer suites into Rust explicitly #42517 made the Messages, token counter and tokenizer routes Python-only, so tests/test_litellm_rust silently exercised the Python path or failed outright. Each suite now prepends a RUST_OPT_IN rule for its route, keeping native coverage without changing the shipped default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(rust): pop one at a time in the Redis 6 lpop pipeline and drop explanatory comments Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
283 lines
8.5 KiB
Rust
283 lines
8.5 KiB
Rust
use std::{
|
|
path::Path,
|
|
sync::Arc,
|
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use litellm_cache::{
|
|
BaseCache, BatchCache, BatchEntry, CacheCodec, CounterCache, DeleteCache, DisconnectCache,
|
|
Error, ExactCacheContext, FlushCache,
|
|
};
|
|
|
|
use crate::{DiskStore, DiskcacheSqliteStore, PythonDiskCacheAdapter, StoredValue, ValueAdapter};
|
|
|
|
pub struct DiskCache<S, D = DiskcacheSqliteStore, A = PythonDiskCacheAdapter> {
|
|
store: Arc<D>,
|
|
adapter: Arc<A>,
|
|
codec: S,
|
|
}
|
|
|
|
impl<S: CacheCodec> DiskCache<S> {
|
|
pub fn open(directory: impl AsRef<Path>, codec: S) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
store: Arc::new(DiskcacheSqliteStore::open(directory)?),
|
|
adapter: Arc::new(PythonDiskCacheAdapter),
|
|
codec,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore> DiskCache<S, D, PythonDiskCacheAdapter> {
|
|
pub fn with_store(store: D, codec: S) -> Self {
|
|
Self {
|
|
store: Arc::new(store),
|
|
adapter: Arc::new(PythonDiskCacheAdapter),
|
|
codec,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> DiskCache<S, D, A> {
|
|
pub fn with_adapter(store: D, adapter: A, codec: S) -> Self {
|
|
Self {
|
|
store: Arc::new(store),
|
|
adapter: Arc::new(adapter),
|
|
codec,
|
|
}
|
|
}
|
|
|
|
pub fn directory(&self) -> &Path {
|
|
self.store.directory()
|
|
}
|
|
|
|
fn decode_stored(&self, value: StoredValue) -> Result<Option<S::Value>, Error> {
|
|
let Some(bytes) = self.adapter.read(value)? else {
|
|
return Ok(None);
|
|
};
|
|
self.codec.decode(&bytes).map(Some)
|
|
}
|
|
|
|
async fn run_blocking<T, F>(store: Arc<D>, operation: F) -> Result<T, Error>
|
|
where
|
|
T: Send + 'static,
|
|
F: FnOnce(&D) -> Result<T, Error> + Send + 'static,
|
|
{
|
|
tokio::task::spawn_blocking(move || operation(&store))
|
|
.await
|
|
.map_err(|_| Error::Unavailable)?
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> BaseCache for DiskCache<S, D, A> {
|
|
type Value = S::Value;
|
|
type Context = ExactCacheContext;
|
|
|
|
fn get_ttl(&self, context: &Self::Context) -> Option<Duration> {
|
|
context.ttl
|
|
}
|
|
|
|
fn set_cache(
|
|
&self,
|
|
key: &str,
|
|
value: Self::Value,
|
|
context: &Self::Context,
|
|
) -> Result<(), Error> {
|
|
let value = self.adapter.write(self.codec.encode(&value)?);
|
|
let expire_time = context.ttl.map(|ttl| unix_now() + ttl.as_secs_f64());
|
|
self.store.set(key, value, expire_time, unix_now())
|
|
}
|
|
|
|
fn get_cache(&self, key: &str, _: &Self::Context) -> Result<Option<Self::Value>, Error> {
|
|
self.store
|
|
.get(key, unix_now())?
|
|
.map(|value| self.decode_stored(value))
|
|
.transpose()
|
|
.map(|value| value.flatten())
|
|
}
|
|
|
|
async fn async_set_cache(
|
|
&self,
|
|
key: &str,
|
|
value: Self::Value,
|
|
context: ExactCacheContext,
|
|
) -> Result<(), Error> {
|
|
let value = self.adapter.write(self.codec.encode(&value)?);
|
|
let ttl = context.ttl;
|
|
let key = key.to_string();
|
|
Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
let expire_time = ttl.map(|ttl| unix_now() + ttl.as_secs_f64());
|
|
store.set(&key, value, expire_time, unix_now())
|
|
})
|
|
.await
|
|
}
|
|
|
|
async fn async_get_cache(
|
|
&self,
|
|
key: &str,
|
|
_: &ExactCacheContext,
|
|
) -> Result<Option<Self::Value>, Error> {
|
|
let key = key.to_string();
|
|
let value = Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
store.get(&key, unix_now())
|
|
})
|
|
.await?;
|
|
value
|
|
.map(|value| self.decode_stored(value))
|
|
.transpose()
|
|
.map(|value| value.flatten())
|
|
}
|
|
|
|
async fn async_set_cache_pipeline(
|
|
&self,
|
|
entries: Vec<(String, Self::Value)>,
|
|
context: ExactCacheContext,
|
|
) -> Result<(), Error> {
|
|
let entries = entries
|
|
.into_iter()
|
|
.map(|(key, value)| {
|
|
self.codec
|
|
.encode(&value)
|
|
.map(|value| (key, self.adapter.write(value)))
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let expire_after = context.ttl;
|
|
Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
for (key, value) in entries {
|
|
let expire_time = expire_after.map(|ttl| unix_now() + ttl.as_secs_f64());
|
|
store.set(&key, value, expire_time, unix_now())?;
|
|
}
|
|
Ok(())
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> BatchCache for DiskCache<S, D, A> {
|
|
fn batch_get_cache(
|
|
&self,
|
|
keys: &[String],
|
|
context: &ExactCacheContext,
|
|
) -> Result<Vec<BatchEntry<Self::Value>>, Error> {
|
|
keys.iter()
|
|
.map(|key| match self.get_cache(key, context) {
|
|
Ok(Some(value)) => Ok(BatchEntry::Hit(value)),
|
|
Ok(None) => Ok(BatchEntry::Miss),
|
|
Err(Error::InvalidEntry) => Ok(BatchEntry::Invalid),
|
|
Err(error) => Err(error),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn async_batch_get_cache(
|
|
&self,
|
|
keys: Vec<String>,
|
|
_: ExactCacheContext,
|
|
) -> Result<Vec<BatchEntry<Self::Value>>, Error> {
|
|
let values = Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
keys.into_iter()
|
|
.map(|key| store.get(&key, unix_now()).map(|value| (key, value)))
|
|
.collect::<Result<Vec<_>, _>>()
|
|
})
|
|
.await?;
|
|
values
|
|
.into_iter()
|
|
.map(|(_, value)| match value {
|
|
None => Ok(BatchEntry::Miss),
|
|
Some(value) => match self.decode_stored(value) {
|
|
Ok(Some(value)) => Ok(BatchEntry::Hit(value)),
|
|
Ok(None) => Ok(BatchEntry::Miss),
|
|
Err(Error::InvalidEntry) => Ok(BatchEntry::Invalid),
|
|
Err(error) => Err(error),
|
|
},
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> DeleteCache for DiskCache<S, D, A> {
|
|
fn delete_cache(&self, key: &str) -> Result<(), Error> {
|
|
self.store.pop(key, unix_now()).map(|_| ())
|
|
}
|
|
|
|
async fn async_delete_cache(&self, key: &str) -> Result<(), Error> {
|
|
let key = key.to_string();
|
|
Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
store.pop(&key, unix_now()).map(|_| ())
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> FlushCache for DiskCache<S, D, A> {
|
|
fn flush_cache(&self) -> Result<(), Error> {
|
|
self.store.clear()
|
|
}
|
|
|
|
async fn async_flush_cache(&self) -> Result<(), Error> {
|
|
Self::run_blocking(Arc::clone(&self.store), |store| store.clear()).await
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> DisconnectCache for DiskCache<S, D, A> {
|
|
async fn disconnect(&self) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<S: CacheCodec, D: DiskStore, A: ValueAdapter> CounterCache for DiskCache<S, D, A> {
|
|
fn increment_cache(
|
|
&self,
|
|
key: &str,
|
|
amount: f64,
|
|
context: ExactCacheContext,
|
|
) -> Result<f64, Error> {
|
|
increment(
|
|
self.adapter.as_ref(),
|
|
self.store.as_ref(),
|
|
key,
|
|
amount,
|
|
context.ttl,
|
|
)
|
|
}
|
|
|
|
async fn async_increment(
|
|
&self,
|
|
key: &str,
|
|
amount: f64,
|
|
context: ExactCacheContext,
|
|
_refresh_ttl: bool,
|
|
) -> Result<f64, Error> {
|
|
let key = key.to_string();
|
|
let adapter = Arc::clone(&self.adapter);
|
|
Self::run_blocking(Arc::clone(&self.store), move |store| {
|
|
increment(adapter.as_ref(), store, &key, amount, context.ttl)
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
fn increment<A: ValueAdapter, D: DiskStore>(
|
|
adapter: &A,
|
|
store: &D,
|
|
key: &str,
|
|
amount: f64,
|
|
ttl: Option<Duration>,
|
|
) -> Result<f64, Error> {
|
|
let mut result = None;
|
|
let mut apply = |current: Option<StoredValue>| {
|
|
let initial = adapter.counter_seed(current)?;
|
|
let value = initial + amount;
|
|
let stored = adapter.counter_value(value);
|
|
result = Some(value);
|
|
Ok((stored, ttl.map(|ttl| unix_now() + ttl.as_secs_f64())))
|
|
};
|
|
store.update(key, unix_now(), &mut apply)?;
|
|
result.ok_or(Error::InvalidEntry)
|
|
}
|
|
|
|
fn unix_now() -> f64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs_f64()
|
|
}
|