mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
* 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>
183 lines
5.7 KiB
Python
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,
|
|
)
|