diff --git a/litellm/rust_bridge/deprecated_arguments.py b/litellm/rust_bridge/deprecated_arguments.py new file mode 100644 index 00000000000..89073cd28ef --- /dev/null +++ b/litellm/rust_bridge/deprecated_arguments.py @@ -0,0 +1,44 @@ +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 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" + ) + } +) + + +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_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 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) diff --git a/litellm/utils.py b/litellm/utils.py index 53b02ebddac..7fc8568898d 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/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/test_audio_transcription_rust_bridge.py b/tests/unit/test_audio_transcription_rust_bridge.py index cd94fb32af2..1ad0cf4afab 100644 --- a/tests/unit/test_audio_transcription_rust_bridge.py +++ b/tests/unit/test_audio_transcription_rust_bridge.py @@ -1,8 +1,10 @@ from __future__ import annotations +import warnings from collections.abc import Generator from typing import Final +import openai import pytest import litellm @@ -301,3 +303,43 @@ 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__)] + + +@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__)] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 924c3738638..b4e6556a78e 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 @@ -11626,3 +11627,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) == []