From 3d1f36190486cebb089f7e31dfa18e86e1600f26 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 5 Nov 2026 04:33:12 +0900 Subject: [PATCH] refactor(rust-bridge): warn about client= once, in function_setup function_setup runs once per top-level SDK call on both paths: @client calls it for Python and the legacy logging adapter calls it for Rust. Warning there replaces the warnings in PublicDispatch and transcription, and native_call_hook no longer strips client, since Rust never reads it. Co-Authored-By: Claude Opus 5.5 --- litellm/main.py | 4 -- ...d_arguments.py => deprecated_arguments.py} | 8 ---- litellm/rust_bridge/dispatch.py | 3 -- litellm/rust_bridge/public_call.py | 6 +-- litellm/utils.py | 2 + tests/test_litellm_rust/test_dispatch.py | 39 ------------------- tests/unit/rust_bridge/test_public_call.py | 25 +----------- tests/unit/test_utils.py | 30 ++++++++++++++ 8 files changed, 35 insertions(+), 82 deletions(-) rename litellm/rust_bridge/{ignored_arguments.py => deprecated_arguments.py} (78%) diff --git a/litellm/main.py b/litellm/main.py index 47196936bb6..8cb133bf76b 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -158,7 +158,6 @@ from litellm.llms.vertex_ai.common_utils import ( ) from litellm.realtime_api.main import realtime_health_check from litellm.responses.mcp.tool_precheck import may_name_litellm_mcp_gateway -from litellm.rust_bridge.ignored_arguments import warn_deprecated, warn_deprecated_in from litellm.secret_managers.main import get_secret_bool, get_secret_str from litellm.text_completions.streaming_iterator import TextCompletionStreamWrapper from litellm.types.completion import ( @@ -8413,7 +8412,6 @@ async def atranscription( loop: Final = asyncio.get_event_loop() model: Final = args[0] if len(args) > 0 else kwargs["model"] ### PASS ARGS TO Image Generation ### - warn_deprecated_in(_REQUEST_KWARGS_ADAPTER.validate_python(kwargs)) kwargs["atranscription"] = True file: Final = kwargs.get("file", None) try: @@ -8568,8 +8566,6 @@ def transcription( client: openai.AsyncOpenAI | openai.OpenAI | openai.AzureOpenAI | openai.AsyncAzureOpenAI | None = kwargs.pop( "client", None ) - if client is not None and not atranscription: - warn_deprecated("client") if litellm_logging_obj: litellm_logging_obj.model_call_details["client"] = str(client) diff --git a/litellm/rust_bridge/ignored_arguments.py b/litellm/rust_bridge/deprecated_arguments.py similarity index 78% rename from litellm/rust_bridge/ignored_arguments.py rename to litellm/rust_bridge/deprecated_arguments.py index e30548b3371..2495f2d1ecb 100644 --- a/litellm/rust_bridge/ignored_arguments.py +++ b/litellm/rust_bridge/deprecated_arguments.py @@ -15,8 +15,6 @@ _DEPRECATED: Final = MappingProxyType( "client": "client= is deprecated: litellm is moving requests to Rust, which ignores it and uses its own HTTP settings" } ) -_INTERNAL: Final = frozenset({"shared_session"}) -_DROPPED: Final = _INTERNAL | frozenset(_DEPRECATED) def _first_stacklevel_outside_litellm() -> int: @@ -40,9 +38,3 @@ def warn_deprecated(name: str) -> None: def warn_deprecated_in(kwargs: Mapping[str, object]) -> None: for name in (name for name in _DEPRECATED if kwargs.get(name) is not None): warn_deprecated(name) - - -def without_ignored(kwargs: Mapping[str, object]) -> Mapping[str, object]: - if _DROPPED.isdisjoint(kwargs): - return kwargs - return MappingProxyType({key: value for key, value in kwargs.items() if key not in _DROPPED}) diff --git a/litellm/rust_bridge/dispatch.py b/litellm/rust_bridge/dispatch.py index 681bd032dcf..ca98fe2b5b2 100644 --- a/litellm/rust_bridge/dispatch.py +++ b/litellm/rust_bridge/dispatch.py @@ -8,7 +8,6 @@ from litellm._logging import verbose_logger from litellm.rust_bridge import catalog, runtime from litellm.rust_bridge.bindings import NativeBinding from litellm.rust_bridge.catalog import Route, RouteContext, Rules -from litellm.rust_bridge.ignored_arguments import warn_deprecated_in RequestT: Final = TypeVar("RequestT") NativeT: Final = TypeVar("NativeT") @@ -59,7 +58,6 @@ class PublicDispatch(Generic[RequestT]): native: Callable[[NativeT, RequestT, tuple[object, ...], Mapping[str, object]], ResultT], rules: Rules | None = None, ) -> ResultT: - warn_deprecated_in(kwargs) selected_rules: Final = catalog.RULES if rules is None else rules if isinstance(python, runtime.NoPythonImplementation): native_request: Final = self._native_request(args, kwargs) @@ -96,7 +94,6 @@ class PublicDispatch(Generic[RequestT]): native: Callable[[NativeT, RequestT, tuple[object, ...], Mapping[str, object]], Awaitable[ResultT]], rules: Rules | None = None, ) -> ResultT: - warn_deprecated_in(kwargs) selected_rules: Final = catalog.RULES if rules is None else rules if isinstance(python, runtime.NoPythonImplementation): native_request: Final = self._native_request(args, kwargs) diff --git a/litellm/rust_bridge/public_call.py b/litellm/rust_bridge/public_call.py index 9fe3729ea49..8625ca71bb2 100644 --- a/litellm/rust_bridge/public_call.py +++ b/litellm/rust_bridge/public_call.py @@ -4,12 +4,10 @@ from __future__ import annotations import inspect from collections.abc import Callable, Collection, Mapping, Sequence -from dataclasses import dataclass, replace +from dataclasses import dataclass from types import MappingProxyType from typing import Final, TypeVar, cast # noqa: TID251 # narrows caller-owned containers without copying them -from litellm.rust_bridge.ignored_arguments import without_ignored - def signature(legacy: Callable[..., object]) -> inspect.Signature: return inspect.signature(legacy) @@ -116,4 +114,4 @@ def native_call_hook( _args: tuple[object, ...], _kwargs: Mapping[str, object], ) -> NativeResultT: - return hook(replace(call, kwargs=without_ignored(call.kwargs))) + return hook(call) diff --git a/litellm/utils.py b/litellm/utils.py index b457da058ad..f6b1ba5a2fe 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -111,6 +111,7 @@ from litellm.litellm_core_utils.tool_fields import ( from litellm.rust_bridge import tokenizer as tokenizer_dispatch from litellm.rust_bridge.catalog import decision from litellm.rust_bridge.configuration import Decision +from litellm.rust_bridge.deprecated_arguments import warn_deprecated_in _CachingHandlerResponse = None _LLMCachingHandler = None @@ -1267,6 +1268,7 @@ def function_setup( **kwargs: Any, # kwargs-ok: forwarded to Logging()/callbacks, varies per call_type ) -> tuple[LiteLLMLoggingObject, dict[str, Any]]: ### NOTICES ### + warn_deprecated_in(kwargs) if litellm.set_verbose is True: verbose_logger.warning( "`litellm.set_verbose` is deprecated. Please set `os.environ['LITELLM_LOG'] = 'DEBUG'` for debug logs." diff --git a/tests/test_litellm_rust/test_dispatch.py b/tests/test_litellm_rust/test_dispatch.py index d57ff76c0f0..193866409a2 100644 --- a/tests/test_litellm_rust/test_dispatch.py +++ b/tests/test_litellm_rust/test_dispatch.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator, Mapping from dataclasses import dataclass from typing import Final, NoReturn, TypeAlias @@ -330,41 +329,3 @@ async def test_dispatch_without_python_never_falls_back( with pytest.raises(NoPythonImplementationError, match=f"ocr has no Python implementation, so .*{reason}"): await dispatch_without_python(ocr_dispatch(request_value), bound, rules, asynchronous=asynchronous) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("asynchronous", [False, True]) -async def test_python_served_call_keeps_the_client_but_warns_it_is_deprecated(asynchronous: bool) -> None: - client: Final = object() - received: Final[list[object]] = [] - - def python(*args: object, **kwargs: object) -> object: # kwargs-ok: records the forwarded call - received.append(kwargs["client"]) - return "python" - - async def apython(*args: object, **kwargs: object) -> object: # kwargs-ok: records the forwarded call - return python(*args, **kwargs) - - dispatch: Final = PublicDispatch( - route=Route.CHAT_COMPLETIONS, - request=lambda args, kwargs: None, - context=lambda _: RouteContext(Route.CHAT_COMPLETIONS), - ) - rules: Final[Rules] = (RouteRule(Route.CHAT_COMPLETIONS, Rollout.PYTHON_ONLY),) - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - result: Final = ( - await dispatch.arun( - ("model",), {"client": client}, python=apython, binding=binding(), native=pytest.fail, rules=rules - ) - if asynchronous - else dispatch.run( - ("model",), {"client": client}, python=python, binding=binding(), native=pytest.fail, rules=rules - ) - ) - - assert result == "python" - assert received == [client] - assert [(warning.category, warning.filename) for warning in caught if "client=" in str(warning.message)] == [ - (DeprecationWarning, __file__) - ] diff --git a/tests/unit/rust_bridge/test_public_call.py b/tests/unit/rust_bridge/test_public_call.py index 87231d89980..5f50db6ea5a 100644 --- a/tests/unit/rust_bridge/test_public_call.py +++ b/tests/unit/rust_bridge/test_public_call.py @@ -1,10 +1,9 @@ -import warnings from collections.abc import Mapping, Sequence from typing import Final import pytest -from litellm.rust_bridge.public_call import AsyncFlag, native_call, native_call_hook, signature +from litellm.rust_bridge.public_call import AsyncFlag, native_call, signature def _messages( @@ -77,25 +76,3 @@ def test_flag_keywords_name_positionals_and_drop_only_the_flag() -> None: def test_flag_keywords_reject_a_parameter_given_twice() -> None: with pytest.raises(TypeError): _FLAG.keywords((128,), {"max_tokens": 64}) - - - -@pytest.mark.parametrize( - "supplied", - ( - {"client": object(), "api_key": "k"}, - {"shared_session": object(), "api_key": "k"}, - {"client": None, "api_key": "k"}, - {"api_key": "k"}, - ), -) -def test_hook_hands_rust_the_call_without_client_or_shared_session(supplied: Mapping[str, object]) -> None: - extra_body: Final = {"pages": [0]} - call: Final = native_call(_SIGNATURE, (128, [], "model"), {**supplied, "extra_body": extra_body}) - - with warnings.catch_warnings(): - warnings.simplefilter("error") - seen: Final = native_call_hook(lambda hooked: hooked.kwargs, call, (), {}) - - assert seen == {"api_key": "k", "extra_body": extra_body} - assert seen["extra_body"] is extra_body diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index e293b2a5be3..8d027e5d5be 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -13,6 +13,7 @@ import queue import threading import time import uuid +import warnings from collections.abc import Callable, Iterator, Mapping from concurrent.futures import Future, ThreadPoolExecutor from datetime import datetime, timedelta, timezone @@ -11624,3 +11625,32 @@ async def test_success_enqueue_is_tracked_without_starting_callbacks_synchronous assert GLOBAL_LOGGING_WORKER.pending_count > 0, "Tracked enqueue must finish before worker draining starts" await GLOBAL_LOGGING_WORKER.flush() assert logging_obj.model_call_details["has_logged_async_success"] is True + + +def _client_deprecations(caught: list[warnings.WarningMessage]) -> list[tuple[type[Warning], str]]: + return [(warning.category, warning.filename) for warning in caught if "client=" in str(warning.message)] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("asynchronous", [False, True]) +async def test_python_served_call_keeps_the_client_but_warns_once_that_it_is_deprecated(asynchronous: bool) -> None: + arguments: Final = { + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + "mock_response": "hi", + "client": object(), + } + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + response: Final = await litellm.acompletion(**arguments) if asynchronous else litellm.completion(**arguments) + + assert response.choices[0].message.content == "hi" + assert _client_deprecations(caught) == [(DeprecationWarning, __file__)] + + +def test_call_without_a_client_does_not_warn() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + litellm.completion(model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}], mock_response="hi") + + assert _client_deprecations(caught) == []