mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +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>
174 lines
5.2 KiB
Rust
174 lines
5.2 KiB
Rust
use litellm_cache_response::{
|
|
CacheControls, CacheKeyContext, CacheKeyField, CacheKeyInput, cache_key, get_cache_key,
|
|
should_use_cache,
|
|
};
|
|
use rstest::rstest;
|
|
use sha2::{Digest, Sha256};
|
|
|
|
fn field(name: &str, value: Option<&str>) -> CacheKeyField {
|
|
CacheKeyField {
|
|
name: name.into(),
|
|
value: value.map(str::to_owned),
|
|
api_parameter: true,
|
|
internal_parameter: false,
|
|
}
|
|
}
|
|
|
|
fn hash(preimage: &[u8]) -> String {
|
|
format!("{:x}", Sha256::digest(preimage))
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::caching_group_and_checksum(
|
|
CacheKeyContext {
|
|
model_group: Some("group".into()),
|
|
caching_groups: vec![(vec!["group".into()], "['group']".into())],
|
|
file_checksum: Some("checksum".into()),
|
|
..Default::default()
|
|
},
|
|
Some("team"),
|
|
"team:",
|
|
b"model: ['group']file: checksum".as_slice(),
|
|
)]
|
|
#[case::model_group_outside_caching_groups(
|
|
CacheKeyContext {
|
|
model_group: Some("group".into()),
|
|
caching_groups: vec![(vec!["other".into()], "['other']".into())],
|
|
file_object_name: Some("object".into()),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
"",
|
|
b"model: groupfile: object".as_slice(),
|
|
)]
|
|
#[case::metadata_file_name_before_parameters(
|
|
CacheKeyContext {
|
|
metadata_file_name: Some("metadata".into()),
|
|
parameters_file_name: Some("parameters".into()),
|
|
..Default::default()
|
|
},
|
|
Some(""),
|
|
"",
|
|
b"model: deploymentfile: metadata".as_slice(),
|
|
)]
|
|
#[case::parameters_file_name_last(
|
|
CacheKeyContext {
|
|
parameters_file_name: Some("parameters".into()),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
"",
|
|
b"model: deploymentfile: parameters".as_slice(),
|
|
)]
|
|
#[case::no_context_keeps_the_request_model(
|
|
CacheKeyContext::default(),
|
|
Some("team"),
|
|
"team:",
|
|
b"model: deployment".as_slice(),
|
|
)]
|
|
fn keys_match_python_order_groups_files_and_namespaces(
|
|
#[case] context: CacheKeyContext,
|
|
#[case] namespace: Option<&str>,
|
|
#[case] prefix: &str,
|
|
#[case] preimage: &[u8],
|
|
) {
|
|
let mut input = CacheKeyInput {
|
|
fields: vec![field("model", Some("deployment")), field("file", None)],
|
|
namespace: namespace.map(str::to_owned),
|
|
..Default::default()
|
|
};
|
|
context.apply(&mut input);
|
|
let expected = format!("{prefix}{}", hash(preimage));
|
|
assert_eq!(cache_key(&input), expected);
|
|
assert_eq!(get_cache_key(&input), expected);
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::api_parameter(true, false, false, true)]
|
|
#[case::provider_parameter_when_included(false, false, true, true)]
|
|
#[case::provider_parameter_when_excluded(false, false, false, false)]
|
|
#[case::internal_parameter_never(false, true, true, false)]
|
|
fn keys_hash_api_and_opted_in_provider_parameters(
|
|
#[case] api_parameter: bool,
|
|
#[case] internal_parameter: bool,
|
|
#[case] include_provider_parameters: bool,
|
|
#[case] hashed: bool,
|
|
) {
|
|
let input = CacheKeyInput {
|
|
fields: vec![
|
|
field("model", Some("a")),
|
|
CacheKeyField {
|
|
name: "extra".into(),
|
|
value: Some("x".into()),
|
|
api_parameter,
|
|
internal_parameter,
|
|
},
|
|
],
|
|
include_provider_parameters,
|
|
..Default::default()
|
|
};
|
|
let preimage: &[u8] = if hashed {
|
|
b"model: aextra: x"
|
|
} else {
|
|
b"model: a"
|
|
};
|
|
assert_eq!(cache_key(&input), hash(preimage));
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::without_namespace(None)]
|
|
#[case::with_namespace(Some("team"))]
|
|
fn preset_keys_are_used_verbatim(#[case] namespace: Option<&str>) {
|
|
let input = CacheKeyInput {
|
|
fields: vec![field("model", Some("a"))],
|
|
preset: Some("preset".into()),
|
|
namespace: namespace.map(str::to_owned),
|
|
..Default::default()
|
|
};
|
|
assert_eq!(cache_key(&input), "preset");
|
|
assert_eq!(get_cache_key(&input), "preset");
|
|
}
|
|
|
|
const ENABLED: CacheControls = CacheControls {
|
|
supported_call_type: true,
|
|
configured: true,
|
|
native_backend: false,
|
|
default_on: true,
|
|
caching: None,
|
|
no_cache: false,
|
|
no_store: false,
|
|
use_cache: false,
|
|
};
|
|
|
|
#[rstest]
|
|
#[case::enabled(ENABLED, true, true)]
|
|
#[case::default_off(CacheControls { default_on: false, ..ENABLED }, false, false)]
|
|
#[case::default_off_with_use_cache(
|
|
CacheControls { default_on: false, use_cache: true, ..ENABLED },
|
|
true,
|
|
true
|
|
)]
|
|
#[case::no_cache(CacheControls { no_cache: true, ..ENABLED }, false, true)]
|
|
#[case::no_store(CacheControls { no_store: true, ..ENABLED }, true, false)]
|
|
#[case::no_cache_and_no_store(
|
|
CacheControls { no_cache: true, no_store: true, ..ENABLED },
|
|
false,
|
|
false
|
|
)]
|
|
#[case::caching_disabled(CacheControls { caching: Some(false), ..ENABLED }, false, false)]
|
|
#[case::caching_enabled(CacheControls { caching: Some(true), ..ENABLED }, true, true)]
|
|
#[case::unsupported_call_type(
|
|
CacheControls { supported_call_type: false, ..ENABLED },
|
|
false,
|
|
false
|
|
)]
|
|
#[case::unconfigured(CacheControls { configured: false, ..ENABLED }, false, false)]
|
|
fn cache_controls_honor_default_modes_and_directives(
|
|
#[case] controls: CacheControls,
|
|
#[case] reads: bool,
|
|
#[case] writes: bool,
|
|
) {
|
|
assert_eq!(controls.reads(), reads);
|
|
assert_eq!(controls.writes(), writes);
|
|
assert_eq!(should_use_cache(controls), reads || writes);
|
|
}
|