fix(speech): try the provider-scoped key last so working global keys keep priority
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

Putting get_llm_provider's key ahead of litellm.api_key, OPENAI_API_KEY and
AZURE_API_KEY broke TTS setups that already worked through a global key, and
let local-inference placeholders like fake-api-key outrank a real
OPENAI_API_KEY. The provider-scoped key is now the last fallback in both
speech key chains, so only callers that had no key at all change behavior
This commit is contained in:
mateo-berri 2026-10-20 21:29:45 -07:00
parent 1cb02d9ccc
commit feef600f64
2 changed files with 41 additions and 11 deletions

View file

@ -8677,10 +8677,10 @@ def speech(
# set API KEY
api_key = (
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 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
)
organization = (
@ -8763,11 +8763,11 @@ def speech(
api_key = (
api_key
or dynamic_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

@ -4999,11 +4999,33 @@ def _clear_global_tts_keys(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(litellm, attribute, None)
def test_speech_openai_compatible_sends_the_provider_scoped_key(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
def _set_or_clear_env(monkeypatch: pytest.MonkeyPatch, values: dict[str, str | None]) -> None:
for name, value in values.items():
if value is None:
monkeypatch.delenv(name, raising=False)
else:
monkeypatch.setenv(name, value)
@pytest.mark.parametrize(
("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"),
(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"],
)
def test_speech_openai_compatible_falls_back_to_the_provider_scoped_key(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
provider_key: str | None,
openai_key: str | None,
sent_authorization: str,
):
_clear_global_tts_keys(monkeypatch)
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "hosted-vllm-scoped-key")
_set_or_clear_env(monkeypatch, {"HOSTED_VLLM_API_KEY": provider_key, "OPENAI_API_KEY": openai_key})
route: Final = respx_mock.post("https://vllm.internal/v1/audio/speech").mock(
return_value=httpx.Response(200, content=TTS_AUDIO, headers={"content-type": "audio/mpeg"})
)
@ -5014,14 +5036,22 @@ def test_speech_openai_compatible_sends_the_provider_scoped_key(
assert response.content == TTS_AUDIO
assert route.call_count == 1
assert route.calls.last.request.headers["authorization"] == "Bearer hosted-vllm-scoped-key"
assert route.calls.last.request.headers["authorization"] == sent_authorization
def test_speech_azure_sends_the_provider_scoped_key(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):
@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"],
)
def test_speech_azure_falls_back_to_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)
monkeypatch.setenv("AZURE_AI_API_KEY", "azure-ai-scoped-key")
monkeypatch.setenv("AZURE_OPENAI_API_KEY", "legacy-azure-openai-key")
monkeypatch.setenv("AZURE_API_KEY", "legacy-azure-key")
_set_or_clear_env(
monkeypatch,
{"AZURE_AI_API_KEY": "azure-ai-scoped-key", "AZURE_OPENAI_API_KEY": None, "AZURE_API_KEY": generic_azure_key},
)
route: Final = respx_mock.post(
url__regex=r"https://foundry\.internal/openai/deployments/tts-1/audio/speech\?api-version=.+"
).mock(return_value=httpx.Response(200, content=TTS_AUDIO, headers={"content-type": "audio/mpeg"}))
@ -5036,7 +5066,7 @@ def test_speech_azure_sends_the_provider_scoped_key(respx_mock: respx.MockRouter
assert response.content == TTS_AUDIO
assert route.call_count == 1
assert route.calls.last.request.headers["api-key"] == "azure-ai-scoped-key"
assert route.calls.last.request.headers["api-key"] == sent_api_key
def test_speech_keeps_an_explicitly_passed_api_key_over_the_provider_scoped_one(