This commit is contained in:
Mateo Wang 2026-11-03 21:00:41 +00:00 • committed by GitHub
commit cb49e5d4a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 357 additions and 7 deletions

View file

@ -350,6 +350,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

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

@ -69,6 +69,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
@ -102,6 +103,7 @@ from litellm.litellm_core_utils.get_provider_specific_headers import (
ProviderSpecificHeaderUtils,
)
from litellm.litellm_core_utils.health_check_utils import (
OPTIONAL_STR,
_filter_model_params,
create_health_check_response,
)
@ -251,6 +253,7 @@ from .llms.anthropic.common_utils import is_claude_tool_target
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
@ -9006,6 +9009,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=caller_api_base
)
provider_api_key: Final = None if dynamic_api_key in PLACEHOLDER_API_KEYS else dynamic_api_key
kwargs.pop("tags", [])
optional_params = {}
@ -9095,9 +9099,11 @@ def speech(
# set API KEY
api_key = (
api_key
or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there
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
)
organization = (
@ -9178,18 +9184,24 @@ def speech(
api_version = api_version or litellm.api_version or get_secret("AZURE_API_VERSION")
request_azure_ad_token: Final = OPTIONAL_STR.validate_python(
optional_params.get("extra_body", {}).pop("azure_ad_token", None)
)
request_kwargs: Final = _REQUEST_KWARGS_ADAPTER.validate_python(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 uses_entra_id else provider_api_key)
or litellm.api_key
or litellm.azure_key
or get_secret("AZURE_OPENAI_API_KEY")
or get_secret("AZURE_API_KEY")
)
azure_ad_token: Final[str | None] = optional_params.get("extra_body", {}).pop(
"azure_ad_token", None
) or get_secret("AZURE_AD_TOKEN")
azure_ad_token_provider: Final = kwargs.get("azure_ad_token_provider", None)
azure_ad_token: Final[str | None] = request_azure_ad_token or get_secret("AZURE_AD_TOKEN")
if extra_headers:
optional_params["extra_headers"] = extra_headers
@ -9203,7 +9215,7 @@ def speech(
api_base=api_base,
api_version=api_version,
azure_ad_token=azure_ad_token,
azure_ad_token_provider=azure_ad_token_provider,
azure_ad_token_provider=kwargs.get("azure_ad_token_provider", None),
organization=organization,
max_retries=max_retries,
timeout=http_timeout,
@ -9487,7 +9499,6 @@ async def ahealth_check(
"""
from litellm.litellm_core_utils.cached_imports import get_litellm_logging_class
from litellm.litellm_core_utils.health_check_helpers import HealthCheckHelpers, default_health_check_mode
from litellm.litellm_core_utils.health_check_utils import OPTIONAL_STR
# Use cached import helper to lazy-load Logging class (only loads when function is called)
Logging: Final = get_litellm_logging_class()

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,
@ -2500,3 +2501,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

@ -6107,6 +6107,276 @@ def test_speech_mistral_routes_to_configured_api_base(respx_mock: respx.MockRout
assert response.content == audio_bytes
TTS_AUDIO: Final = b"ID3\x04\x00\x00\x00fake-mp3-payload"
def _clear_global_tts_keys(monkeypatch: pytest.MonkeyPatch) -> None:
for attribute in ("api_key", "openai_key", "azure_key", "api_base"):
monkeypatch.setattr(litellm, attribute, None)
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 hosted-vllm-scoped-key"),
(None, "generic-openai-key", "Bearer generic-openai-key"),
(None, None, "Bearer fake-api-key"),
],
ids=["provider-key-alone", "provider-key-beats-generic", "generic-key-beats-placeholder", "placeholder-when-unset"],
)
def test_speech_openai_compatible_sends_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)
_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"})
)
response: Final = litellm.speech(
model="hosted_vllm/tts-1", input="which key goes out", voice="alloy", api_base="https://vllm.internal/v1"
)
assert response.content == TTS_AUDIO
assert route.call_count == 1
assert route.calls.last.request.headers["authorization"] == sent_authorization
@pytest.mark.parametrize(
("generic_azure_key", "sent_api_key"),
[(None, "azure-ai-scoped-key"), ("generic-azure-key", "azure-ai-scoped-key")],
ids=["provider-key-alone", "provider-key-beats-generic"],
)
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)
_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"}))
response: Final = litellm.speech(
model="azure_ai/tts-1",
input="which key goes out",
voice="alloy",
api_base="https://foundry.internal",
api_version="2024-05-01-preview",
)
assert response.content == TTS_AUDIO
assert route.call_count == 1
assert route.calls.last.request.headers["api-key"] == sent_api_key
@pytest.mark.parametrize(
("model", "provider_env", "module_key", "header", "sent_key"),
[
("hosted_vllm/tts-1", "HOSTED_VLLM_API_KEY", "api_key", "authorization", "Bearer provider-scoped-key"),
("hosted_vllm/tts-1", "HOSTED_VLLM_API_KEY", "openai_key", "authorization", "Bearer provider-scoped-key"),
("azure_ai/tts-1", "AZURE_AI_API_KEY", "api_key", "api-key", "provider-scoped-key"),
("azure_ai/tts-1", "AZURE_AI_API_KEY", "azure_key", "api-key", "provider-scoped-key"),
],
ids=["openai-compatible-over-api_key", "openai-compatible-over-openai_key", "azure-over-api_key", "azure-over-azure_key"],
)
def test_speech_provider_scoped_key_beats_module_level_keys(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
model: str,
provider_env: str,
module_key: str,
header: str,
sent_key: str,
):
_clear_global_tts_keys(monkeypatch)
_set_or_clear_env(
monkeypatch,
{provider_env: "provider-scoped-key", "OPENAI_API_KEY": None, "AZURE_OPENAI_API_KEY": None, "AZURE_API_KEY": None},
)
monkeypatch.setattr(litellm, module_key, "module-level-key")
route: Final = respx_mock.post(url__regex=r"https://tts\.internal/.*audio/speech.*").mock(
return_value=httpx.Response(200, content=TTS_AUDIO, headers={"content-type": "audio/mpeg"})
)
litellm.speech(
model=model,
input="which key goes out",
voice="alloy",
api_base="https://tts.internal/v1" if model.startswith("hosted_vllm/") else "https://tts.internal",
api_version="2024-05-01-preview",
)
assert route.calls.last.request.headers[header] == sent_key
def test_speech_azure_ai_keeps_the_deployments_entra_token_over_the_provider_scoped_key(
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
):
_clear_global_tts_keys(monkeypatch)
_set_or_clear_env(
monkeypatch,
{"AZURE_AI_API_KEY": "key-for-another-resource", "AZURE_OPENAI_API_KEY": None, "AZURE_API_KEY": None},
)
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",
azure_ad_token="deployment-entra-token",
)
sent_headers: Final = route.calls.last.request.headers
assert sent_headers["authorization"] == "Bearer deployment-entra-token"
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"
@pytest.mark.parametrize(
("unsent_env", "request_params"),
[
({"AZURE_AD_TOKEN": "token-tts-never-sends"}, {}),
({}, {"extra_body": {"azure_ad_token": "token-tts-never-sends"}}),
({}, {"extra_body": ["not", "an", "object"]}),
],
ids=["ad-token-in-env", "ad-token-in-extra-body", "non-object-extra-body"],
)
def test_speech_azure_ai_sends_the_provider_scoped_key_next_to_an_ad_token_tts_never_sends(
respx_mock: respx.MockRouter,
monkeypatch: pytest.MonkeyPatch,
unsent_env: dict[str, str],
request_params: Mapping[str, object],
):
_clear_global_tts_keys(monkeypatch)
_set_or_clear_env(
monkeypatch,
{
"AZURE_AD_TOKEN": None,
"AZURE_OPENAI_API_KEY": None,
"AZURE_API_KEY": None,
"AZURE_AI_API_KEY": "azure-ai-scoped-key",
**unsent_env,
},
)
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"}))
response: Final = litellm.speech(
model="azure_ai/tts-1",
input="which key goes out",
voice="alloy",
api_base="https://foundry.internal",
api_version="2024-05-01-preview",
**request_params,
)
assert response.content == TTS_AUDIO
assert route.calls.last.request.headers["api-key"] == "azure-ai-scoped-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
):
_clear_global_tts_keys(monkeypatch)
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "hosted-vllm-scoped-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"})
)
litellm.speech(
model="hosted_vllm/tts-1",
input="which key goes out",
voice="alloy",
api_base="https://vllm.internal/v1",
api_key="caller-supplied-key",
)
assert route.calls.last.request.headers["authorization"] == "Bearer caller-supplied-key"
FOUNDRY_HOST: Final = "https://my-project.services.ai.azure.com"