mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
0dd95d64ab
commit
312c5f8988
6 changed files with 97 additions and 31 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__)]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue