diff --git a/litellm/litellm_core_utils/llm_response_utils/get_api_base.py b/litellm/litellm_core_utils/llm_response_utils/get_api_base.py index 3815ea91b51..4d731b5e63a 100644 --- a/litellm/litellm_core_utils/llm_response_utils/get_api_base.py +++ b/litellm/litellm_core_utils/llm_response_utils/get_api_base.py @@ -59,6 +59,10 @@ def get_api_base(model: str, optional_params: dict | LiteLLM_Params) -> str | No if _optional_params.api_base is not None: return _optional_params.api_base + extra_params: Final = _optional_params.model_extra + base_url_alias: Final = extra_params.get("base_url") if extra_params is not None else None + if isinstance(base_url_alias, str) and base_url_alias: + return base_url_alias if litellm.model_alias_map and model in litellm.model_alias_map: model = litellm.model_alias_map[model] diff --git a/litellm/main.py b/litellm/main.py index 769eac79488..8c9d7f2513d 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -7931,6 +7931,7 @@ def transcription( api_version: str | None = None, max_retries: int | None = None, custom_llm_provider=None, + base_url: str | None = None, **kwargs, ) -> TranscriptionResponse | Coroutine[object, object, TranscriptionResponse]: """ @@ -7964,7 +7965,7 @@ def transcription( model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider( model=model, custom_llm_provider=custom_llm_provider, - api_base=api_base, + api_base=api_base or base_url, api_key=api_key, ) @@ -8237,6 +8238,7 @@ def speech( headers: dict | None = None, custom_llm_provider: str | None = None, aspeech: bool | None = None, + base_url: str | None = None, **kwargs, ) -> HttpxBinaryResponseContent | Coroutine[object, object, HttpxBinaryResponseContent]: user: Final = kwargs.get("user", None) @@ -8246,7 +8248,7 @@ def speech( model_info: Final = kwargs.get("model_info", None) shared_session: Final = kwargs.get("shared_session", None) model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider( - model=model, custom_llm_provider=custom_llm_provider, api_base=api_base + model=model, custom_llm_provider=custom_llm_provider, api_base=api_base or base_url ) kwargs.pop("tags", []) @@ -8550,7 +8552,7 @@ def speech( extra_headers=headers, base_llm_http_handler=base_llm_http_handler, aspeech=aspeech or False, - api_base=generic_optional_params.api_base, + api_base=api_base, api_key=None, # Vertex AI uses OAuth, not API key **kwargs, ) diff --git a/tests/unit/litellm_core_utils/llm_response_utils/test_get_api_base.py b/tests/unit/litellm_core_utils/llm_response_utils/test_get_api_base.py index 63977c30270..90fd5ba6c88 100644 --- a/tests/unit/litellm_core_utils/llm_response_utils/test_get_api_base.py +++ b/tests/unit/litellm_core_utils/llm_response_utils/test_get_api_base.py @@ -91,3 +91,22 @@ def test_providers_with_a_fixed_base_still_get_it(model, expected, monkeypatch): monkeypatch.delenv(env, raising=False) assert litellm.get_api_base(model=model, optional_params={}) == expected + + +def test_base_url_alias_is_reported_as_the_api_base(): + api_base = litellm.get_api_base( + model="groq/whisper-large-v3", optional_params={"base_url": "https://groq.gateway.internal/openai/v1"} + ) + + assert api_base == "https://groq.gateway.internal/openai/v1" + assert ( + litellm.get_api_base( + model="groq/whisper-large-v3", + optional_params={"api_base": "https://explicit.internal/v1", "base_url": "https://alias.internal/v1"}, + ) + == "https://explicit.internal/v1" + ) + assert ( + litellm.get_api_base(model="groq/whisper-large-v3", optional_params={"base_url": ""}) + == "https://api.groq.com/openai/v1" + ) diff --git a/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py b/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py index ee7bdebe745..fd667e8f425 100644 --- a/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py +++ b/tests/unit/llms/vertex_ai/text_to_speech/test_transformation.py @@ -261,9 +261,7 @@ class TestVertexAILyriaTextToSpeechConfig: ) def test_get_complete_url_encodes_injected_predict_path_segments(self, monkeypatch: pytest.MonkeyPatch) -> None: - injected: Final = ( - "victim-project/locations/us-central1/publishers/google/models/other-model:predict?ignored=" - ) + injected: Final = "victim-project/locations/us-central1/publishers/google/models/other-model:predict?ignored=" encoded: Final = ( "victim-project%2Flocations%2Fus-central1%2Fpublishers%2Fgoogle" "%2Fmodels%2Fother-model%3Apredict%3Fignored%3D" @@ -554,6 +552,33 @@ class TestVertexAILyriaTextToSpeechConfig: assert mock_post.call_args.kwargs["json"] == expected_body +@pytest.mark.parametrize("endpoint_kwarg", ["api_base", "base_url"]) +def test_litellm_speech_vertex_ai_sends_request_to_the_configured_endpoint(endpoint_kwarg: str): + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"audioContent": "SGVsbG8gV29ybGQ="} + with ( + patch.object( # test-quality-ok: litellm.speech has no seam for Vertex token minting + VertexAITextToSpeechConfig, "_ensure_access_token", return_value=("mock-token", "test-project") + ), + patch( # test-quality-ok: litellm.speech has no seam for the HTTP handler + "litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post", return_value=mock_response + ) as mock_post, + ): + response = litellm.speech( + model="vertex_ai/chirp", + input="Hello", + voice="en-US-Chirp3-HD-Charon", + vertex_project="test-project", + vertex_location="us-central1", + **{endpoint_kwarg: "https://tts.gateway.internal/v1/text:synthesize"}, + ) + + assert mock_post.call_args.kwargs["url"] == "https://tts.gateway.internal/v1/text:synthesize" + assert response.content == b"Hello World" + + @patch("litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post") @patch.object(VertexAITextToSpeechConfig, "_ensure_access_token") @patch.object(VertexAITextToSpeechConfig, "_get_token_and_url") diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e0e1fcfe105..e159e564a71 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -4188,6 +4188,62 @@ def test_azure_ai_speech_on_a_foundry_host_uses_the_azure_openai_deployment_rout assert response.content == b"mp3-bytes" +GROQ_INTERNAL_BASE: Final = "https://groq.gateway.internal/openai/v1" +GROQ_WAV_FILE: Final = ("tone.wav", b"RIFF\x00\x00\x00\x00WAVE", "audio/wav") + + +def test_groq_transcription_honors_base_url_alias(respx_mock: respx.MockRouter): + route: Final = respx_mock.post(f"{GROQ_INTERNAL_BASE}/audio/transcriptions").mock( + return_value=httpx.Response(200, json={"text": "hello"}) + ) + + response: Final = litellm.transcription( + model="groq/whisper-large-v3", + file=GROQ_WAV_FILE, + base_url=GROQ_INTERNAL_BASE, + api_key="fake-key", + ) + + assert route.called + assert response.text == "hello" + + +async def test_groq_atranscription_honors_base_url_alias( + respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + route: Final = respx_mock.post(f"{GROQ_INTERNAL_BASE}/audio/transcriptions").mock( + return_value=httpx.Response(200, json={"text": "hello"}) + ) + + response: Final = await litellm.atranscription( + model="groq/whisper-large-v3", + file=GROQ_WAV_FILE, + base_url=GROQ_INTERNAL_BASE, + api_key="fake-key", + ) + + assert route.called + assert response.text == "hello" + + +def test_groq_speech_honors_base_url_alias(respx_mock: respx.MockRouter): + route: Final = respx_mock.post(f"{GROQ_INTERNAL_BASE}/audio/speech").mock( + return_value=httpx.Response(200, content=b"mp3-bytes") + ) + + response: Final = litellm.speech( + model="groq/playai-tts", + input="hello", + voice="Fritz-PlayAI", + base_url=GROQ_INTERNAL_BASE, + api_key="fake-key", + ) + + assert route.called + assert response.content == b"mp3-bytes" + + FORWARDED_CLIENT_HEADERS: Final = {"x-forwarded-for": "10.0.0.1", "x-amzn-trace-id": "Root=1-lit7694"}