fix(speech): keep Entra ID from env vars and token refresh ahead of the provider-scoped key
Some checks are pending
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run

This commit is contained in:
mateo-berri 2026-10-28 22:38:35 -07:00
parent 19123dd9f4
commit 33d3544ea6
4 changed files with 118 additions and 6 deletions

View file

@ -145,6 +145,34 @@ def cached_azure_ad_token_refresh_provider(scope: str) -> Callable[[], str]:
return _cached_azure_ad_token_refresh_provider(scope)
def _param_or_env_set(
litellm_params: Mapping[str, object], environ: Mapping[str, str], param_key: str, env_var_key: str
) -> bool:
param_value: Final = litellm_params.get(param_key)
return bool(param_value if param_value is not None else environ.get(env_var_key))
def uses_azure_entra_id_auth(
litellm_params: Mapping[str, object], environ: Mapping[str, str], token_refresh_enabled: bool
) -> bool:
client_id_set: Final = _param_or_env_set(litellm_params, environ, "client_id", "AZURE_CLIENT_ID")
return (
litellm_params.get("azure_ad_token_provider") is not None
or bool(litellm_params.get("azure_ad_token"))
or (
client_id_set
and _param_or_env_set(litellm_params, environ, "tenant_id", "AZURE_TENANT_ID")
and _param_or_env_set(litellm_params, environ, "client_secret", "AZURE_CLIENT_SECRET")
)
or (
client_id_set
and _param_or_env_set(litellm_params, environ, "azure_username", "AZURE_USERNAME")
and _param_or_env_set(litellm_params, environ, "azure_password", "AZURE_PASSWORD")
)
or token_refresh_enabled
)
def _async_azure_ad_token_provider(
provider: Callable[[], str | Awaitable[str]],
) -> Callable[[], Awaitable[str]]:

View file

