From 0dd95d64abee9d7bedc132c5c611f42de6fefc05 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 5 Nov 2026 03:46:50 +0900 Subject: [PATCH 1/4] refactor(rust-bridge): warn once when a Rust-served call ignores client= native_call_hook now strips client and shared_session before Rust sees the call and raises a DeprecationWarning pointed at the caller's line when client= was set. Bedrock transcription, which skips the hook, warns the same way. OCR and Bedrock transcription used to drop client= without saying so. Co-Authored-By: Claude Opus 5.5 --- litellm/main.py | 3 ++ litellm/rust_bridge/ignored_arguments.py | 43 +++++++++++++++++++ litellm/rust_bridge/public_call.py | 6 ++- tests/test_litellm_rust/ocr/test_dispatch.py | 21 +++++++++ tests/unit/rust_bridge/test_public_call.py | 34 ++++++++++++++- .../test_audio_transcription_rust_bridge.py | 15 +++++++ 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 litellm/rust_bridge/ignored_arguments.py diff --git a/litellm/main.py b/litellm/main.py index 8cb133bf76b..7792b21438b 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8751,7 +8751,10 @@ def transcription( ) elif custom_llm_provider == "bedrock": from litellm.llms.bedrock.audio_transcription import BedrockAudioTranscriptionRustDispatch + from litellm.rust_bridge.ignored_arguments import warn_ignored + if client is not None: + warn_ignored("client") dispatch: Final = BedrockAudioTranscriptionRustDispatch() if atranscription: response = dispatch.async_audio_transcriptions( diff --git a/litellm/rust_bridge/ignored_arguments.py b/litellm/rust_bridge/ignored_arguments.py new file mode 100644 index 00000000000..e9fa6608801 --- /dev/null +++ b/litellm/rust_bridge/ignored_arguments.py @@ -0,0 +1,43 @@ +import inspect +import os +import sys +import traceback +import warnings +from collections.abc import Mapping +from pathlib import Path +from types import MappingProxyType +from typing import Final + +_LITELLM_ROOT: Final = f"{Path(__file__).parents[1]}{os.sep}" + +_DEPRECATED: Final = MappingProxyType( + {"client": "client= is ignored: litellm sends this request from Rust with its own HTTP settings"} +) +_INTERNAL: Final = frozenset({"shared_session"}) +_DROPPED: Final = _INTERNAL | frozenset(_DEPRECATED) + + +def _first_stacklevel_outside_litellm() -> int: + return next( + ( + level + for level, (frame, _) in enumerate(traceback.walk_stack(inspect.currentframe())) + if not frame.f_code.co_filename.startswith(_LITELLM_ROOT) + ), + 1, + ) + + +def warn_ignored(name: str) -> None: + if sys.version_info >= (3, 12): + warnings.warn(_DEPRECATED[name], DeprecationWarning, skip_file_prefixes=(_LITELLM_ROOT,)) + return + warnings.warn(_DEPRECATED[name], DeprecationWarning, stacklevel=_first_stacklevel_outside_litellm()) + + +def without_ignored(kwargs: Mapping[str, object]) -> Mapping[str, object]: + for name in (name for name in _DEPRECATED if kwargs.get(name) is not None): + warn_ignored(name) + 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/public_call.py b/litellm/rust_bridge/public_call.py index 8625ca71bb2..9fe3729ea49 100644 --- a/litellm/rust_bridge/public_call.py +++ b/litellm/rust_bridge/public_call.py @@ -4,10 +4,12 @@ from __future__ import annotations import inspect from collections.abc import Callable, Collection, Mapping, Sequence -from dataclasses import dataclass +from dataclasses import dataclass, replace 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) @@ -114,4 +116,4 @@ def native_call_hook( _args: tuple[object, ...], _kwargs: Mapping[str, object], ) -> NativeResultT: - return hook(call) + return hook(replace(call, kwargs=without_ignored(call.kwargs))) diff --git a/tests/test_litellm_rust/ocr/test_dispatch.py b/tests/test_litellm_rust/ocr/test_dispatch.py index 3206cc606a8..3eac16f7aee 100644 --- a/tests/test_litellm_rust/ocr/test_dispatch.py +++ b/tests/test_litellm_rust/ocr/test_dispatch.py @@ -1,3 +1,4 @@ +import warnings from collections.abc import Mapping from typing import Final, NoReturn @@ -44,6 +45,26 @@ async def test_ocr_does_not_depend_on_chat_cache( assert len(server.requests) == 1 +@pytest.mark.asyncio +@pytest.mark.parametrize("asynchronous", [False, True]) +async def test_ocr_warns_that_a_caller_client_is_ignored(server: RecordingServer, asynchronous: bool) -> None: + arguments: Final = { + "model": OCR_MODEL, + "document": OCR_DOCUMENT, + "api_key": "test-key", + "api_base": server.base_url, + "client": object(), + } + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + response: Final = await litellm.aocr(**arguments) if asynchronous else litellm.ocr(**arguments) + + ignored: Final = [warning for warning in caught if "client=" in str(warning.message)] + assert response.pages[0].markdown == "native OCR response" + assert len(server.requests) == 1 + assert [(warning.category, warning.filename) for warning in ignored] == [(DeprecationWarning, __file__)] + + @pytest.mark.parametrize("failure", ["invalid", "live", "schema"]) def test_native_projection_errors_never_select_python( server: RecordingServer, monkeypatch: pytest.MonkeyPatch, failure: str diff --git a/tests/unit/rust_bridge/test_public_call.py b/tests/unit/rust_bridge/test_public_call.py index 5f50db6ea5a..6997945dd2e 100644 --- a/tests/unit/rust_bridge/test_public_call.py +++ b/tests/unit/rust_bridge/test_public_call.py @@ -1,9 +1,10 @@ +import warnings from collections.abc import Mapping, Sequence from typing import Final import pytest -from litellm.rust_bridge.public_call import AsyncFlag, native_call, signature +from litellm.rust_bridge.public_call import AsyncFlag, native_call, native_call_hook, signature def _messages( @@ -76,3 +77,34 @@ 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}) + + +def _hook_kwargs(supplied: Mapping[str, object]) -> tuple[Mapping[str, object], tuple[warnings.WarningMessage, ...]]: + call: Final = native_call(_SIGNATURE, (128, [], "model"), supplied) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + seen: Final = native_call_hook(lambda hooked: hooked.kwargs, call, (), {}) + return seen, tuple(warning for warning in caught if issubclass(warning.category, DeprecationWarning)) + + +def test_hook_drops_a_caller_client_and_warns_at_the_callers_line() -> None: + extra_body: Final = {"pages": [0]} + + seen, caught = _hook_kwargs({"client": object(), "pages": [1, 2], "extra_body": extra_body}) + + assert seen == {"pages": [1, 2], "extra_body": extra_body} + assert seen["extra_body"] is extra_body + assert len(caught) == 1 + assert "client=" in str(caught[0].message) + assert caught[0].filename == __file__ + + +@pytest.mark.parametrize( + "supplied", + ({"shared_session": object(), "api_key": "k"}, {"client": None, "api_key": "k"}, {"api_key": "k"}), +) +def test_hook_drops_internal_or_unset_clients_without_warning(supplied: Mapping[str, object]) -> None: + seen, caught = _hook_kwargs(supplied) + + assert seen == {"api_key": "k"} + assert caught == () diff --git a/tests/unit/test_audio_transcription_rust_bridge.py b/tests/unit/test_audio_transcription_rust_bridge.py index cd94fb32af2..c3762330d8c 100644 --- a/tests/unit/test_audio_transcription_rust_bridge.py +++ b/tests/unit/test_audio_transcription_rust_bridge.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from collections.abc import Generator from typing import Final @@ -301,3 +302,17 @@ def test_bedrock_transcription_rejects_params_it_cannot_honor(unsupported: dict[ forwarded: Final = bridge.calls[0].resolved["optional_params"] assert forwarded["language"] == "fr" assert forwarded.keys().isdisjoint(unsupported) + + +def test_bedrock_transcription_warns_that_a_caller_client_is_ignored() -> None: + bridge: Final = SyncBridge() + NATIVE_TRANSCRIPTION.override(bridge) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + response: Final = litellm.transcription(model=MODEL, file=AUDIO_FILE, client=object()) + + ignored: Final = [warning for warning in caught if "client=" in str(warning.message)] + assert response.text == "rust" + assert len(bridge.calls) == 1 + assert [(warning.category, warning.filename) for warning in ignored] == [(DeprecationWarning, __file__)] From 312c5f8988ea70364a441afd2203eef54c81b375 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 5 Nov 2026 04:24:30 +0900 Subject: [PATCH 2/4] refactor(rust-bridge): deprecate client= on every public LLM call client= now warns on every call that goes through the public dispatch (completion, embedding, responses, anthropic messages, ocr) and on transcription for every provider, whether Python still honors it today or Rust already ignores it. native_call_hook keeps stripping client and shared_session before Rust sees the call, without warning again. Co-Authored-By: Claude Opus 5.5 --- litellm/main.py | 7 ++-- litellm/rust_bridge/dispatch.py | 3 ++ litellm/rust_bridge/ignored_arguments.py | 13 +++++-- tests/test_litellm_rust/test_dispatch.py | 39 +++++++++++++++++++ tests/unit/rust_bridge/test_public_call.py | 39 +++++++------------ .../test_audio_transcription_rust_bridge.py | 27 +++++++++++++ 6 files changed, 97 insertions(+), 31 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 7792b21438b..47196936bb6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -158,6 +158,7 @@ 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 ( @@ -8412,6 +8413,7 @@ 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: @@ -8566,6 +8568,8 @@ 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) @@ -8751,10 +8755,7 @@ def transcription( ) elif custom_llm_provider == "bedrock": from litellm.llms.bedrock.audio_transcription import BedrockAudioTranscriptionRustDispatch - from litellm.rust_bridge.ignored_arguments import warn_ignored - if client is not None: - warn_ignored("client") dispatch: Final = BedrockAudioTranscriptionRustDispatch() if atranscription: response = dispatch.async_audio_transcriptions( diff --git a/litellm/rust_bridge/dispatch.py b/litellm/rust_bridge/dispatch.py index ca98fe2b5b2..681bd032dcf 100644 --- a/litellm/rust_bridge/dispatch.py +++ b/litellm/rust_bridge/dispatch.py @@ -8,6 +8,7 @@ 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") @@ -58,6 +59,7 @@ 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) @@ -94,6 +96,7 @@ 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/ignored_arguments.py b/litellm/rust_bridge/ignored_arguments.py index e9fa6608801..e30548b3371 100644 --- a/litellm/rust_bridge/ignored_arguments.py +++ b/litellm/rust_bridge/ignored_arguments.py @@ -11,7 +11,9 @@ from typing import Final _LITELLM_ROOT: Final = f"{Path(__file__).parents[1]}{os.sep}" _DEPRECATED: Final = MappingProxyType( - {"client": "client= is ignored: litellm sends this request from Rust with its own HTTP settings"} + { + "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) @@ -28,16 +30,19 @@ def _first_stacklevel_outside_litellm() -> int: ) -def warn_ignored(name: str) -> None: +def warn_deprecated(name: str) -> None: if sys.version_info >= (3, 12): warnings.warn(_DEPRECATED[name], DeprecationWarning, skip_file_prefixes=(_LITELLM_ROOT,)) return warnings.warn(_DEPRECATED[name], DeprecationWarning, stacklevel=_first_stacklevel_outside_litellm()) -def without_ignored(kwargs: Mapping[str, object]) -> Mapping[str, object]: +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_ignored(name) + 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/tests/test_litellm_rust/test_dispatch.py b/tests/test_litellm_rust/test_dispatch.py index 193866409a2..d57ff76c0f0 100644 --- a/tests/test_litellm_rust/test_dispatch.py +++ b/tests/test_litellm_rust/test_dispatch.py @@ -1,3 +1,4 @@ +import warnings from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator, Mapping from dataclasses import dataclass from typing import Final, NoReturn, TypeAlias @@ -329,3 +330,41 @@ 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 6997945dd2e..87231d89980 100644 --- a/tests/unit/rust_bridge/test_public_call.py +++ b/tests/unit/rust_bridge/test_public_call.py @@ -79,32 +79,23 @@ def test_flag_keywords_reject_a_parameter_given_twice() -> None: _FLAG.keywords((128,), {"max_tokens": 64}) -def _hook_kwargs(supplied: Mapping[str, object]) -> tuple[Mapping[str, object], tuple[warnings.WarningMessage, ...]]: - call: Final = native_call(_SIGNATURE, (128, [], "model"), supplied) - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - seen: Final = native_call_hook(lambda hooked: hooked.kwargs, call, (), {}) - return seen, tuple(warning for warning in caught if issubclass(warning.category, DeprecationWarning)) - - -def test_hook_drops_a_caller_client_and_warns_at_the_callers_line() -> None: - extra_body: Final = {"pages": [0]} - - seen, caught = _hook_kwargs({"client": object(), "pages": [1, 2], "extra_body": extra_body}) - - assert seen == {"pages": [1, 2], "extra_body": extra_body} - assert seen["extra_body"] is extra_body - assert len(caught) == 1 - assert "client=" in str(caught[0].message) - assert caught[0].filename == __file__ - @pytest.mark.parametrize( "supplied", - ({"shared_session": object(), "api_key": "k"}, {"client": None, "api_key": "k"}, {"api_key": "k"}), + ( + {"client": object(), "api_key": "k"}, + {"shared_session": object(), "api_key": "k"}, + {"client": None, "api_key": "k"}, + {"api_key": "k"}, + ), ) -def test_hook_drops_internal_or_unset_clients_without_warning(supplied: Mapping[str, object]) -> None: - seen, caught = _hook_kwargs(supplied) +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}) - assert seen == {"api_key": "k"} - assert caught == () + 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_audio_transcription_rust_bridge.py b/tests/unit/test_audio_transcription_rust_bridge.py index c3762330d8c..1ad0cf4afab 100644 --- a/tests/unit/test_audio_transcription_rust_bridge.py +++ b/tests/unit/test_audio_transcription_rust_bridge.py @@ -4,6 +4,7 @@ import warnings from collections.abc import Generator from typing import Final +import openai import pytest import litellm @@ -316,3 +317,29 @@ def test_bedrock_transcription_warns_that_a_caller_client_is_ignored() -> None: assert response.text == "rust" assert len(bridge.calls) == 1 assert [(warning.category, warning.filename) for warning in ignored] == [(DeprecationWarning, __file__)] + + +@pytest.mark.asyncio +async def test_bedrock_atranscription_warns_once_that_a_caller_client_is_deprecated() -> None: + bridge: Final = AsyncBridge() + NATIVE_ATRANSCRIPTION.override(bridge) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + response: Final = await litellm.atranscription(model=MODEL, file=AUDIO_FILE, client=object()) + + ignored: Final = [warning for warning in caught if "client=" in str(warning.message)] + assert response.text == "async rust" + assert [(warning.category, warning.filename) for warning in ignored] == [(DeprecationWarning, __file__)] + + +def test_python_served_transcription_warns_that_a_caller_client_is_deprecated() -> None: + client: Final = openai.OpenAI(api_key="sk-test", base_url="http://127.0.0.1:9", max_retries=0) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + with pytest.raises(openai.APIConnectionError, match="Connection error"): + litellm.transcription(model="openai/whisper-1", file=AUDIO_FILE, client=client) + + ignored: Final = [warning for warning in caught if "client=" in str(warning.message)] + assert [(warning.category, warning.filename) for warning in ignored] == [(DeprecationWarning, __file__)] From 3d1f36190486cebb089f7e31dfa18e86e1600f26 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 5 Nov 2026 04:33:12 +0900 Subject: [PATCH 3/4] 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) == [] From 46215f8863f3a2dba1b22d7723963af2ebf856d2 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 5 Nov 2026 04:46:54 +0900 Subject: [PATCH 4/4] fix(rust-bridge): say which network settings Rust honors in the client= warning Rust does read litellm.ssl_verify, litellm.ssl_certificate and the proxy and SSL environment variables, so the warning points callers at those instead of saying Rust uses its own HTTP settings. Co-Authored-By: Claude Opus 5.5 --- litellm/rust_bridge/deprecated_arguments.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/litellm/rust_bridge/deprecated_arguments.py b/litellm/rust_bridge/deprecated_arguments.py index 2495f2d1ecb..89073cd28ef 100644 --- a/litellm/rust_bridge/deprecated_arguments.py +++ b/litellm/rust_bridge/deprecated_arguments.py @@ -12,7 +12,11 @@ _LITELLM_ROOT: Final = f"{Path(__file__).parents[1]}{os.sep}" _DEPRECATED: Final = MappingProxyType( { - "client": "client= is deprecated: litellm is moving requests to Rust, which ignores it and uses its own HTTP settings" + "client": ( + "client= is deprecated: litellm is moving requests to Rust, which cannot use a caller's HTTP client. " + "Set network options with litellm.ssl_verify, litellm.ssl_certificate, or proxy and SSL environment " + "variables instead" + ) } )