From 77bf16b84784fc4557f328b47c0a7cd60e6927f9 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 5 Sep 2026 19:19:23 -0700 Subject: [PATCH] test(native): cover shared dispatch contracts --- tests/test_litellm/ocr/test_rust_bridge.py | 21 ++++- .../test_litellm/rust_bridge/test_runtime.py | 79 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/ocr/test_rust_bridge.py b/tests/test_litellm/ocr/test_rust_bridge.py index a42dfd6f0fa..b936b0e75c2 100644 --- a/tests/test_litellm/ocr/test_rust_bridge.py +++ b/tests/test_litellm/ocr/test_rust_bridge.py @@ -11,8 +11,8 @@ import pytest import litellm from litellm.llms.base_llm.ocr.transformation import OCRResponse -from litellm.rust_bridge.timeouts import timeout_to_seconds from litellm.rust_bridge import configuration +from litellm.rust_bridge.timeouts import timeout_to_seconds # `litellm/__init__.py` does `from .ocr.main import *`, which binds the `ocr` # function onto `litellm.ocr` and shadows the submodule, so import the modules @@ -864,6 +864,25 @@ async def test_ocr_fallback_skips_native_preparation( fallback.assert_called_once() +@pytest.mark.asyncio +async def test_aocr_rejects_empty_python_fallback_response(monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, object] = {} + + def fake_exception_type(**kwargs: object) -> CapturedException: + captured.update(kwargs) + return CapturedException("wrapped") + + monkeypatch.setattr(ocr_main.litellm, "exception_type", fake_exception_type) + monkeypatch.setattr(ocr_main.base_llm_http_handler, "ocr", AsyncMock(return_value=None)) + + with pytest.raises(CapturedException, match="wrapped"): + await litellm.aocr(model=MODEL, document=DOCUMENT, api_key="sk-test") + + original: Final = captured["original_exception"] + assert isinstance(original, ValueError) + assert str(original) == "Got an unexpected None response from the OCR API: None" + + def test_ocr_provider_configs_expose_api_key_env_vars(): from litellm.llms.azure_ai.ocr.document_intelligence.transformation import ( AzureDocumentIntelligenceOCRConfig, diff --git a/tests/test_litellm/rust_bridge/test_runtime.py b/tests/test_litellm/rust_bridge/test_runtime.py index c00d3fc1950..1ba13fa99fd 100644 --- a/tests/test_litellm/rust_bridge/test_runtime.py +++ b/tests/test_litellm/rust_bridge/test_runtime.py @@ -292,6 +292,19 @@ def test_require_explains_why_rust_did_not_handle_request( ) +@pytest.mark.asyncio +async def test_arequire_explains_unavailable_native_binding() -> None: + endpoint: Final = runtime.EndpointBinding(route="messages", load=lambda: None, enabled=enabled) + + with pytest.raises(RuntimeError, match=r"^native messages endpoint is unavailable$"): + await endpoint.arequire( + prepare=lambda: pytest.fail("must not prepare"), + call=lambda _binding, _request: pytest.fail("must not invoke"), + adapt=str, + error_context=context(), + ) + + @pytest.mark.parametrize( ("state", "expected", "expected_events"), ( @@ -358,6 +371,68 @@ def test_native_endpoint_applies_partial_overrides_and_reset(monkeypatch: pytest assert endpoint.asynchronous.load() is native_async +def test_direct_endpoint_binding_rejects_native_state_controls() -> None: + endpoint: Final = runtime.EndpointBinding(route="test", load=object, enabled=enabled) + + with pytest.raises(RuntimeError, match="only native Rust bridges support binding overrides"): + endpoint.override(object()) + with pytest.raises(RuntimeError, match="only native Rust bridges support binding resets"): + endpoint.reset() + + +@pytest.mark.parametrize( + ("enabled_state", "reason", "expected"), + ( + pytest.param(False, None, runtime.PythonFallbackReason.NATIVE_DISABLED, id="disabled"), + pytest.param(True, "unsupported model", runtime.PythonFallbackReason.NATIVE_DECLINED, id="declined"), + pytest.param(True, None, None, id="accepted"), + ), +) +def test_assess_reports_binding_eligibility( + enabled_state: bool, + reason: str | None, + expected: runtime.PythonFallbackReason | None, +) -> None: + binding: Final = object() + checked: list[object] = [] + endpoint: Final = runtime.EndpointBinding(route="test", load=lambda: binding, enabled=lambda: enabled_state) + + result: Final = endpoint.assess(check=lambda value: checked.append(value) or reason) + + assert (result.reason if result is not None else None) is expected + assert (result.detail if result is not None else None) == reason + assert checked == ([binding] if enabled_state else []) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("asynchronous", (False, True)) +async def test_dispatch_require_returns_adapted_native_success_without_exception_metadata( + monkeypatch: pytest.MonkeyPatch, + asynchronous: bool, +) -> None: + monkeypatch.setattr(bindings, "get_native_bridge", lambda: None) + endpoint: Final = runtime.EndpointDispatch( + sync=runtime.EndpointBinding(route="test", load=object, enabled=enabled), + asynchronous=runtime.EndpointBinding(route="test", load=object, enabled=enabled), + ) + + async def acall(_binding: object, request: int) -> int: + return request * 2 + + result: Final = ( + await endpoint.arequire(prepare=lambda: 3, call=acall, adapt=str, error_context=context()) + if asynchronous + else endpoint.require( + prepare=lambda: 3, + call=lambda _binding, request: request * 2, + adapt=str, + error_context=context(), + ) + ) + + assert result == "6" + + @pytest.mark.asyncio @pytest.mark.parametrize("asynchronous", (False, True)) async def test_response_adaptation_failure_never_authorizes_fallback(asynchronous: bool) -> None: @@ -479,7 +554,9 @@ async def test_preflight_runs_after_binding_selection_before_preparation( assert events == ( ["load", "preflight", "prepare", "native"] if available and accepted - else ["load", "preflight", "python"] if available else ["load", "python"] + else ["load", "preflight", "python"] + if available + else ["load", "python"] )