diff --git a/litellm/rust_bridge/configuration.py b/litellm/rust_bridge/configuration.py index 2cea27e7b09..791e13a51d0 100644 --- a/litellm/rust_bridge/configuration.py +++ b/litellm/rust_bridge/configuration.py @@ -4,10 +4,11 @@ import os from enum import Enum, auto from typing import Final +from pydantic import TypeAdapter, ValidationError from typing_extensions import assert_never -_TRUE_ENV_VALUES: Final = frozenset({"1", "true", "yes", "on"}) _GLOBAL_ENV_NAME: Final = "LITELLM_RUST" +_ENV_BOOL: Final = TypeAdapter(bool) class Rollout(Enum): @@ -34,7 +35,10 @@ _CONFIGURATION: Final = _RustConfiguration() def _parse_env_bool(value: str | None) -> bool | None: if value is None: return None - return value.strip().lower() in _TRUE_ENV_VALUES + try: + return _ENV_BOOL.validate_python(value.strip()) + except ValidationError: + return None def decide( @@ -50,10 +54,10 @@ def decide( return Decision.RUST_REQUIRED case Rollout.RUST_OPT_IN | Rollout.RUST_OPT_OUT: switch: Final = ( - process_override - if process_override is not None - else environment_override + environment_override if environment_override is not None + else process_override + if process_override is not None else rollout is Rollout.RUST_OPT_OUT ) return Decision.RUST_WITH_FALLBACK if switch else Decision.PYTHON @@ -80,6 +84,7 @@ def reset_rust_configuration() -> None: def rust(enabled: bool | None) -> None: """Set the process override for optional Rust paths. - ``PYTHON_ONLY`` and ``RUST_REQUIRED`` routes in the catalog ignore this switch. + ``PYTHON_ONLY`` and ``RUST_REQUIRED`` routes in the catalog ignore this switch, + and an explicit ``LITELLM_RUST`` environment value wins over it. """ _CONFIGURATION.override = enabled diff --git a/tests/test_litellm/rust_bridge/test_configuration.py b/tests/test_litellm/rust_bridge/test_configuration.py index 4fa5b6d834d..38fdfd0f476 100644 --- a/tests/test_litellm/rust_bridge/test_configuration.py +++ b/tests/test_litellm/rust_bridge/test_configuration.py @@ -33,12 +33,14 @@ Decision: Final = configuration.Decision (Rollout.RUST_REQUIRED, False, False, Decision.RUST_REQUIRED), (Rollout.RUST_OPT_IN, None, None, Decision.PYTHON), (Rollout.RUST_OPT_IN, None, True, Decision.RUST_WITH_FALLBACK), - (Rollout.RUST_OPT_IN, True, False, Decision.RUST_WITH_FALLBACK), - (Rollout.RUST_OPT_IN, False, True, Decision.PYTHON), + (Rollout.RUST_OPT_IN, True, None, Decision.RUST_WITH_FALLBACK), + (Rollout.RUST_OPT_IN, True, False, Decision.PYTHON), + (Rollout.RUST_OPT_IN, False, True, Decision.RUST_WITH_FALLBACK), (Rollout.RUST_OPT_OUT, None, None, Decision.RUST_WITH_FALLBACK), (Rollout.RUST_OPT_OUT, None, False, Decision.PYTHON), - (Rollout.RUST_OPT_OUT, False, True, Decision.PYTHON), - (Rollout.RUST_OPT_OUT, True, False, Decision.RUST_WITH_FALLBACK), + (Rollout.RUST_OPT_OUT, False, None, Decision.PYTHON), + (Rollout.RUST_OPT_OUT, False, True, Decision.RUST_WITH_FALLBACK), + (Rollout.RUST_OPT_OUT, True, False, Decision.PYTHON), ), ) def test_decide_precedence( @@ -68,50 +70,53 @@ def test_opt_out_route_configuration( expected: Final = ( Decision.RUST_WITH_FALLBACK - if process is True or (process is None and environment not in frozenset({"0", "off"})) + if environment == "1" or (environment is None and process is not False) else Decision.PYTHON ) assert configuration.decision(Rollout.RUST_OPT_OUT) is expected -def test_process_override_wins_over_environment(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("LITELLM_RUST", "0") +@pytest.mark.parametrize( + ("environment", "process", "expected"), + ( + *((value, True, False) for value in ("0", "false", "False", "no", "off", "f", "n", " 0 ")), + *((value, False, True) for value in ("1", "true", "TRUE", "yes", "on", "t", "y", " 1 ")), + ), +) +def test_environment_wins_over_process_override( + monkeypatch: pytest.MonkeyPatch, environment: str, process: bool, expected: bool +) -> None: + monkeypatch.setenv("LITELLM_RUST", environment) + configuration.rust(process) + + assert configuration.rust_enabled() is expected + + +def test_process_override_applies_when_environment_is_unset() -> None: configuration.rust(True) assert configuration.rust_enabled() is True -def test_global_environment_accepts_explicit_false(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("LITELLM_RUST", "off") - - assert configuration.rust_enabled() is False - - @pytest.mark.parametrize("value", ("", " ", "sometimes", "2")) -def test_invalid_environment_value_disables_rust(monkeypatch: pytest.MonkeyPatch, value: str) -> None: +def test_invalid_environment_value_is_ignored(monkeypatch: pytest.MonkeyPatch, value: str) -> None: monkeypatch.setenv("LITELLM_RUST", value) assert configuration.rust_enabled() is False - - -def test_process_override_and_reset_apply_to_existing_threads(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("LITELLM_RUST", "1") - - with ThreadPoolExecutor(max_workers=1) as executor: - assert executor.submit(configuration.rust_enabled).result() is True - configuration.rust(False) - assert executor.submit(configuration.rust_enabled).result() is False - configuration.reset_rust_configuration() - assert executor.submit(configuration.rust_enabled).result() is True - - -def test_explicit_override_precedes_invalid_environment(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("LITELLM_RUST", "sometimes") - + assert configuration.decision(Rollout.RUST_OPT_OUT) is Decision.RUST_WITH_FALLBACK configuration.rust(True) assert configuration.rust_enabled() is True +def test_process_override_and_reset_apply_to_existing_threads() -> None: + with ThreadPoolExecutor(max_workers=1) as executor: + assert executor.submit(configuration.rust_enabled).result() is False + configuration.rust(True) + assert executor.submit(configuration.rust_enabled).result() is True + configuration.reset_rust_configuration() + assert executor.submit(configuration.rust_enabled).result() is False + + @pytest.mark.parametrize(("value", "expected"), (("1", "True"), ("0", "False"))) def test_environment_controls_startup(value: str, expected: str) -> None: environment: Final = {**os.environ, "LITELLM_RUST": value} diff --git a/tests/test_litellm/rust_bridge/test_ocr_lifecycle.py b/tests/test_litellm/rust_bridge/test_ocr_lifecycle.py index c9c469168ce..c61c5d79855 100644 --- a/tests/test_litellm/rust_bridge/test_ocr_lifecycle.py +++ b/tests/test_litellm/rust_bridge/test_ocr_lifecycle.py @@ -143,7 +143,7 @@ def test_public_missing_required_argument_error_does_not_depend_on_native_select @pytest.mark.asyncio @pytest.mark.parametrize("asynchronous", [False, True]) -@pytest.mark.parametrize("enabled", [False, None]) +@pytest.mark.parametrize("enabled", [False, True, None]) async def test_environment_opt_out_never_loads_native( monkeypatch: pytest.MonkeyPatch, asynchronous: bool, enabled: bool | None ) -> None: diff --git a/tests/test_litellm/rust_bridge/test_runtime.py b/tests/test_litellm/rust_bridge/test_runtime.py index ee3950f8455..1f5f75bb809 100644 --- a/tests/test_litellm/rust_bridge/test_runtime.py +++ b/tests/test_litellm/rust_bridge/test_runtime.py @@ -114,6 +114,32 @@ def test_environment_switch_enables_opt_in_route(monkeypatch: pytest.MonkeyPatch assert calls.calls == (RUST,) +@pytest.mark.parametrize( + ("rollout", "environment", "switch", "expected"), + ( + (Rollout.RUST_OPT_IN, "0", True, (PYTHON,)), + (Rollout.RUST_OPT_OUT, "0", True, (PYTHON,)), + (Rollout.RUST_OPT_IN, "1", False, (RUST,)), + (Rollout.RUST_OPT_OUT, "1", False, (RUST,)), + (Rollout.RUST_REQUIRED, "0", False, (RUST,)), + (Rollout.PYTHON_ONLY, "1", True, (PYTHON,)), + ), +) +def test_environment_switch_wins_over_process_switch( + monkeypatch: pytest.MonkeyPatch, + rollout: Rollout, + environment: str, + switch: bool, + expected: tuple[str, ...], +) -> None: + calls: Final = recorder() + monkeypatch.setenv("LITELLM_RUST", environment) + configuration.rust(switch) + + assert run(rollout, calls) == expected[-1] + assert calls.calls == expected + + def test_context_outside_rule_stays_on_python() -> None: calls: Final = recorder() configuration.rust(True)