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 <aiden@weco.ai>
This commit is contained in:
Dixing Xu 2026-08-14 01:27:17 +00:00
parent d7e4b1bdd0
commit b8282f095b
2 changed files with 49 additions and 16 deletions

View file

@ -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))

View file

@ -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}