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__)]