fix(speech): keep the provider key first but skip local-inference placeholders

Trying the provider key last let OPENAI_API_KEY or AZURE_API_KEY go to a
hosted_vllm, deepinfra or azure_ai endpoint whose own key was configured,
which leaks the global credential and still fails the reported setup. The
provider key goes first again, matching completion() and transcription(),
and the fake-api-key / dummy-key placeholders get_llm_provider returns for
local-inference providers are skipped there, so a real OPENAI_API_KEY still
wins over them
This commit is contained in:
mateo-berri 2026-10-20 22:08:26 -07:00
parent feef600f64
commit d30be01aca
3 changed files with 28 additions and 8 deletions

View file

@ -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"

View file

@ -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(

View file

@ -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
):