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 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-11-05 04:33:12 +09:00
parent 312c5f8988
commit 3d1f361904
8 changed files with 35 additions and 82 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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."

View file

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

View file

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

View file

@ -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) == []