mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-30 01:52:18 +00:00
* refactor(cache): organize v2 cache as a package * docs: clarify experimental v2 guidance * fix(cache): verify cache-hit accounting and preserve logging metadata * refactor(cache): separate execution facts from host accounting * refactor(rust): build messages routes with named dependencies * wip * fix(cache): preserve facade policy and preflight fallback * refactor(cache): defer shared Python logging changes * test(gateway-inference): allow dead code in shared test helpers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(cache): key prepared requests and honor facade controls * feat(cache): use Python caches from Rust Messages inference * refactor(cache): separate native and Python cache adapters * refactor(cache): enforce shared composition and adapter boundaries * fix(cache): let Python key delegated Rust Messages entries --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
167 lines
6.3 KiB
Python
167 lines
6.3 KiB
Python
import json
|
|
import time
|
|
from datetime import datetime
|
|
from types import SimpleNamespace
|
|
from typing import Final, cast
|
|
from unittest.mock import Mock
|
|
|
|
import boto3
|
|
import botocore.config
|
|
import pytest
|
|
|
|
from litellm.caching.caching import Cache
|
|
from litellm.caching.s3_cache import S3Cache
|
|
from litellm.rust_bridge import _native
|
|
from litellm.types.caching import LiteLLMCacheType
|
|
from tests.test_litellm_rust.support.cache import CacheTestResolver, activate_native, native_runtime, request
|
|
from tests.test_litellm_rust.support.isolation import rebound
|
|
from tests.test_litellm_rust.support.s3_stub import S3Stub
|
|
|
|
pytestmark: Final = pytest.mark.requires_rust_extension
|
|
|
|
|
|
def python_s3(url: str) -> S3Cache:
|
|
return S3Cache(
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
)
|
|
|
|
|
|
def s3_facade(url: str) -> Cache:
|
|
return Cache(
|
|
type=LiteLLMCacheType.S3,
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
)
|
|
|
|
|
|
async def test_s3_reads_python_entries_and_writes_with_python_metadata(s3_stub: S3Stub) -> None:
|
|
python_cache: Final = python_s3(s3_stub.url)
|
|
response: Final = {"choices": [{"text": "cached"}], "usage": {"total_tokens": 3}}
|
|
python_cache.set_cache("sync:key", {"timestamp": time.time(), "response": response}, ttl=90)
|
|
python_cache.set_cache("plain", {"timestamp": time.time(), "response": response})
|
|
s3_stub.put_object("team/malformed", b"not a cache entry")
|
|
s3_stub.put_object(
|
|
"team/expired",
|
|
json.dumps({"timestamp": time.time(), "response": response}).encode(),
|
|
{"expires": "Thu, 01 Jan 1970 00:00:00 GMT"},
|
|
)
|
|
binding: Final = native_runtime(s3_facade(s3_stub.url))
|
|
|
|
assert binding.lookup(request("sync:key")) == response
|
|
assert await binding.async_lookup(request("plain")) == response
|
|
assert binding.lookup(request("malformed")) is None
|
|
assert binding.lookup(request("expired")) is None
|
|
assert binding.lookup(request("absent")) is None
|
|
|
|
binding.store({**request("native:key"), "ttl_seconds": 90.0}, response)
|
|
await binding.async_store(request("no_ttl"), response)
|
|
stored: Final = s3_stub.objects["team/native/key"]
|
|
assert stored.headers["content-type"] == "application/json"
|
|
assert stored.headers["content-language"] == "en"
|
|
assert stored.headers["content-disposition"] == 'inline; filename="team/native/key.json"'
|
|
assert stored.headers["cache-control"] == "immutable, max-age=90, s-maxage=90"
|
|
expires: Final = cast(datetime, s3_stub.expires("team/native/key"))
|
|
remaining: Final = (expires - datetime.now(expires.tzinfo)).total_seconds()
|
|
assert 60 < remaining <= 91
|
|
no_ttl: Final = s3_stub.objects["team/no_ttl"]
|
|
assert no_ttl.headers["cache-control"] == "immutable, max-age=31536000, s-maxage=31536000"
|
|
assert "expires" not in no_ttl.headers
|
|
assert python_cache.get_cache("native:key")["response"] == response
|
|
|
|
partial: Final = await binding.async_lookup_batch([request("native:key"), request("absent"), request("malformed")])
|
|
assert partial == {"values": [response, None, None], "missing_indices": [1, 2]}
|
|
|
|
|
|
def test_selected_s3_runtime_declines_backend_mutation(s3_stub: S3Stub) -> None:
|
|
facade: Final = Cache(
|
|
type=LiteLLMCacheType.S3,
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=s3_stub.url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
)
|
|
activate_native(facade)
|
|
resolver: Final = CacheTestResolver(SimpleNamespace(cache=facade))
|
|
binding: Final = resolver.resolve()
|
|
assert binding.kind == "native"
|
|
|
|
handler: Final = Mock()
|
|
facade.cache.s3_client.meta.events.register("before-call.s3.*", handler)
|
|
binding.store(request("native"), {"answer": 1})
|
|
assert binding.lookup(request("native")) == {"answer": 1}
|
|
assert handler.call_count == 0
|
|
assert "team/native" in s3_stub.objects
|
|
|
|
with rebound(facade.cache, "bucket_name", "other"):
|
|
with pytest.raises(_native.RustBridgeDeclined):
|
|
resolver.resolve()
|
|
other_client: Final = boto3.client(
|
|
"s3",
|
|
region_name="us-east-1",
|
|
endpoint_url=s3_stub.url,
|
|
aws_access_key_id="key",
|
|
aws_secret_access_key="secret",
|
|
)
|
|
with rebound(facade.cache, "s3_client", other_client):
|
|
with pytest.raises(_native.RustBridgeDeclined):
|
|
resolver.resolve()
|
|
|
|
class CustomS3Cache(S3Cache):
|
|
pass
|
|
|
|
subclassed: Final = Cache(
|
|
type=LiteLLMCacheType.S3,
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=s3_stub.url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
)
|
|
subclassed.cache = CustomS3Cache(
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=s3_stub.url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
)
|
|
assert CacheTestResolver(SimpleNamespace(cache=subclassed)).resolve().kind == "python_callback"
|
|
|
|
|
|
def test_s3_facade_rejects_configurations_that_require_python(s3_stub: S3Stub) -> None:
|
|
unverified: Final = Cache(
|
|
type=LiteLLMCacheType.S3,
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url="https://s3.example.test",
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
s3_verify=False,
|
|
)
|
|
with pytest.raises(_native.RustBridgeDeclined, match="requires Python"):
|
|
native_runtime(unverified)
|
|
proxied: Final = Cache(
|
|
type=LiteLLMCacheType.S3,
|
|
s3_bucket_name="cache-bucket",
|
|
s3_region_name="us-east-1",
|
|
s3_endpoint_url=s3_stub.url,
|
|
s3_aws_access_key_id="key",
|
|
s3_aws_secret_access_key="secret",
|
|
s3_path="team",
|
|
s3_config=botocore.config.Config(proxies={"https": "http://proxy.test"}),
|
|
)
|
|
with pytest.raises(_native.RustBridgeDeclined, match="requires Python"):
|
|
native_runtime(proxied)
|