mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-06 22:22:23 +00:00
test(caching): cover semantic cache isolation guards
This commit is contained in:
@@ -59,7 +59,7 @@ def _normalize_operation_ids(paths: Dict[str, Dict]) -> None:
|
||||
break
|
||||
|
||||
|
||||
def generate_snapshot() -> Dict[str, Dict]:
|
||||
def generate_snapshot() -> Dict[str, Dict]: # pragma: no cover
|
||||
import importlib
|
||||
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
@@ -100,7 +100,7 @@ def generate_snapshot() -> Dict[str, Dict]:
|
||||
return fragments
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
fragments = generate_snapshot()
|
||||
SNAPSHOT_FILE.write_text(json.dumps(fragments, indent=2, sort_keys=True) + "\n")
|
||||
sys.stdout.write(f"wrote {len(fragments)} feature fragments to {SNAPSHOT_FILE}\n")
|
||||
|
||||
@@ -204,6 +204,39 @@ def test_qdrant_semantic_cache_rejects_unscoped_cache_hit():
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_qdrant_semantic_cache_payload_index_failure_is_non_blocking():
|
||||
from litellm.caching.qdrant_semantic_cache import QdrantSemanticCache
|
||||
|
||||
qdrant_cache = QdrantSemanticCache.__new__(QdrantSemanticCache)
|
||||
qdrant_cache.qdrant_api_base = "http://test.qdrant.local"
|
||||
qdrant_cache.collection_name = "test_collection"
|
||||
qdrant_cache.headers = {"Content-Type": "application/json"}
|
||||
qdrant_cache.sync_client = MagicMock()
|
||||
response = MagicMock()
|
||||
response.status_code = 400
|
||||
response.text = "bad index"
|
||||
qdrant_cache.sync_client.put.return_value = response
|
||||
|
||||
qdrant_cache._ensure_cache_key_payload_index()
|
||||
|
||||
qdrant_cache.sync_client.put.assert_called_once()
|
||||
|
||||
|
||||
def test_qdrant_semantic_cache_payload_index_exception_is_non_blocking():
|
||||
from litellm.caching.qdrant_semantic_cache import QdrantSemanticCache
|
||||
|
||||
qdrant_cache = QdrantSemanticCache.__new__(QdrantSemanticCache)
|
||||
qdrant_cache.qdrant_api_base = "http://test.qdrant.local"
|
||||
qdrant_cache.collection_name = "test_collection"
|
||||
qdrant_cache.headers = {"Content-Type": "application/json"}
|
||||
qdrant_cache.sync_client = MagicMock()
|
||||
qdrant_cache.sync_client.put.side_effect = Exception("boom")
|
||||
|
||||
qdrant_cache._ensure_cache_key_payload_index()
|
||||
|
||||
qdrant_cache.sync_client.put.assert_called_once()
|
||||
|
||||
|
||||
def test_qdrant_semantic_cache_get_cache_miss():
|
||||
"""
|
||||
Test QDRANT semantic cache get method when there's a cache miss.
|
||||
|
||||
@@ -225,6 +225,52 @@ def test_redis_semantic_cache_uses_isolated_index_for_old_schema(monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
def test_redis_semantic_cache_reraises_unexpected_index_error():
|
||||
from litellm.caching.redis_semantic_cache import RedisSemanticCache
|
||||
|
||||
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
|
||||
redis_semantic_cache.distance_threshold = 0.2
|
||||
semantic_cache_mock = MagicMock(side_effect=ValueError("connection failed"))
|
||||
|
||||
with pytest.raises(ValueError, match="connection failed"):
|
||||
redis_semantic_cache._init_semantic_cache(
|
||||
semantic_cache_cls=semantic_cache_mock,
|
||||
index_name="existing_index",
|
||||
redis_url="redis://localhost:6379",
|
||||
cache_vectorizer=MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
def test_redis_semantic_cache_matches_bytes_cache_key():
|
||||
from litellm.caching.redis_semantic_cache import RedisSemanticCache
|
||||
|
||||
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
|
||||
|
||||
assert redis_semantic_cache._cache_hit_matches_key(
|
||||
cache_hit={RedisSemanticCache.CACHE_KEY_FIELD_NAME: b"test_key"},
|
||||
key="test_key",
|
||||
)
|
||||
|
||||
|
||||
def test_redis_semantic_cache_builds_filter_expression(monkeypatch):
|
||||
class FakeTag:
|
||||
def __init__(self, field_name):
|
||||
self.field_name = field_name
|
||||
|
||||
def __eq__(self, value):
|
||||
return (self.field_name, value)
|
||||
|
||||
with patch.dict("sys.modules", {"redisvl.query.filter": MagicMock(Tag=FakeTag)}):
|
||||
from litellm.caching.redis_semantic_cache import RedisSemanticCache
|
||||
|
||||
redis_semantic_cache = RedisSemanticCache.__new__(RedisSemanticCache)
|
||||
|
||||
assert redis_semantic_cache._get_cache_key_filter_expression("test_key") == (
|
||||
RedisSemanticCache.CACHE_KEY_FIELD_NAME,
|
||||
"test_key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_semantic_cache_async_get_cache(monkeypatch):
|
||||
# Mock the redisvl import
|
||||
@@ -289,6 +335,54 @@ async def test_redis_semantic_cache_async_get_cache(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_semantic_cache_async_get_cache_rejects_unscoped_hit(monkeypatch):
|
||||
semantic_cache_mock = MagicMock()
|
||||
custom_vectorizer_mock = MagicMock()
|
||||
|
||||
with patch.dict(
|
||||
"sys.modules",
|
||||
{
|
||||
"redisvl.extensions.llmcache": MagicMock(SemanticCache=semantic_cache_mock),
|
||||
"redisvl.utils.vectorize": MagicMock(
|
||||
CustomTextVectorizer=custom_vectorizer_mock
|
||||
),
|
||||
},
|
||||
):
|
||||
from litellm.caching.redis_semantic_cache import RedisSemanticCache
|
||||
|
||||
monkeypatch.setenv("REDIS_HOST", "localhost")
|
||||
monkeypatch.setenv("REDIS_PORT", "6379")
|
||||
monkeypatch.setenv("REDIS_PASSWORD", "test_password")
|
||||
|
||||
redis_semantic_cache = RedisSemanticCache(similarity_threshold=0.8)
|
||||
redis_semantic_cache.llmcache.acheck = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"prompt": "What is the capital of France?",
|
||||
"response": '{"content": "Paris"}',
|
||||
"vector_distance": 0.1,
|
||||
}
|
||||
]
|
||||
)
|
||||
redis_semantic_cache._get_async_embedding = AsyncMock(
|
||||
return_value=[0.1, 0.2, 0.3]
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
redis_semantic_cache,
|
||||
"_get_cache_key_filter_expression",
|
||||
return_value="cache-key-filter",
|
||||
):
|
||||
result = await redis_semantic_cache.async_get_cache(
|
||||
key="test_key",
|
||||
messages=[{"content": "What is the capital of France?"}],
|
||||
metadata={},
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_semantic_cache_async_set_cache_stores_cache_key_filter(
|
||||
monkeypatch,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from litellm.proxy._lazy_openapi_snapshot import _normalize_operation_ids
|
||||
|
||||
|
||||
def test_normalize_operation_ids_uses_each_http_method():
|
||||
paths = {
|
||||
"/proxy/{endpoint}": {
|
||||
"delete": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"get": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"post": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"put": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
}
|
||||
}
|
||||
|
||||
_normalize_operation_ids(paths)
|
||||
|
||||
operations = paths["/proxy/{endpoint}"]
|
||||
assert operations["delete"]["operationId"] == "proxy_route_proxy__endpoint__delete"
|
||||
assert operations["get"]["operationId"] == "proxy_route_proxy__endpoint__get"
|
||||
assert operations["post"]["operationId"] == "proxy_route_proxy__endpoint__post"
|
||||
assert operations["put"]["operationId"] == "proxy_route_proxy__endpoint__put"
|
||||
|
||||
|
||||
def test_normalize_operation_ids_preserves_custom_ids():
|
||||
paths = {
|
||||
"/proxy/{endpoint}": {
|
||||
"get": {"operationId": "custom_operation"},
|
||||
"post": {"operationId": "custom_operation"},
|
||||
}
|
||||
}
|
||||
|
||||
_normalize_operation_ids(paths)
|
||||
|
||||
operations = paths["/proxy/{endpoint}"]
|
||||
assert operations["get"]["operationId"] == "custom_operation"
|
||||
assert operations["post"]["operationId"] == "custom_operation"
|
||||
@@ -0,0 +1,76 @@
|
||||
from litellm.proxy import _lazy_openapi_snapshot as snapshot_module
|
||||
|
||||
|
||||
def test_load_snapshot_returns_none_when_missing(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", tmp_path / "missing.json")
|
||||
|
||||
assert snapshot_module.load_snapshot() is None
|
||||
|
||||
|
||||
def test_load_snapshot_reads_json(monkeypatch, tmp_path):
|
||||
snapshot_file = tmp_path / "snapshot.json"
|
||||
snapshot_file.write_text('{"mcp": {"paths": {}}}')
|
||||
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", snapshot_file)
|
||||
|
||||
assert snapshot_module.load_snapshot() == {"mcp": {"paths": {}}}
|
||||
|
||||
|
||||
def test_load_snapshot_returns_none_for_invalid_json(monkeypatch, tmp_path):
|
||||
snapshot_file = tmp_path / "snapshot.json"
|
||||
snapshot_file.write_text("{")
|
||||
monkeypatch.setattr(snapshot_module, "SNAPSHOT_FILE", snapshot_file)
|
||||
|
||||
assert snapshot_module.load_snapshot() is None
|
||||
|
||||
|
||||
def test_normalize_operation_ids_uses_each_http_method():
|
||||
paths = {
|
||||
"/proxy/{endpoint}": {
|
||||
"delete": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"get": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"post": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
"put": {"operationId": "proxy_route_proxy__endpoint__put"},
|
||||
}
|
||||
}
|
||||
|
||||
snapshot_module._normalize_operation_ids(paths)
|
||||
|
||||
operations = paths["/proxy/{endpoint}"]
|
||||
assert operations["delete"]["operationId"] == "proxy_route_proxy__endpoint__delete"
|
||||
assert operations["get"]["operationId"] == "proxy_route_proxy__endpoint__get"
|
||||
assert operations["post"]["operationId"] == "proxy_route_proxy__endpoint__post"
|
||||
assert operations["put"]["operationId"] == "proxy_route_proxy__endpoint__put"
|
||||
|
||||
|
||||
def test_normalize_operation_ids_preserves_custom_ids():
|
||||
paths = {
|
||||
"/proxy/{endpoint}": {
|
||||
"get": {"operationId": "custom_operation"},
|
||||
"post": {"operationId": "custom_operation"},
|
||||
}
|
||||
}
|
||||
|
||||
snapshot_module._normalize_operation_ids(paths)
|
||||
|
||||
operations = paths["/proxy/{endpoint}"]
|
||||
assert operations["get"]["operationId"] == "custom_operation"
|
||||
assert operations["post"]["operationId"] == "custom_operation"
|
||||
|
||||
|
||||
def test_normalize_operation_ids_skips_invalid_entries():
|
||||
paths = {
|
||||
"/not-a-dict": "skip",
|
||||
"/no-http-methods": {"parameters": []},
|
||||
"/invalid-operation": {
|
||||
"get": ["skip"],
|
||||
"post": {"operationId": 123},
|
||||
"parameters": [],
|
||||
},
|
||||
}
|
||||
|
||||
snapshot_module._normalize_operation_ids(paths)
|
||||
|
||||
assert paths["/not-a-dict"] == "skip"
|
||||
assert paths["/no-http-methods"] == {"parameters": []}
|
||||
assert paths["/invalid-operation"]["get"] == ["skip"]
|
||||
assert paths["/invalid-operation"]["post"] == {"operationId": 123}
|
||||
Reference in New Issue
Block a user