mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +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>
123 lines
3.8 KiB
Rust
123 lines
3.8 KiB
Rust
mod support;
|
|
|
|
use std::{sync::Arc, time::Duration};
|
|
|
|
use litellm_cache::CacheConnectionStatus;
|
|
use litellm_cache_memory::InMemoryCache;
|
|
use litellm_cache_response::{
|
|
CacheEntry, ConnectionProbe, ExactResponseCache, ResponseCache, ResponseCacheRequest,
|
|
};
|
|
use redis_test::MockCmd;
|
|
use rstest::rstest;
|
|
use serde_json::json;
|
|
use support::{keyed, memory, redis, request};
|
|
|
|
#[rstest]
|
|
#[case::reachable(
|
|
Ok("PONG"),
|
|
CacheConnectionStatus::Success,
|
|
"Redis connection test successful",
|
|
false
|
|
)]
|
|
#[case::unexpected_reply(
|
|
Ok("NOPE"),
|
|
CacheConnectionStatus::Failed,
|
|
"Redis ping returned False",
|
|
false
|
|
)]
|
|
#[case::connection_refused(
|
|
Err(redis::RedisError::from((redis::ErrorKind::Io, "connection refused"))),
|
|
CacheConnectionStatus::Failed,
|
|
"Redis connection failed:",
|
|
true
|
|
)]
|
|
#[tokio::test]
|
|
async fn connection_backends_are_reachable_as_a_probe(
|
|
#[case] reply: redis::RedisResult<&'static str>,
|
|
#[case] status: CacheConnectionStatus,
|
|
#[case] message: &str,
|
|
#[case] has_error: bool,
|
|
) {
|
|
let probe: Arc<dyn ConnectionProbe> =
|
|
Arc::new(redis(vec![MockCmd::new(redis::cmd("PING"), reply)], None));
|
|
|
|
let result = probe.test_connection().await.unwrap();
|
|
assert_eq!(result.status, status);
|
|
assert!(result.message.starts_with(message), "{}", result.message);
|
|
assert_eq!(result.error.is_some(), has_error);
|
|
}
|
|
|
|
#[rstest]
|
|
#[tokio::test]
|
|
async fn one_service_serves_both_the_exact_cache_and_its_probe(request: ResponseCacheRequest) {
|
|
let service = Arc::new(redis(
|
|
vec![
|
|
MockCmd::new(redis::cmd("PING"), Ok("PONG")),
|
|
MockCmd::new(
|
|
redis::cmd("GET").arg("tenant:key"),
|
|
Ok(br#"{"timestamp":100.0,"response":{"ok":true}}"#.to_vec()),
|
|
),
|
|
],
|
|
Some("tenant"),
|
|
));
|
|
let probe: Arc<dyn ConnectionProbe> = service.clone();
|
|
let exact: Arc<dyn ExactResponseCache> = service;
|
|
|
|
assert_eq!(
|
|
probe.test_connection().await.unwrap().status,
|
|
CacheConnectionStatus::Success
|
|
);
|
|
assert_eq!(
|
|
exact
|
|
.async_lookup(&request, Duration::from_secs(100))
|
|
.await
|
|
.unwrap(),
|
|
Some(json!({"ok": true}))
|
|
);
|
|
}
|
|
|
|
/// The in-memory backend has no `test_connection`, as in Python, and still serves every response
|
|
/// operation.
|
|
#[rstest]
|
|
#[tokio::test]
|
|
async fn backends_without_a_connection_test_serve_every_response_operation(
|
|
#[from(memory)] service: Arc<ResponseCache<InMemoryCache<CacheEntry>>>,
|
|
request: ResponseCacheRequest,
|
|
) {
|
|
let cache: Arc<dyn ExactResponseCache> = service;
|
|
let now = Duration::from_secs(100);
|
|
let other = keyed("tenant:other");
|
|
let missing = keyed("tenant:missing");
|
|
|
|
assert_eq!(cache.default_ttl(), Some(Duration::from_secs(600)));
|
|
cache.store(&request, json!({"v": 1}), now).unwrap();
|
|
assert_eq!(cache.lookup(&request, now).unwrap(), Some(json!({"v": 1})));
|
|
cache
|
|
.async_store(&other, json!({"v": 2}), now)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
cache.async_lookup(&other, now).await.unwrap(),
|
|
Some(json!({"v": 2}))
|
|
);
|
|
|
|
let requests = [request.clone(), missing.clone(), other.clone()];
|
|
let partial = cache.lookup_batch(&requests, now).unwrap();
|
|
assert_eq!(
|
|
partial.values,
|
|
vec![Some(json!({"v": 1})), None, Some(json!({"v": 2}))]
|
|
);
|
|
assert_eq!(partial.missing_indices, vec![1]);
|
|
|
|
cache
|
|
.async_store_batch(vec![(missing.clone(), json!({"v": 3}))], now)
|
|
.await
|
|
.unwrap();
|
|
let partial = cache.async_lookup_batch(&requests, now).await.unwrap();
|
|
assert!(partial.missing_indices.is_empty());
|
|
assert_eq!(partial.values[1], Some(json!({"v": 3})));
|
|
|
|
cache.async_flush().await.unwrap();
|
|
let partial = cache.async_lookup_batch(&requests, now).await.unwrap();
|
|
assert_eq!(partial.missing_indices, vec![0, 1, 2]);
|
|
}
|