fix: satisfy cache type-discipline gate with read-only boundaries

This commit is contained in:
Maximilian Rink 2026-10-16 12:53:37 +02:00
parent a48ea6d994
commit ac55d1366d
4 changed files with 26 additions and 14 deletions

View file

@ -854,7 +854,10 @@ class Cache:
else:
cache_key = self.get_cache_key(**kwargs) # rebind-ok: derive caller key
if cache_key is not None:
cache_control_args: Final[DynamicCacheControl] = cast(DynamicCacheControl, kwargs.get("cache", {}))
cache_control_value: Final[object] = kwargs.get("cache", {})
cache_control_args: Final[Mapping[str, object]] = (
cache_control_value if isinstance(cache_control_value, Mapping) else {}
)
max_age = cache_control_args.get("s-maxage") or cache_control_args.get("s-max-age") or float("inf")
cache_lookup_kwargs: Final = self._get_safe_cache_lookup_kwargs(kwargs)
if dynamic_cache_object is not None:
@ -889,7 +892,10 @@ class Cache:
else:
cache_key = self.get_cache_key(**kwargs) # rebind-ok: derive caller key
if cache_key is not None:
cache_control_args: Final[DynamicCacheControl] = cast(DynamicCacheControl, kwargs.get("cache", {}))
cache_control_value: Final[object] = kwargs.get("cache", {})
cache_control_args: Final[Mapping[str, object]] = (
cache_control_value if isinstance(cache_control_value, Mapping) else {}
)
max_age: Final = cache_control_args.get(
"s-max-age", cache_control_args.get("s-maxage", float("inf"))
)
@ -1157,7 +1163,7 @@ class Cache:
def _embedding_cache_entries(
self,
result: object,
kwargs: dict[str, Any],
kwargs: dict[str, Any], # mutable-ok: helper adds each generated cache_key before cache writeback
) -> tuple[list[tuple[str, Any]], dict[str, Any]] | None:
cache_list: list[tuple[str, Any]] = []
inputs = kwargs["input"]

View file

@ -111,7 +111,7 @@ class ContextCachingEndpoints(VertexBase):
Returns
token, url
"""
auth_header: str | dict[str, str | None] | None
auth_header: str | Mapping[str, str | None] | None
if custom_llm_provider == "gemini":
auth_header = {"x-goog-api-key": gemini_api_key}
endpoint = "cachedContents"
@ -436,7 +436,7 @@ class ContextCachingEndpoints(VertexBase):
client = client
## CHECK IF CACHED ALREADY
cache_key_kwargs: Final[dict[str, object]] = {
cache_key_kwargs: Final[Mapping[str, object]] = {
"messages": cached_messages,
"tools": tools,
"tool_choice": tool_choice,
@ -615,7 +615,7 @@ class ContextCachingEndpoints(VertexBase):
client = client
## CHECK IF CACHED ALREADY
cache_key_kwargs: Final[dict[str, object]] = {
cache_key_kwargs: Final[Mapping[str, object]] = {
"messages": cached_messages,
"tools": tools,
"tool_choice": tool_choice,

View file

@ -4,7 +4,8 @@
### [BETA] this is in Beta. And might change.
import traceback
from typing import Final, Literal, cast # noqa: TID251 # typed cache kwargs boundary
from collections.abc import Mapping
from typing import Final, Literal
from fastapi import HTTPException
@ -13,7 +14,6 @@ from litellm._logging import verbose_proxy_logger
from litellm.caching.caching import DualCache, InMemoryCache, RedisCache, response_cache_phase
from litellm.integrations.custom_logger import CustomLogger
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.caching import DynamicCacheControl
class _PROXY_BatchRedisRequests(CustomLogger):
@ -107,7 +107,10 @@ class _PROXY_BatchRedisRequests(CustomLogger):
) # returns "<cache_key_name>:<hash>" - we pass redis_namespace in async_pre_call_hook. Done to avoid rewriting the async_set_cache logic
if cache_key is not None and self.in_memory_cache is not None:
cache_control_args: Final[DynamicCacheControl] = cast(DynamicCacheControl, kwargs.get("cache", {}))
cache_control_value: Final[object] = kwargs.get("cache", {})
cache_control_args: Final[Mapping[str, object]] = (
cache_control_value if isinstance(cache_control_value, Mapping) else {}
)
max_age: Final = cache_control_args.get("s-max-age", cache_control_args.get("s-maxage", float("inf")))
cached_result = self.in_memory_cache.get_cache(cache_key, *args, **kwargs)
if cached_result is None:

View file

@ -333,12 +333,15 @@ def _cost_row_request_id_suffix(
def get_spend_logs_id(
call_type: str, response_obj: dict, kwargs: Mapping[str, object], metadata: Mapping[str, object] = EMPTY_MAPPING
call_type: str,
response_obj: Mapping[str, object],
kwargs: Mapping[str, object],
metadata: Mapping[str, object] = EMPTY_MAPPING,
) -> str | None:
standard_logging_payload = kwargs.get("standard_logging_object")
candidate_ids: Final = (
response_obj.get("id"),
standard_logging_payload.get("id") if isinstance(standard_logging_payload, dict) else None,
standard_logging_payload.get("id") if isinstance(standard_logging_payload, Mapping) else None,
kwargs.get("litellm_call_id"),
)
resolved_id: Final = next(
@ -578,8 +581,8 @@ def get_logging_payload(
response_obj = {"result": str(response_obj)}
# standardize this function to be used across, s3, dynamoDB, langfuse logging
litellm_params_value: Final[object] = kwargs.get("litellm_params", {})
litellm_params: Final[dict[str, object]] = (
cast(dict[str, object], litellm_params_value) if isinstance(litellm_params_value, Mapping) else {}
litellm_params: Final[Mapping[str, object]] = (
litellm_params_value if isinstance(litellm_params_value, Mapping) else EMPTY_MAPPING
)
metadata: Final = get_litellm_metadata_from_kwargs(kwargs)
routing_litellm_params: Final[Mapping[str, object] | None] = (
@ -1631,7 +1634,7 @@ def _convert_to_json_serializable_dict(obj: object, visited: set[int] | None = N
def _convert_mapping_to_json_serializable(
obj: Mapping[str, object],
) -> dict[str, object]:
) -> Mapping[str, object]:
converted: Final = _convert_to_json_serializable_dict(obj)
if isinstance(converted, dict):
return converted