Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/common_utils/cache_pydantic_utils.py: 61%
30 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2DualCache presents a single API for reads and writes, but the two backends behave
3differently: the in-memory layer can store arbitrary Python objects (including live
4``BaseModel`` instances), while Redis persists strings and therefore needs JSON-safe
5payloads (``json.dumps`` on the Redis side).
7Call sites therefore see cache ``value`` / ``cached`` as effectively ``Any``: the same
8key may deserialize to a model on one process (memory hit) or to a ``dict`` after a
9Redis round-trip. ``CacheCodec`` centralizes encode/decode at that boundary:
10``CacheCodec.serialize`` before ``set``, ``CacheCodec.deserialize`` after ``get``
11when you need a typed ``BaseModel``.
13``dataclasses`` are not supported: only ``dict`` and Pydantic ``BaseModel`` inputs
14are encoded; pass a Pydantic model or convert with e.g. ``dataclasses.asdict`` first.
15"""
17from __future__ import annotations
19from typing import Any, TypeVar
21from pydantic import BaseModel, ValidationError
23from litellm._logging import verbose_proxy_logger
25T = TypeVar("T", bound=BaseModel)
28class CacheCodec:
29 """
30 Encode/decode Pydantic models for DualCache (memory vs Redis safe payloads).
32 Dataclasses are not supported yet (only ``dict`` and ``BaseModel``).
34 Use ``serialize`` with ``model_type`` when writing so the same schema is used
35 as on read (``deserialize``). Pass ``model_type`` whenever you know it
36 (validates ``dict`` payloads and normalizes ``BaseModel`` instances).
37 """
39 @staticmethod
40 def serialize(value: object, model_type: type[T] | None = None) -> object:
41 """
42 Encode a value for DualCache / Redis (``json.dumps``-safe).
44 If ``model_type`` is set, the payload is validated with that model, then
45 ``model_dump(mode="json")`` — symmetric with ``deserialize``.
47 If the value is already an instance of ``model_type`` (or a subclass),
48 ``model_validate`` is skipped to avoid an unnecessary Pydantic copy — the
49 value is dumped directly.
51 If ``model_type`` is omitted, any ``BaseModel`` is dumped as above; other
52 values (e.g. plain ``dict``) are returned unchanged.
53 """
54 if model_type is not None:
55 if isinstance(value, model_type): 55 ↛ 58line 55 didn't jump to line 58 because the condition on line 55 was always true
56 # Already the right type: dump directly, skip re-validation copy.
57 return value.model_dump(mode="json")
58 if isinstance(value, (dict, BaseModel)):
59 return model_type.model_validate(value).model_dump(mode="json")
60 return value
61 if isinstance(value, BaseModel): 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 return value.model_dump(mode="json")
63 return value
65 @staticmethod
66 def deserialize(cached: Any, model_type: type[T]) -> T | None:
67 """
68 Decode a cache entry to ``model_type``.
70 - ``None`` → ``None``
71 - Already an instance of ``model_type`` (including subclasses) → returned as-is
72 - ``dict`` → ``model_type.model_validate(...)``; on ``ValidationError``,
73 logs a warning and returns ``None`` (treat as cache miss; avoids serving
74 malformed or schema-drifted entries)
75 - Any other type → ``None`` (caller should treat as cache miss or log)
76 """
77 if cached is None: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 return None
79 if isinstance(cached, model_type): 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 return cached
81 if isinstance(cached, dict): 81 ↛ 91line 81 didn't jump to line 91 because the condition on line 81 was always true
82 try:
83 return model_type.model_validate(cached)
84 except ValidationError as e:
85 verbose_proxy_logger.warning(
86 "CacheCodec.deserialize: validation failed for %s (%s)",
87 model_type.__name__,
88 e,
89 )
90 return None
91 return None