diff --git a/litellm/constants.py b/litellm/constants.py index 6a503463a8c..238d60e9dde 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -342,6 +342,7 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET: Final = get_env_int( # Provider-specific API base URLs XAI_API_BASE: Final = "https://api.x.ai/v1" PLACEHOLDER_API_KEY: Final = "fake-api-key" +PLACEHOLDER_API_KEYS: Final = frozenset({PLACEHOLDER_API_KEY, "dummy-key"}) OPEN_SANDBOX_API_BASE_ENV_VAR: Final = "OPEN_SANDBOX_API_BASE" OPEN_SANDBOX_API_KEY_ENV_VAR: Final = "OPEN_SANDBOX_API_KEY" OPEN_SANDBOX_DEFAULT_TEMPLATE: Final = "opensandbox/code-interpreter:v1.1.0" diff --git a/litellm/main.py b/litellm/main.py index 864906c1439..f42c2b104b7 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -66,6 +66,7 @@ from litellm.constants import ( DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT, NADIR_DEFAULT_API_BASE, OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS, + PLACEHOLDER_API_KEYS, ) from litellm.exceptions import LiteLLMUnknownProvider from litellm.integrations.custom_logger import CustomLogger @@ -8588,6 +8589,7 @@ def speech( model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider( model=model, custom_llm_provider=custom_llm_provider, api_base=api_base or base_url ) + provider_api_key: Final = None if dynamic_api_key in PLACEHOLDER_API_KEYS else dynamic_api_key kwargs.pop("tags", []) optional_params = {} @@ -8677,10 +8679,11 @@ def speech( # set API KEY api_key = ( api_key + or provider_api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there or litellm.api_key or litellm.openai_key or get_secret("OPENAI_API_KEY") - or dynamic_api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there + or dynamic_api_key ) organization = ( @@ -8763,11 +8766,11 @@ def speech( api_key = ( api_key + or provider_api_key or litellm.api_key or litellm.azure_key or get_secret("AZURE_OPENAI_API_KEY") or get_secret("AZURE_API_KEY") - or dynamic_api_key ) azure_ad_token: Final[str | None] = optional_params.get("extra_body", {}).pop( diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 74cb80d348b..72c2ec0678d 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -5011,13 +5011,13 @@ def _set_or_clear_env(monkeypatch: pytest.MonkeyPatch, values: dict[str, str | N ("provider_key", "openai_key", "sent_authorization"), [ ("hosted-vllm-scoped-key", None, "Bearer hosted-vllm-scoped-key"), - ("hosted-vllm-scoped-key", "generic-openai-key", "Bearer generic-openai-key"), + ("hosted-vllm-scoped-key", "generic-openai-key", "Bearer hosted-vllm-scoped-key"), (None, "generic-openai-key", "Bearer generic-openai-key"), (None, None, "Bearer fake-api-key"), ], - ids=["provider-key-alone", "generic-key-still-wins", "generic-key-beats-placeholder", "placeholder-when-unset"], + ids=["provider-key-alone", "provider-key-beats-generic", "generic-key-beats-placeholder", "placeholder-when-unset"], ) -def test_speech_openai_compatible_falls_back_to_the_provider_scoped_key( +def test_speech_openai_compatible_sends_the_provider_scoped_key( respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, provider_key: str | None, @@ -5041,10 +5041,10 @@ def test_speech_openai_compatible_falls_back_to_the_provider_scoped_key( @pytest.mark.parametrize( ("generic_azure_key", "sent_api_key"), - [(None, "azure-ai-scoped-key"), ("generic-azure-key", "generic-azure-key")], - ids=["provider-key-alone", "generic-key-still-wins"], + [(None, "azure-ai-scoped-key"), ("generic-azure-key", "azure-ai-scoped-key")], + ids=["provider-key-alone", "provider-key-beats-generic"], ) -def test_speech_azure_falls_back_to_the_provider_scoped_key( +def test_speech_azure_sends_the_provider_scoped_key( respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch, generic_azure_key: str | None, sent_api_key: str ): _clear_global_tts_keys(monkeypatch) @@ -5069,6 +5069,22 @@ def test_speech_azure_falls_back_to_the_provider_scoped_key( assert route.calls.last.request.headers["api-key"] == sent_api_key +def test_speech_docker_model_runner_placeholder_does_not_beat_a_real_key( + respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch +): + _clear_global_tts_keys(monkeypatch) + _set_or_clear_env(monkeypatch, {"DOCKER_MODEL_RUNNER_API_KEY": None, "OPENAI_API_KEY": "generic-openai-key"}) + route: Final = respx_mock.post("https://dmr.internal/v1/audio/speech").mock( + return_value=httpx.Response(200, content=TTS_AUDIO, headers={"content-type": "audio/mpeg"}) + ) + + litellm.speech( + model="docker_model_runner/tts-1", input="which key goes out", voice="alloy", api_base="https://dmr.internal/v1" + ) + + assert route.calls.last.request.headers["authorization"] == "Bearer generic-openai-key" + + def test_speech_keeps_an_explicitly_passed_api_key_over_the_provider_scoped_one( respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch ):