From ac55d1366d529bbb1442ddc0efed3794a5812052 Mon Sep 17 00:00:00 2001 From: Maximilian Rink Date: Fri, 16 Oct 2026 12:53:37 +0200 Subject: [PATCH] fix: satisfy cache type-discipline gate with read-only boundaries --- litellm/caching/caching.py | 12 +++++++++--- .../context_caching/vertex_ai_context_caching.py | 6 +++--- litellm/proxy/hooks/batch_redis_get.py | 9 ++++++--- .../proxy/spend_tracking/spend_tracking_utils.py | 13 ++++++++----- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index 23d35486eae..4cb9cbd5c26 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -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"] diff --git a/litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py b/litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py index ac79024aa54..49e01628a35 100644 --- a/litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py +++ b/litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py @@ -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, diff --git a/litellm/proxy/hooks/batch_redis_get.py b/litellm/proxy/hooks/batch_redis_get.py index 2a695278ee9..5e7e87dced7 100644 --- a/litellm/proxy/hooks/batch_redis_get.py +++ b/litellm/proxy/hooks/batch_redis_get.py @@ -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 ":" - 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: diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 7a9314b901a..5bcddac3249 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -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