From b8282f095bc961e29ea0331aeea1297cf6dadb93 Mon Sep 17 00:00:00 2001 From: Dixing Xu Date: Fri, 14 Aug 2026 01:27:17 +0000 Subject: [PATCH] perf(caching): skip cache bookkeeping when caching is disabled Return before copying kwargs or inspecting signatures when caching is disabled, and cache bounded positional parameter-name lookups for enabled-cache calls. Preserve positional-argument behavior with focused regression coverage, including empty and surplus positional arguments. Co-Authored-By: Aiden --- litellm/caching/caching_handler.py | 28 ++++++-------- .../caching/test_caching_handler.py | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index 7526dfd4e4c..eb73daaa13d 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -19,6 +19,7 @@ import datetime import inspect import time from collections.abc import AsyncGenerator, AsyncIterator, Callable, Generator, Mapping +from functools import lru_cache from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar from pydantic import BaseModel @@ -1033,6 +1034,9 @@ class LLMCachingHandler: Sync internal method to add the result to the cache """ + if litellm.cache is None: + return + new_kwargs: Final = kwargs.copy() new_kwargs.update( convert_args_to_kwargs( @@ -1040,8 +1044,6 @@ class LLMCachingHandler: args, ) ) - if litellm.cache is None: - return if self._should_store_result_in_cache(original_function=self.original_function, kwargs=new_kwargs): litellm.cache.add_cache(result, **new_kwargs) @@ -1215,22 +1217,16 @@ class LLMCachingHandler: ) +@lru_cache(maxsize=1024) +def _positional_param_names(original_function: Callable) -> tuple[str, ...]: + return tuple(inspect.signature(original_function).parameters) + + def convert_args_to_kwargs( original_function: Callable, args: tuple[object, ...] | None = None, ) -> dict[str, object]: - # Get the signature of the original function - signature: Final = inspect.signature(original_function) + if not args: + return {} - # Get parameter names in the order they appear in the original function - param_names: Final = list(signature.parameters.keys()) - - # Create a mapping of positional arguments to parameter names - args_to_kwargs: Final = {} - if args: - for index, arg in enumerate(args): - if index < len(param_names): - param_name = param_names[index] - args_to_kwargs[param_name] = arg - - return args_to_kwargs + return dict(zip(_positional_param_names(original_function), args)) diff --git a/tests/test_litellm/caching/test_caching_handler.py b/tests/test_litellm/caching/test_caching_handler.py index 38019fc0fee..7ffb429e867 100644 --- a/tests/test_litellm/caching/test_caching_handler.py +++ b/tests/test_litellm/caching/test_caching_handler.py @@ -622,3 +622,40 @@ def test_request_kwargs_does_not_retain_logging_obj(): assert "litellm_logging_obj" not in handler.request_kwargs assert handler.request_kwargs["messages"] == kwargs["messages"] assert handler.request_kwargs["model"] == "gpt-4o" + + +def test_convert_args_to_kwargs_maps_positional_args_on_decorated_functions(): + """ + convert_args_to_kwargs must resolve positional arguments against the + signature litellm's public entry points actually expose, not against the + `client` decorator's `(*args, **kwargs)` wrapper. + + litellm.completion is decorated, so wrapper.__code__.co_argcount is 0 and + reading __code__ directly yields no parameter names at all. Those names are + what turns a positional call into cache-key material, so losing them makes + distinct requests collide on one key instead of raising. + """ + import litellm + from litellm.caching.caching_handler import convert_args_to_kwargs + + messages = [{"role": "user", "content": "hello"}] + + mapped = convert_args_to_kwargs(litellm.completion, ("gpt-4o", messages)) + + assert mapped["model"] == "gpt-4o" + assert mapped["messages"] == messages + + assert convert_args_to_kwargs(litellm.completion, ()) == {} + assert convert_args_to_kwargs(litellm.completion, None) == {} + + +def test_convert_args_to_kwargs_truncates_extra_positional_args(): + """More positional args than parameters must not raise or invent names.""" + from litellm.caching.caching_handler import convert_args_to_kwargs + + def sample(model, messages, temperature=None): + return None + + mapped = convert_args_to_kwargs(sample, ("gpt-4o", [], 0.5, "extra", "extra2")) + + assert mapped == {"model": "gpt-4o", "messages": [], "temperature": 0.5}