litellm/tests/unit/test_audio_transcription_rust_bridge.py
yuneng-jiang f6882246d4
test: move tests/test_litellm root and small trees into tests/unit (#43186)
* ci: run the unit_selection.sh shard files on every event instead of only fork pull requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci: rename fork-flag to unit-flag now that it applies on every event

* test: move tests/test_litellm root and small trees into tests/unit

Pure renames, no content changes. Follow-up commits in this PR fix
references, merge the three files that already existed in tests/unit,
keep live-provider tests in tests/test_litellm and wire CI.

* test: carry tests/test_litellm conftest isolation into tests/unit

Callback lists, routing fallbacks, cached HTTP clients, logger state, AWS,
proxy-URL and keychain env, and session-end client cleanup now reset for
unit tests too. The environment isolation owns its MonkeyPatch so a test's
own monkeypatch is undone before the model-cost teardown runs.

* test: merge, split and prune the moved root and small-tree tests

Merge batches/test_batch_utils.py and the chat_completions and messages
dispatch tests into the files that already existed in tests/unit. Keep
the live Gemini interactions tests, the async image-fetch format test and
the OpenAI embedding scorer test in tests/test_litellm since they need
real network or keys. Put test_router.py under tests/unit/test_router so
the existing package no longer shadows it. Delete eight tests the audit
found superseded by stronger ones kept in this move.

* ci: run the moved root and small-tree tests under their legacy flags

Add the misc and responses-caching-types flags to unit_selection.sh and
CircleCI, extend enterprise-routing and mcp-integration, and point the
legacy GHA shards, Makefile, redis-compat workflow, merge smoke manifest
and change classifier at the new paths.

* test: make the new tests/unit directories packages

tests/unit/test_package_layout.py requires every directory to carry an
__init__.py, and without one the moved and retained
test_litellm_responses_bridge.py modules collide on import.

* test: scope the unit socket block to tests/unit in shared sessions

The GHA shards collect the legacy test-path and the unit selection in one
pytest session. The unit conftest's loopback-only block leaked into legacy
modules that reach the network at import. The legacy conftest now lifts the
restriction at collect and setup time, and the unit conftest re-applies it
when collecting its own modules.

* test: give the shard-script tests their own GITHUB_OUTPUT

They only passed where the runner set it. The CircleCI unit job's env
allowlist drops it, so the script's redirect failed there.

* test: point the router and module-deletion checks at tests/unit

router_code_coverage and code_qa_check_tests only searched tests/test_litellm,
so the moved router tests no longer counted. The two silent-experiment tests
the audit deleted were the only direct callers of those methods; they are
replaced with tests that assert the forwarded shadow request and the
recursion guard.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 11:30:43 -07:00

183 lines
5.7 KiB
Python

from __future__ import annotations
from collections.abc import Generator
from types import SimpleNamespace
from typing import Final
import pytest
import litellm
from litellm.llms.bedrock.audio_transcription import BedrockAudioTranscriptionRustDispatch
from litellm.rust_bridge import bindings, configuration
from litellm.rust_bridge.transcription.native import NATIVE_ATRANSCRIPTION, NATIVE_TRANSCRIPTION
MODEL: Final = "bedrock/mistral.voxtral-mini-3b-2507"
AUDIO_FILE: Final = ("audio.wav", b"audio", "audio/wav")
class RustBridgeDeclined(Exception):
pass
class RustUpstreamError(Exception):
pass
@pytest.fixture(autouse=True)
def isolated_bridge(monkeypatch: pytest.MonkeyPatch) -> Generator[None]:
native: Final = SimpleNamespace(RustBridgeDeclined=RustBridgeDeclined, RustUpstreamError=RustUpstreamError)
monkeypatch.setattr(bindings, "get_native_bridge", lambda: native)
monkeypatch.delenv("LITELLM_RUST", raising=False)
configuration.reset_rust_configuration()
yield
NATIVE_TRANSCRIPTION.reset()
NATIVE_ATRANSCRIPTION.reset()
configuration.reset_rust_configuration()
class SyncBridge:
def __init__(self, effect: BaseException | None = None) -> None:
self._effect: Final = effect
self.calls: tuple[dict[str, object], ...] = ()
def __call__(
self,
model: str,
audio: dict[str, object],
api_key: str | None,
api_base: str | None,
custom_llm_provider: str | None,
extra_headers: dict[str, object] | None,
optional_params: dict[str, object],
timeout_seconds: float | None,
) -> dict[str, object]:
self.calls = (
*self.calls,
{"model": model, "audio": audio, "provider": custom_llm_provider, "timeout": timeout_seconds},
)
if self._effect is not None:
raise self._effect
return {"text": "rust"}
class AsyncBridge:
def __init__(self) -> None:
self.calls: tuple[str, ...] = ()
async def __call__(
self,
model: str,
audio: dict[str, object],
api_key: str | None,
api_base: str | None,
custom_llm_provider: str | None,
extra_headers: dict[str, object] | None,
optional_params: dict[str, object],
timeout_seconds: float | None,
) -> dict[str, object]:
self.calls = (*self.calls, model)
return {"text": "async rust"}
def dispatch_sync() -> litellm.TranscriptionResponse:
return BedrockAudioTranscriptionRustDispatch().audio_transcriptions(
model=MODEL,
audio_file=AUDIO_FILE,
api_key=None,
api_base=None,
custom_llm_provider="bedrock",
extra_headers=None,
optional_params={"temperature": 0},
timeout=5,
)
def test_dispatch_marshals_audio_into_rust_call() -> None:
bridge: Final = SyncBridge()
NATIVE_TRANSCRIPTION.override(bridge)
response: Final = dispatch_sync()
assert response.text == "rust"
assert bridge.calls == (
{
"model": MODEL,
"audio": {"data": "YXVkaW8=", "format": "wav", "filename": "audio.wav"},
"provider": "bedrock",
"timeout": 5.0,
},
)
@pytest.mark.parametrize("disable", ("process", "environment"))
def test_bedrock_transcription_ignores_optional_rust_switches(disable: str, monkeypatch: pytest.MonkeyPatch) -> None:
if disable == "process":
litellm.rust(False)
else:
monkeypatch.setenv("LITELLM_RUST", "0")
bridge: Final = SyncBridge()
NATIVE_TRANSCRIPTION.override(bridge)
assert dispatch_sync().text == "rust"
assert len(bridge.calls) == 1
def test_missing_native_binding_raises_without_python_fallback() -> None:
NATIVE_TRANSCRIPTION.override(None)
with pytest.raises(RuntimeError, match="bridge is unavailable"):
dispatch_sync()
def test_admission_decline_raises_for_required_route() -> None:
NATIVE_TRANSCRIPTION.override(SyncBridge(RustBridgeDeclined("unsupported format")))
with pytest.raises(RuntimeError, match="declined the request: unsupported format"):
dispatch_sync()
def test_upstream_error_maps_to_api_error() -> None:
NATIVE_TRANSCRIPTION.override(SyncBridge(RustUpstreamError(503, "bedrock down")))
with pytest.raises(litellm.APIError, match="bedrock down") as raised:
dispatch_sync()
assert raised.value.status_code == 503
def test_bedrock_transcription_dispatches_to_rust_from_sdk_entrypoint() -> None:
bridge: Final = SyncBridge()
NATIVE_TRANSCRIPTION.override(bridge)
response: Final = litellm.transcription(model=MODEL, file=AUDIO_FILE)
assert isinstance(response, litellm.TranscriptionResponse)
assert response.text == "rust"
assert bridge.calls[0]["model"] == MODEL.removeprefix("bedrock/")
@pytest.mark.asyncio
async def test_bedrock_atranscription_dispatches_to_rust_from_sdk_entrypoint() -> None:
bridge: Final = AsyncBridge()
NATIVE_ATRANSCRIPTION.override(bridge)
response: Final = await litellm.atranscription(model=MODEL, file=AUDIO_FILE)
assert response.text == "async rust"
assert bridge.calls == (MODEL.removeprefix("bedrock/"),)
@pytest.mark.asyncio
async def test_async_missing_native_binding_raises_without_python_fallback() -> None:
NATIVE_ATRANSCRIPTION.override(None)
with pytest.raises(RuntimeError, match="bridge is unavailable"):
await BedrockAudioTranscriptionRustDispatch().async_audio_transcriptions(
model=MODEL,
audio_file=AUDIO_FILE,
api_key=None,
api_base=None,
custom_llm_provider="bedrock",
extra_headers=None,
optional_params={},
timeout=None,
)