mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
* test(rust): group cache tests under cache/ and fold test_ocr.py into ocr/ The two failure cases in test_ocr.py duplicated the upstream-500 and timeout rows of PUBLIC_FAILURES, so only the file-input encoding case moves to ocr/test_requests.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(rust): split the response cache suite into one file per backend test_response_cache.py grew to 2400 lines. Each backend now has its own file, shared fixtures live in cache/conftest.py and shared helpers in support/cache.py. The helpers alias the private native test handles once, dropping the per-call reportPrivateUsage hits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * split tokenizer test * test(core): consolidate route integration tests under tests/ with rstest and wiremock Moves the public-API OCR route tests out of src/ocr/route.rs and document.rs into tests/ocr/, split per provider plus lifecycle, machine, and document tests, merging the duplicated pairs. Messages, audio transcription, and chat completions share one wiremock-based upstream and recording secret source in tests/support, and gain table-driven cases for auth, routing, upstream errors, streaming, and declines. Tests of litellm-llms items move to that crate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(messages): keep the stream relay test independent of the stream head contents The stream head carries no headers on main, so the relay test asserts the open-then-deliver order and the relayed body instead of header hand-off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
621 lines
21 KiB
Python
621 lines
21 KiB
Python
import asyncio
|
|
import contextvars
|
|
import hashlib
|
|
import os
|
|
import struct
|
|
import threading
|
|
import time
|
|
from collections.abc import Generator, Mapping
|
|
from types import SimpleNamespace
|
|
from typing import Final, cast
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import redis
|
|
|
|
from litellm.caching.caching import Cache
|
|
from litellm.caching.valkey_semantic_cache import ValkeySemanticCache
|
|
from litellm.rust_bridge import _native, catalog
|
|
from litellm.rust_bridge.catalog import CacheRule
|
|
from litellm.rust_bridge.configuration import Rollout
|
|
from litellm.rust_bridge.response_cache import ResponseCacheRuntime
|
|
from litellm.types.caching import LiteLLMCacheType
|
|
|
|
pytestmark: Final = pytest.mark.requires_rust_extension
|
|
embedding_context: Final = contextvars.ContextVar("embedding_context")
|
|
|
|
|
|
@pytest.fixture
|
|
def valkey_url() -> str:
|
|
url: Final = os.environ.get("LITELLM_TEST_VALKEY_URL")
|
|
if url is None:
|
|
pytest.skip("LITELLM_TEST_VALKEY_URL is not set")
|
|
return url
|
|
|
|
|
|
@pytest.fixture
|
|
def index_name(valkey_url: str) -> Generator[str]:
|
|
index: Final = f"litellm_test_{uuid4().hex}"
|
|
yield index
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
try:
|
|
client.ft(index).dropindex(delete_documents=True)
|
|
except redis.ResponseError:
|
|
pass
|
|
finally:
|
|
client.close()
|
|
|
|
|
|
def _request(prompt: str = "semantic cache prompt") -> dict[str, object]:
|
|
return {
|
|
"key": {"preset": "key"},
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
}
|
|
|
|
|
|
def _field_request(
|
|
prompt: str,
|
|
metadata: Mapping[str, object],
|
|
*,
|
|
namespace: str | None = None,
|
|
litellm_metadata: Mapping[str, object] | None = None,
|
|
litellm_params: Mapping[str, object] | None = None,
|
|
) -> dict[str, object]:
|
|
request: Final = {
|
|
"key": {
|
|
"fields": [
|
|
{
|
|
"name": "model",
|
|
"value": "gpt-4.1",
|
|
"api_parameter": True,
|
|
"internal_parameter": False,
|
|
},
|
|
{
|
|
"name": "messages",
|
|
"value": prompt,
|
|
"api_parameter": True,
|
|
"internal_parameter": False,
|
|
},
|
|
],
|
|
"namespace": namespace,
|
|
},
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"metadata": dict(metadata),
|
|
}
|
|
if litellm_metadata is not None:
|
|
request["litellm_metadata"] = dict(litellm_metadata)
|
|
if litellm_params is not None:
|
|
request["litellm_params"] = dict(litellm_params)
|
|
return request
|
|
|
|
|
|
def _facade(
|
|
url: str,
|
|
index_name: str,
|
|
embeddings: Mapping[str, list[float]],
|
|
*,
|
|
namespace: str | None = None,
|
|
) -> Cache:
|
|
facade: Final = Cache(
|
|
type=LiteLLMCacheType.VALKEY_SEMANTIC,
|
|
redis_url=url,
|
|
similarity_threshold=0.8,
|
|
valkey_semantic_cache_index_name=index_name,
|
|
namespace=namespace,
|
|
)
|
|
vectors: Final = embeddings
|
|
|
|
def embed(prompt: str, metadata: Mapping[str, object] | None = None) -> list[float]:
|
|
return vectors[prompt]
|
|
|
|
async def async_embedding(prompt: str, metadata: dict[str, object] | None = None) -> list[float]:
|
|
return vectors[prompt]
|
|
|
|
facade.cache._get_embedding = embed
|
|
facade.cache._get_async_embedding = async_embedding
|
|
return facade
|
|
|
|
|
|
def _backend(
|
|
url: str,
|
|
index_name: str,
|
|
embeddings: Mapping[str, list[float]] | None = None,
|
|
) -> ValkeySemanticCache:
|
|
vectors: Final = embeddings or {"semantic cache prompt": [1.0, 0.0]}
|
|
backend: Final = ValkeySemanticCache(
|
|
redis_url=url,
|
|
similarity_threshold=0.8,
|
|
index_name=index_name,
|
|
)
|
|
|
|
def embed(prompt: str, metadata: Mapping[str, object] | None = None) -> list[float]:
|
|
return vectors[prompt]
|
|
|
|
async def async_embedding(prompt: str, metadata: dict[str, object] | None = None) -> list[float]:
|
|
return vectors[prompt]
|
|
|
|
backend._get_embedding = embed
|
|
backend._get_async_embedding = async_embedding
|
|
return backend
|
|
|
|
|
|
def test_python_write_native_read(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
response: Final = {"answer": "python"}
|
|
backend.set_cache("key", response, messages=_request()["messages"])
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
assert binding.lookup(_request()) == response
|
|
|
|
|
|
def test_native_write_python_read(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
response: Final = {"answer": "native"}
|
|
binding.store({**_request(), "ttl_seconds": 2.0}, response)
|
|
cached: Final = cast(Mapping[str, object], backend.get_cache("key", messages=_request()["messages"]))
|
|
assert cached["response"] == response
|
|
|
|
|
|
async def test_async_lookup_and_store(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
request: Final = {**_request(), "ttl_seconds": 2.0}
|
|
await binding.async_store(request, {"answer": "async"})
|
|
assert await binding.async_lookup(request) == {"answer": "async"}
|
|
|
|
|
|
async def test_disabled_cache_controls_skip_async_embedding(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
calls: Final = []
|
|
|
|
async def fail_embedding(prompt: str, metadata: dict[str, object] | None = None) -> list[float]:
|
|
calls.append(prompt)
|
|
raise AssertionError("embedding must not run")
|
|
|
|
backend._get_async_embedding = fail_embedding
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
controls: Final = {
|
|
"supported_call_type": True,
|
|
"configured": True,
|
|
"native_backend": True,
|
|
"default_on": True,
|
|
"caching": True,
|
|
"no_cache": False,
|
|
"no_store": False,
|
|
"use_cache": True,
|
|
}
|
|
no_read_request: Final = {**_request(), "controls": {**controls, "no_cache": True}}
|
|
assert await binding.async_lookup(no_read_request) is None
|
|
no_write_request: Final = {**_request(), "controls": {**controls, "no_store": True}}
|
|
await binding.async_store(no_write_request, {"answer": "blocked"})
|
|
assert calls == []
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
assert list(client.scan_iter(f"{index_name}:*")) == []
|
|
client.close()
|
|
|
|
|
|
async def test_async_embedding_runs_inline_in_caller_task(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
observed: dict[str, object] = {}
|
|
|
|
async def async_embedding(prompt: str, metadata: dict[str, object] | None = None) -> list[float]:
|
|
observed["context"] = embedding_context.get("missing")
|
|
observed["task"] = asyncio.current_task()
|
|
observed["thread"] = threading.get_ident()
|
|
embedding_context.set("embedder")
|
|
return [1.0, 0.0]
|
|
|
|
backend._get_async_embedding = async_embedding
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
request: Final = {**_request(), "ttl_seconds": 2.0}
|
|
caller_task: Final = asyncio.current_task()
|
|
caller_thread: Final = threading.get_ident()
|
|
token: Final = embedding_context.set("caller")
|
|
try:
|
|
await binding.async_store(request, {"answer": "inline"})
|
|
assert observed["context"] == "caller"
|
|
assert observed["task"] is caller_task
|
|
assert observed["thread"] == caller_thread
|
|
assert embedding_context.get() == "embedder"
|
|
assert await binding.async_lookup(request) == {"answer": "inline"}
|
|
finally:
|
|
embedding_context.reset(token)
|
|
|
|
|
|
def test_facade_activation_and_mutation_fallback(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = Cache(
|
|
type=LiteLLMCacheType.VALKEY_SEMANTIC,
|
|
redis_url=valkey_url,
|
|
similarity_threshold=0.8,
|
|
valkey_semantic_cache_index_name=index_name,
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
facade.cache,
|
|
)
|
|
handle._bind_facade(facade)
|
|
resolver: Final = _native._CacheTestResolver(SimpleNamespace(cache=facade))
|
|
assert resolver.resolve().kind == "native"
|
|
facade.cache.similarity_threshold = 0.7
|
|
assert resolver.resolve().kind == "python_callback"
|
|
|
|
|
|
def test_batch_lookup_is_unsupported(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
backend,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
with pytest.raises(NotImplementedError):
|
|
binding.lookup_batch([_request()])
|
|
|
|
|
|
def test_ttl_expiry(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
binding.store({**_request(), "ttl_seconds": 1.0}, {"answer": "expires"})
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
documents: Final = list(client.scan_iter(f"{index_name}:*"))
|
|
assert len(documents) == 1
|
|
assert client.ttl(documents[0]) > 0
|
|
time.sleep(1.5)
|
|
assert binding.lookup(_request()) is None
|
|
|
|
|
|
def test_no_ttl_is_persistent_and_python_reads_native_value(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
response: Final = {"answer": "persistent"}
|
|
binding.store(_request(), response)
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
documents: Final = list(client.scan_iter(f"{index_name}:*"))
|
|
assert len(documents) == 1
|
|
assert client.ttl(documents[0]) == -1
|
|
cached: Final = cast(Mapping[str, object], backend.get_cache("key", messages=_request()["messages"]))
|
|
assert cached["response"] == response
|
|
|
|
|
|
def test_below_threshold_misses_on_native_and_python(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(
|
|
valkey_url,
|
|
index_name,
|
|
{"prompt A": [1.0, 0.0], "prompt B": [0.0, 1.0]},
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
binding.store(_request("prompt A"), {"answer": "A"})
|
|
assert binding.lookup(_request("prompt B")) is None
|
|
assert backend.get_cache("key", messages=_request("prompt B")["messages"]) is None
|
|
|
|
|
|
def test_malformed_entry_is_a_miss_on_native_and_python(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
scope: Final = hashlib.sha256(b"key").hexdigest()
|
|
document: Final = f"{index_name}:{scope}:{uuid4().hex}"
|
|
client.hset(
|
|
document,
|
|
mapping={
|
|
"litellm_cache_key": scope,
|
|
"prompt": "semantic cache prompt",
|
|
"response": "not json",
|
|
"embedding": struct.pack("<2f", 1.0, 0.0),
|
|
},
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
assert binding.lookup(_request()) is None
|
|
assert backend.get_cache("key", messages=_request()["messages"]) is None
|
|
|
|
|
|
def test_mixed_content_parts_match_python_semantic_behavior(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
messages: Final = [{"role": "user", "content": ["raw", {"text": "hello"}]}]
|
|
backend.set_cache("key", {"answer": "mixed"}, messages=messages)
|
|
assert backend.get_cache("key", messages=messages) is None
|
|
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
request: Final = {**_request(), "messages": messages}
|
|
binding.store(request, {"answer": "mixed"})
|
|
assert binding.lookup(request) is None
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
assert list(client.scan_iter(f"{index_name}:*")) == []
|
|
client.close()
|
|
|
|
|
|
async def test_async_store_batch_and_lookup(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(
|
|
valkey_url,
|
|
index_name,
|
|
{"prompt A": [1.0, 0.0], "prompt B": [0.0, 1.0]},
|
|
)
|
|
sync_calls: Final = []
|
|
async_tasks: Final = []
|
|
|
|
def sync_embedding(prompt: str, metadata: Mapping[str, object] | None = None) -> list[float]:
|
|
sync_calls.append(prompt)
|
|
return {"prompt A": [1.0, 0.0], "prompt B": [0.0, 1.0]}[prompt]
|
|
|
|
async def async_embedding(
|
|
prompt: str,
|
|
metadata: dict[str, object] | None = None,
|
|
) -> list[float]:
|
|
async_tasks.append(asyncio.current_task())
|
|
return {"prompt A": [1.0, 0.0], "prompt B": [0.0, 1.0]}[prompt]
|
|
|
|
backend._get_embedding = sync_embedding
|
|
backend._get_async_embedding = async_embedding
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
requests: Final = [_request("prompt A"), _request("prompt B")]
|
|
responses: Final = [{"answer": "A"}, {"answer": "B"}]
|
|
caller_task: Final = asyncio.current_task()
|
|
await binding.async_store_batch(requests, responses)
|
|
assert sync_calls == []
|
|
assert async_tasks
|
|
assert all(task is caller_task for task in async_tasks)
|
|
assert await binding.async_lookup(requests[0]) == responses[0]
|
|
assert await binding.async_lookup(requests[1]) == responses[1]
|
|
|
|
|
|
def test_subclass_backend_falls_back_to_python(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
class Custom(ValkeySemanticCache):
|
|
pass
|
|
|
|
facade: Final = Cache(
|
|
type=LiteLLMCacheType.VALKEY_SEMANTIC,
|
|
redis_url=valkey_url,
|
|
similarity_threshold=0.8,
|
|
valkey_semantic_cache_index_name=index_name,
|
|
)
|
|
facade.cache = Custom(redis_url=valkey_url, similarity_threshold=0.8, index_name=index_name)
|
|
resolver: Final = _native._CacheTestResolver(SimpleNamespace(cache=facade))
|
|
assert resolver.resolve().kind == "python_callback"
|
|
|
|
|
|
def test_field_key_matches_python_semantic_scope(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = _facade(valkey_url, index_name, {"semantic cache prompt": [1.0, 0.0]})
|
|
metadata: Final = {"user_api_key": "k1"}
|
|
expected: Final = facade.get_cache_key(
|
|
model="gpt-4.1",
|
|
messages=[{"role": "user", "content": "semantic cache prompt"}],
|
|
metadata=metadata,
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
facade.cache,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
binding.store(_field_request("semantic cache prompt", metadata), {"answer": "scoped"})
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
documents: Final = list(client.scan_iter(f"{index_name}:*"))
|
|
assert len(documents) == 1
|
|
document_parts: Final = documents[0].decode().split(":")
|
|
assert document_parts[1] == hashlib.sha256(expected.encode()).hexdigest()
|
|
client.close()
|
|
|
|
|
|
def test_field_key_reads_all_python_tenant_metadata_sources(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = _facade(valkey_url, index_name, {"semantic cache prompt": [1.0, 0.0]})
|
|
params_metadata: Final = {"user_api_key_team_id": "team-from-params"}
|
|
expected: Final = facade.get_cache_key(
|
|
model="gpt-4.1",
|
|
messages=[{"role": "user", "content": "semantic cache prompt"}],
|
|
metadata={},
|
|
litellm_params={"metadata": params_metadata},
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
facade.cache,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
binding.store(
|
|
_field_request(
|
|
"semantic cache prompt",
|
|
{},
|
|
litellm_params={"metadata": params_metadata},
|
|
),
|
|
{"answer": "params"},
|
|
)
|
|
client: Final = redis.Redis.from_url(valkey_url)
|
|
documents: Final = list(client.scan_iter(f"{index_name}:*"))
|
|
assert len(documents) == 1
|
|
document_parts: Final = documents[0].decode().split(":")
|
|
assert document_parts[1] == hashlib.sha256(expected.encode()).hexdigest()
|
|
client.close()
|
|
|
|
assert (
|
|
binding.lookup(
|
|
_field_request(
|
|
"semantic cache prompt",
|
|
{},
|
|
litellm_metadata={"user_api_key_team_id": "team-from-litellm"},
|
|
)
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
def test_namespace_isolates_semantic_entries(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = _facade(
|
|
valkey_url,
|
|
index_name,
|
|
{"semantic cache prompt": [1.0, 0.0]},
|
|
namespace="team-a",
|
|
)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
facade.cache,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
team_a: Final = _field_request("semantic cache prompt", {}, namespace="team-a")
|
|
team_b: Final = _field_request("semantic cache prompt", {}, namespace="team-b")
|
|
binding.store(team_a, {"answer": "team-a"})
|
|
assert binding.lookup(team_b) is None
|
|
assert binding.lookup(team_a) == {"answer": "team-a"}
|
|
cached: Final = cast(
|
|
Mapping[str, object],
|
|
facade.get_cache(
|
|
model="gpt-4.1",
|
|
messages=[{"role": "user", "content": "semantic cache prompt"}],
|
|
),
|
|
)
|
|
assert cached == {"answer": "team-a"}
|
|
|
|
|
|
def test_field_key_isolates_tenant_scope(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = _facade(valkey_url, index_name, {"semantic cache prompt": [1.0, 0.0]})
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(
|
|
valkey_url,
|
|
0.8,
|
|
index_name,
|
|
facade.cache,
|
|
)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
binding.store(
|
|
_field_request("semantic cache prompt", {"user_api_key": "k1"}),
|
|
{"answer": "tenant one"},
|
|
)
|
|
assert binding.lookup(_field_request("semantic cache prompt", {"user_api_key": "k2"})) is None
|
|
assert binding.lookup(_field_request("semantic cache prompt", {"user_api_key": "k1"})) == {"answer": "tenant one"}
|
|
|
|
|
|
def test_tls_valkey_facade_falls_back_to_python(
|
|
index_name: str,
|
|
) -> None:
|
|
facade: Final = Cache(
|
|
type=LiteLLMCacheType.VALKEY_SEMANTIC,
|
|
redis_url="rediss://127.0.0.1:6390/0",
|
|
similarity_threshold=0.8,
|
|
valkey_semantic_cache_index_name=index_name,
|
|
)
|
|
resolver: Final = _native._CacheTestResolver(SimpleNamespace(cache=facade))
|
|
assert resolver.resolve().kind == "python_callback"
|
|
|
|
|
|
async def test_ping_maps_unsupported_native_operation_to_not_implemented(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
) -> None:
|
|
backend: Final = _backend(valkey_url, index_name)
|
|
handle: Final = _native._CacheTestHandle.valkey_semantic(valkey_url, 0.8, index_name, backend)
|
|
binding: Final = _native._CacheTestResolver(SimpleNamespace(cache=handle)).resolve()
|
|
with pytest.raises(NotImplementedError):
|
|
await binding.ping()
|
|
|
|
|
|
async def test_rust_required_rule_activates_the_facade_natively(
|
|
valkey_url: str,
|
|
index_name: str,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
catalog,
|
|
"RULES",
|
|
(CacheRule(Rollout.RUST_REQUIRED, backends=frozenset({LiteLLMCacheType.VALKEY_SEMANTIC})),),
|
|
)
|
|
facade: Final = _facade(valkey_url, index_name, {"semantic cache prompt": [1.0, 0.0]})
|
|
runtime: Final = facade._native_cache # pyright: ignore[reportPrivateUsage] # the activation under test has no public accessor
|
|
assert isinstance(runtime, ResponseCacheRuntime)
|
|
assert runtime.kind == "native"
|
|
kwargs: Final = {"model": "gpt-4o", "messages": _request()["messages"]}
|
|
await facade.async_add_cache({"answer": "valkey"}, **kwargs)
|
|
assert await facade.async_get_cache(**kwargs) == {"answer": "valkey"}
|