@ -239,10 +239,10 @@ from .llms.anthropic.chat import AnthropicChatCompletion
from .llms.azure.audio_transcriptions import AzureAudioTranscription
from .llms.azure.azure import AzureChatCompletion, check_dynamic_azure_params
from .llms.azure.chat.o_series_handler import AzureOpenAIO1ChatCompletion
from .llms.azure.common_utils import uses_azure_entra_id_auth
from .llms.azure.completion.handler import AzureTextCompletion
from .llms.azure_ai.anthropic.handler import AzureAnthropicChatCompletion
from .llms.azure_ai.anthropic.transformation import azure_anthropic_messages_url
from .llms.azure_ai.common_utils import has_azure_entra_params
from .llms.azure_ai.embed import AzureAIEmbedding
from .llms.bedrock.chat import BedrockConverseLLM
from .llms.bedrock.chat.converse_transformation import AmazonConverseConfig
@ -8843,15 +8843,13 @@ def speech(
optional_params.get("extra_body", {}).pop("azure_ad_token", None)
)
request_kwargs: Final = _REQUEST_KWARGS_ADAPTER.validate_python(kwargs)
deployment_uses_entra: Final = (
request_azure_ad_token is not None
or request_kwargs.get("azure_ad_token_provider") is not None
or has_azure_entra_params(request_kwargs)
uses_entra_id: Final = request_azure_ad_token is not None or uses_azure_entra_id_auth(
request_kwargs, os.environ, litellm.enable_azure_ad_token_refresh is True
)
api_key = (
api_key
or (None if deployment_uses_entra else provider_api_key)
or (None if uses_entra_id else provider_api_key)
or litellm.api_key
or litellm.azure_key
or get_secret("AZURE_OPENAI_API_KEY")

View file

@ -22,6 +22,7 @@ from litellm.llms.azure.common_utils import (
get_azure_ad_token_from_entra_id,
get_azure_ad_token_from_oidc,
get_azure_ad_token_from_username_password,
uses_azure_entra_id_auth,
)
from litellm.secret_managers.get_azure_ad_token_provider import (
get_azure_ad_token_provider,
@ -2492,3 +2493,42 @@ def test_an_azure_client_litellm_built_its_own_http_client_for_is_still_closed(m
closer.reap()
assert wrapper.is_closed() is True
@pytest.mark.parametrize(
("litellm_params", "environ", "token_refresh_enabled", "expected"),
[
({"azure_ad_token": "deployment-token"}, {}, False, True),
({"azure_ad_token_provider": lambda: "token"}, {}, False, True),
({"tenant_id": "t", "client_id": "c", "client_secret": "s"}, {}, False, True),
({}, {"AZURE_TENANT_ID": "t", "AZURE_CLIENT_ID": "c", "AZURE_CLIENT_SECRET": "s"}, False, True),
({"tenant_id": "t", "client_id": "c"}, {"AZURE_CLIENT_SECRET": "s"}, False, True),
({"client_id": "c", "azure_username": "u"}, {"AZURE_PASSWORD": "p"}, False, True),
({}, {}, True, True),
({"tenant_id": "t", "client_id": "c"}, {}, False, False),
({"client_secret": "s"}, {}, False, False),
({"azure_username": "u", "azure_password": "p"}, {}, False, False),
({"tenant_id": None, "client_id": "c", "client_secret": "s"}, {"AZURE_TENANT_ID": "t"}, False, True),
({"tenant_id": "", "client_id": "c", "client_secret": "s"}, {"AZURE_TENANT_ID": "t"}, False, False),
({}, {}, False, False),
],
ids=[
"ad-token",
"ad-token-provider",
"service-principal-in-params",
"service-principal-in-env",
"ids-in-params-secret-in-env",
"username-password-split",
"token-refresh",
"service-principal-missing-secret",
"secret-alone",
"username-password-missing-client-id",
"none-param-falls-back-to-env",
"empty-param-does-not-fall-back-to-env",
"nothing-set",
],
)
def test_uses_azure_entra_id_auth_matches_the_sources_the_azure_client_builds_entra_auth_from(
litellm_params, environ, token_refresh_enabled, expected
):
assert uses_azure_entra_id_auth(litellm_params, environ, token_refresh_enabled) is expected

View file

@ -5599,6 +5599,52 @@ def test_speech_azure_ai_keeps_the_deployments_entra_token_over_the_provider_sco
assert "key-for-another-resource" not in sent_headers.get("api-key", "")
@pytest.mark.parametrize(
("entra_env", "deployment_params", "token_refresh"),
[
({"AZURE_TENANT_ID": "t", "AZURE_CLIENT_ID": "c", "AZURE_CLIENT_SECRET": "s"}, {}, False),
({"AZURE_CLIENT_SECRET": "s"}, {"tenant_id": "t", "client_id": "c"}, False),
({}, {}, True),
],
ids=["service-principal-in-env", "ids-in-config-secret-in-env", "token-refresh"],
)
def test_speech_azure_ai_entra_id_setups_keep_the_key_chain_they_had_before_the_provider_key(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
entra_env: dict[str, str],
deployment_params: dict[str, str],
token_refresh: bool,
):
_clear_global_tts_keys(monkeypatch)
monkeypatch.setattr(litellm, "enable_azure_ad_token_refresh", token_refresh)
_set_or_clear_env(
monkeypatch,
{
"AZURE_TENANT_ID": None,
"AZURE_CLIENT_ID": None,
"AZURE_CLIENT_SECRET": None,
"AZURE_OPENAI_API_KEY": None,
"AZURE_AI_API_KEY": "key-for-another-resource",
"AZURE_API_KEY": "generic-azure-key",
**entra_env,
},
)
route: Final = respx_mock.post(url__regex=r"https://foundry\.internal/openai/v1/audio/speech.*").mock(
return_value=httpx.Response(200, content=TTS_AUDIO, headers={"content-type": "audio/mpeg"})
)
litellm.speech(
model="azure_ai/tts-1",
input="which key goes out",
voice="alloy",
api_base="https://foundry.internal",
api_version="preview",
**deployment_params,
)
assert route.calls.last.request.headers["authorization"] == "Bearer generic-azure-key"
def test_speech_docker_model_runner_placeholder_does_not_beat_a_real_key(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
):