diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 145d9dd2304..7109e6942d1 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -2476,21 +2476,12 @@ class BaseLLMHTTPHandler: extra_headers=headers, timeout=timeout, ) - except Exception as rust_error: # noqa: BLE001 # only explicit pre-dispatch declines permit fallback - from litellm.rust_bridge.bindings import native_exception_types, upstream_error_details - - exceptions: Final = native_exception_types() - if exceptions is not None and isinstance(rust_error, exceptions[0]): - return None - if exceptions is not None and isinstance(rust_error, exceptions[1]): - status, message = upstream_error_details(rust_error) - raise litellm.APIError( - status_code=status, - message=message, - llm_provider=custom_llm_provider, - model=model, - ) from rust_error - raise + except Exception as rust_error: # noqa: BLE001 # rollout-safety fallback: any Rust bridge failure must fall back to the Python path + verbose_logger.debug( + "Rust Anthropic messages bridge raised %s; falling back to Python path", + type(rust_error).__name__, + ) + return None if rust_response is None: return None diff --git a/litellm/rust_bridge/runtime.py b/litellm/rust_bridge/runtime.py index 9ee6cf10717..d411673439f 100644 --- a/litellm/rust_bridge/runtime.py +++ b/litellm/rust_bridge/runtime.py @@ -6,7 +6,7 @@ from enum import Enum from typing import Final, Generic, NoReturn, TypeAlias, TypeVar from litellm.exceptions import APIError -from litellm.rust_bridge.bindings import native_exception_types, upstream_error_details +from litellm.rust_bridge.bindings import native_exception_types NativeT = TypeVar("NativeT") ResultT = TypeVar("ResultT") @@ -137,9 +137,13 @@ def _required_reason(result: RustDeclined | RustUnavailable) -> str: def _raise_upstream(error: BaseException, context: BridgeErrorContext) -> NoReturn: - status, message = upstream_error_details(error) + args: Final[tuple[object, ...]] = error.args + status_value: Final = args[0] if args else 0 + message_value: Final = args[1] if len(args) > 1 else str(error) + status: Final = status_value if isinstance(status_value, int) else 0 + message: Final = message_value if isinstance(message_value, str) else str(message_value) raise APIError( - status_code=status, + status_code=status or 500, message=f"litellm rust {context.route}: {message}", llm_provider=context.provider, model=context.model, diff --git a/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py b/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py index 0b68dda027e..a30474245c6 100644 --- a/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py +++ b/tests/test_litellm/anthropic_interface/test_rust_bridge_messages.py @@ -238,66 +238,17 @@ async def test_gate_invokes_rust_and_marks_response_header(): @pytest.mark.asyncio -async def test_gate_propagates_unclassified_bridge_failure(): +async def test_gate_falls_back_to_python_when_bridge_raises(): bridge = RaisingAsyncMessages() litellm.rust(True) rust_messages.set_rust_messages(amessages=bridge) - with pytest.raises(RuntimeError, match="upstream request failed"): - await _gate() + response = await _gate() + + assert response is None assert bridge.calls == 1 -@pytest.mark.asyncio -@pytest.mark.parametrize("status", [0, 400, 429, 500]) -async def test_gate_never_falls_back_after_possible_dispatch(monkeypatch, status): - from types import SimpleNamespace - from litellm.rust_bridge import bindings - - class Declined(Exception): - pass - - class Upstream(Exception): - pass - - error = Upstream(status, "provider failure") - - async def bridge(**kwargs): - raise error - - monkeypatch.setattr( - bindings, "get_native_bridge", lambda: SimpleNamespace(RustBridgeDeclined=Declined, RustUpstreamError=Upstream) - ) - litellm.rust(True) - rust_messages.set_rust_messages(amessages=bridge) - with pytest.raises(litellm.APIError) as caught: - await _gate() - assert caught.value.status_code == (status or 500) - assert caught.value.__cause__ is error - - -@pytest.mark.asyncio -async def test_gate_falls_back_only_for_explicit_decline(monkeypatch): - from types import SimpleNamespace - from litellm.rust_bridge import bindings - - class Declined(Exception): - pass - - class Upstream(Exception): - pass - - async def bridge(**kwargs): - raise Declined("unsupported before dispatch") - - monkeypatch.setattr( - bindings, "get_native_bridge", lambda: SimpleNamespace(RustBridgeDeclined=Declined, RustUpstreamError=Upstream) - ) - litellm.rust(True) - rust_messages.set_rust_messages(amessages=bridge) - assert await _gate() is None - - @pytest.mark.asyncio async def test_gate_skips_rust_when_flag_absent(): bridge = ExplodingAsyncMessages() diff --git a/tests/test_litellm/rust_bridge/test_chat_completions.py b/tests/test_litellm/rust_bridge/test_chat_completions.py index 36ec234ba19..b2fd2e6dcc0 100644 --- a/tests/test_litellm/rust_bridge/test_chat_completions.py +++ b/tests/test_litellm/rust_bridge/test_chat_completions.py @@ -55,9 +55,6 @@ class _FakeNative: def _fake_native_bridge(monkeypatch): """Expose the bridge's exception classes without the compiled extension.""" monkeypatch.setattr(bridge, "get_native_bridge", lambda: _FakeNative()) - from litellm.rust_bridge import bindings - - monkeypatch.setattr(bindings, "get_native_bridge", lambda: _FakeNative()) def _hide_native_bridge(monkeypatch):