mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
fix(transcription): honor base_url alias for Groq Whisper and report it as the api base (#43917)
* fix(transcription): honor base_url alias for Groq Whisper and report it as the api base transcription() and speech() only accepted api_base, so a deployment configured with base_url leaked the alias into the provider params, which Groq rejected as an unknown param, and the request never reached the internal gateway. get_api_base() now reads the same alias so response headers and logs show the configured endpoint instead of the provider default Resolves LIT-9071 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(speech): keep base_url after existing audio params, route Vertex speech to it, skip empty alias Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
e662772ad1
commit
b41715b0c2
5 changed files with 112 additions and 6 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue