From 7771cbb1ec8ad8b0bf29ff305f31b13d4ab18cd5 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:02:55 -0800 Subject: [PATCH 1/3] style: clear the ruff reds lts/1.108.x already has Sorts two imports ruff flags (I001) in redis_update_buffer.py and utils.py, the same hunks #50348 carries, and drops a duplicate GenericStreamingChunk import (F811) in a streaming test. No behavior change. --- litellm/proxy/db/db_transaction_queue/redis_update_buffer.py | 1 - litellm/utils.py | 2 +- .../litellm_core_utils/test_streaming_chunk_builder_utils.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py b/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py index 22f55ea9153..8fbaa7e3a9a 100644 --- a/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py +++ b/litellm/proxy/db/db_transaction_queue/redis_update_buffer.py @@ -13,7 +13,6 @@ from itertools import chain from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, TypeVar, cast from pydantic import TypeAdapter - from redis.exceptions import RedisError from litellm._internal_context import with_service_target diff --git a/litellm/utils.py b/litellm/utils.py index 51dc6db456a..71c9ca0a16e 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -434,11 +434,11 @@ if TYPE_CHECKING: ) from litellm.llms.cohere.common_utils import CohereModelInfo from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler + from litellm.llms.custom_llm import CustomLLM from litellm.router_utils.get_retry_from_policy import ( get_num_retries_from_retry_policy, reset_retry_policy, ) - from litellm.llms.custom_llm import CustomLLM from litellm.types.llms.anthropic import ( ANTHROPIC_API_ONLY_HEADERS, AnthropicThinkingParam, diff --git a/tests/unit/litellm_core_utils/test_streaming_chunk_builder_utils.py b/tests/unit/litellm_core_utils/test_streaming_chunk_builder_utils.py index a46c8bed4ad..716c0af236c 100644 --- a/tests/unit/litellm_core_utils/test_streaming_chunk_builder_utils.py +++ b/tests/unit/litellm_core_utils/test_streaming_chunk_builder_utils.py @@ -18,7 +18,6 @@ from litellm.types.utils import ( ChatCompletionMessageToolCall, Delta, Function, - GenericStreamingChunk, ModelResponseStream, PromptTokensDetails, ServerToolUse, From eb25b1794610c90a9bc62adba4c11815fe83e805 Mon Sep 17 00:00:00 2001 From: Julien Parenti Date: Fri, 6 Nov 2026 02:41:12 -0800 Subject: [PATCH 2/3] fix(vertex_ai): honor vertex_location for the Cloud Text-to-Speech endpoint (#50255) Backport of #50255 to lts/1.108.x. Cherry-picked from 6fc9555f83b8130a0ffd1e18f38b165eebbebe75 (main), adapted by hand: the line's text-to-speech module has no Gemini TTS code, so the Gemini parts of the pick are dropped and chirp takes its host from the deployment's or call's own vertex_location only. --- .../text_to_speech/transformation.py | 45 ++++- .../text_to_speech/test_transformation.py | 173 +++++++++++++++++- 2 files changed, 214 insertions(+), 4 deletions(-) diff --git a/litellm/llms/vertex_ai/text_to_speech/transformation.py b/litellm/llms/vertex_ai/text_to_speech/transformation.py index 2fae2a9de1c..eaaa600b870 100644 --- a/litellm/llms/vertex_ai/text_to_speech/transformation.py +++ b/litellm/llms/vertex_ai/text_to_speech/transformation.py @@ -49,6 +49,43 @@ _LyriaVoice: TypeAlias = str | dict | None _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object], config=ConfigDict(hide_input_in_errors=True)) _JSON_ARRAY: Final = TypeAdapter(tuple[object, ...], config=ConfigDict(hide_input_in_errors=True)) _STR: Final = TypeAdapter(str, config=ConfigDict(hide_input_in_errors=True)) +_CLOUD_TTS_LOCATIONS: Final = MappingProxyType( + { + **dict.fromkeys( + ( + "us", + "us-central1", + "us-east1", + "us-east4", + "us-east5", + "us-south1", + "us-west1", + "us-west2", + "us-west3", + "us-west4", + "us-west5", + ), + "us", + ), + **dict.fromkeys( + ( + "eu", + "europe-central2", + "europe-north1", + "europe-north2", + "europe-southwest1", + "europe-west1", + "europe-west3", + "europe-west4", + "europe-west8", + "europe-west9", + "europe-west10", + "europe-west12", + ), + "eu", + ), + } +) class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): @@ -173,7 +210,7 @@ class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): # Resolve Vertex AI credentials using VertexBase helpers vertex_credentials: Final = self.safe_get_vertex_ai_credentials(litellm_params_dict) vertex_project: Final = self.safe_get_vertex_ai_project(litellm_params_dict) - vertex_location: Final = self.safe_get_vertex_ai_location(litellm_params_dict) + vertex_location: Final = self.explicit_vertex_ai_location(litellm_params_dict) # Convert voice to string if it's a dict (extract name) # Actual voice mapping happens in map_openai_params @@ -314,7 +351,11 @@ class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): if api_base: return api_base - return self.TTS_API_URL + location: Final = _JSON_OBJECT.validate_python(litellm_params).get("vertex_location") + cloud_tts_location: Final = _CLOUD_TTS_LOCATIONS.get(location) if isinstance(location, str) else None + if cloud_tts_location is None: + return self.TTS_API_URL + return f"https://{cloud_tts_location}-texttospeech.googleapis.com/v1/text:synthesize" def _validate_vertex_input( self, 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 dc6f35fcfe5..b4ccb1cf081 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 @@ -48,6 +48,73 @@ class TestVertexAITextToSpeechConfig: assert url == custom_url + @pytest.mark.parametrize( + ("vertex_location", "expected_host"), + [ + ("eu", "eu-texttospeech.googleapis.com"), + ("europe-west4", "eu-texttospeech.googleapis.com"), + ("europe-west1", "eu-texttospeech.googleapis.com"), + ("europe-west3", "eu-texttospeech.googleapis.com"), + ("europe-west8", "eu-texttospeech.googleapis.com"), + ("europe-west9", "eu-texttospeech.googleapis.com"), + ("europe-west12", "eu-texttospeech.googleapis.com"), + ("europe-west10", "eu-texttospeech.googleapis.com"), + ("europe-central2", "eu-texttospeech.googleapis.com"), + ("europe-north1", "eu-texttospeech.googleapis.com"), + ("europe-north2", "eu-texttospeech.googleapis.com"), + ("europe-southwest1", "eu-texttospeech.googleapis.com"), + ("us", "us-texttospeech.googleapis.com"), + ("us-central1", "us-texttospeech.googleapis.com"), + ("us-east1", "us-texttospeech.googleapis.com"), + ("us-east4", "us-texttospeech.googleapis.com"), + ("us-east5", "us-texttospeech.googleapis.com"), + ("us-south1", "us-texttospeech.googleapis.com"), + ("us-west1", "us-texttospeech.googleapis.com"), + ("us-west2", "us-texttospeech.googleapis.com"), + ("us-west3", "us-texttospeech.googleapis.com"), + ("us-west4", "us-texttospeech.googleapis.com"), + ("us-west5", "us-texttospeech.googleapis.com"), + ], + ) + def test_get_complete_url_uses_regional_endpoint_for_vertex_location( + self, vertex_location: str, expected_host: str + ) -> None: + config: Final = VertexAITextToSpeechConfig() + + url: Final = config.get_complete_url( + model="vertex_ai/chirp", + api_base=None, + litellm_params={"vertex_location": vertex_location}, + ) + + assert url == f"https://{expected_host}/v1/text:synthesize" + + @pytest.mark.parametrize( + "vertex_location", ["global", "europe-west2", "asia-northeast1", "northamerica-northeast1", None] + ) + def test_get_complete_url_keeps_global_endpoint_without_a_regional_match(self, vertex_location: str | None) -> None: + config: Final = VertexAITextToSpeechConfig() + + url: Final = config.get_complete_url( + model="vertex_ai/chirp", + api_base=None, + litellm_params={"vertex_location": vertex_location}, + ) + + assert url == "https://texttospeech.googleapis.com/v1/text:synthesize" + + def test_get_complete_url_api_base_wins_over_vertex_location(self) -> None: + config: Final = VertexAITextToSpeechConfig() + + custom_url: Final = "https://custom-tts-endpoint.example.com/v1/synthesize" + url: Final = config.get_complete_url( + model="vertex_ai/chirp", + api_base=custom_url, + litellm_params={"vertex_location": "eu"}, + ) + + assert url == custom_url + @patch.object(VertexAITextToSpeechConfig, "_ensure_access_token") @patch.object(VertexAITextToSpeechConfig, "_get_token_and_url") def test_transform_text_to_speech_request_body(self, mock_get_token, mock_ensure_token): @@ -616,8 +683,7 @@ def test_litellm_speech_vertex_ai_chirp(mock_get_token, mock_ensure_token, mock_ mock_post.assert_called_once() call_kwargs = mock_post.call_args.kwargs - # Verify the URL is the Google Cloud TTS API - assert call_kwargs["url"] == "https://texttospeech.googleapis.com/v1/text:synthesize" + assert call_kwargs["url"] == "https://us-texttospeech.googleapis.com/v1/text:synthesize" # Verify request body structure assert "json" in call_kwargs @@ -764,6 +830,109 @@ async def test_aspeech_vertex_ai_sends_ssml_input( } +@pytest.mark.asyncio +async def test_aspeech_vertex_ai_chirp_keeps_the_global_host_when_only_vertexai_location_is_set( + respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("VERTEXAI_LOCATION", "europe-west4") + route: Final = respx_mock.post(_SYNTHESIZE_URL).mock( + return_value=httpx.Response(200, json={"audioContent": base64.b64encode(b"global-audio").decode()}) + ) + + response: Final = await litellm.aspeech( + model="vertex_ai/chirp", + input="Bonjour", + voice="fr-FR-Chirp3-HD-Aoede", + vertex_credentials=_AUTHORIZED_USER, + ) + + assert route.call_count == 1 + assert response.content == b"global-audio" + + +@pytest.mark.asyncio +async def test_aspeech_vertex_ai_chirp_keeps_the_global_host_with_other_location_defaults( + respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("VERTEX_LOCATION", "europe-west4") + monkeypatch.setattr(litellm, "vertex_location", "us-central1") + route: Final = respx_mock.post(_SYNTHESIZE_URL).mock( + return_value=httpx.Response(200, json={"audioContent": base64.b64encode(b"global-audio").decode()}) + ) + + response: Final = await litellm.aspeech( + model="vertex_ai/chirp", + input="Bonjour", + voice="fr-FR-Chirp3-HD-Aoede", + vertex_credentials=_AUTHORIZED_USER, + ) + + assert route.call_count == 1 + assert response.content == b"global-audio" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("location_param", "location", "expected_host"), + [ + ("vertex_location", "eu", "eu-texttospeech.googleapis.com"), + ("vertex_location", "us-central1", "us-texttospeech.googleapis.com"), + ("vertex_ai_location", "europe-west4", "eu-texttospeech.googleapis.com"), + ], +) +async def test_aspeech_vertex_ai_chirp_calls_the_regional_host_for_an_explicit_location( + respx_mock: respx.MockRouter, + google_token_endpoint: responses.RequestsMock, + location_param: str, + location: str, + expected_host: str, +) -> None: + route: Final = respx_mock.post(f"https://{expected_host}/v1/text:synthesize").mock( + return_value=httpx.Response(200, json={"audioContent": base64.b64encode(b"regional-audio").decode()}) + ) + + response: Final = await litellm.aspeech( + model="vertex_ai/chirp", + input="Bonjour", + voice="fr-FR-Chirp3-HD-Aoede", + vertex_credentials=_AUTHORIZED_USER, + **{location_param: location}, + ) + + assert route.call_count == 1 + assert response.content == b"regional-audio" + + +@pytest.mark.asyncio +async def test_aspeech_vertex_ai_lyria_predict_still_uses_vertexai_location( + respx_mock: respx.MockRouter, google_token_endpoint: responses.RequestsMock, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("VERTEXAI_LOCATION", "us-east5") + route: Final = respx_mock.post( + "https://us-east5-aiplatform.googleapis.com/v1/projects/test-project/locations/us-east5" + "/publishers/google/models/lyria-002:predict" + ).mock( + return_value=httpx.Response( + 200, + json={ + "predictions": [ + {"audioContent": base64.b64encode(b"lyria-audio").decode(), "mimeType": "audio/wav"}, + ] + }, + ) + ) + + response: Final = await litellm.aspeech( + model="vertex_ai/lyria-002", + input="calm piano", + voice="unused", + vertex_credentials=_AUTHORIZED_USER, + ) + + assert route.call_count == 1 + assert response.content == b"lyria-audio" + + _WAV_AUDIO_B64: Final = "UklGRiQAAABXQVZFZm10IA==" _WAV_AUDIO: Final = b"RIFF$\x00\x00\x00WAVEfmt " _UNLABELED_AUDIO_B64: Final = "bHlyaWEtMy1hdWRpbw==" From d6cfa6f0d39611a4650f640f399a538cf5dc5850 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 6 Nov 2026 04:40:50 -0800 Subject: [PATCH 3/3] chore(deps): bump oauthlib 4.0.0 -> 4.0.1 on lts/1.108.x Lock-only bump that clears GHSA-3jp2-3mrg-2vxc and the line's osv-scan red. main already resolves oauthlib 4.0.1 (#49204), and the lines match main's and #50326's. --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 674ffa049ef..ba0f16119ff 100644 --- a/uv.lock +++ b/uv.lock @@ -6194,11 +6194,11 @@ wheels = [ [[package]] name = "oauthlib" -version = "4.0.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/a1bcc8ba112a627f8ffbdc212a78ce18d3ac07e91a5ca65d27918eee25a1/oauthlib-4.0.0.tar.gz", hash = "sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86", size = 187232, upload-time = "2026-09-28T06:01:18.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/17/2bb072de0b990303c985f26869fd47bb179024a051f47e7f9d208eb92f8e/oauthlib-4.0.1.tar.gz", hash = "sha256:3d73febdb6faa00f277c81fb04be7b7586fb96fc3b7f0b9c772dada69a25ede7", size = 191219, upload-time = "2026-10-31T09:52:58.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/f4/78229a1066068ca14fc60fb26cf7381cabe4382261392b90e5f9552722d4/oauthlib-4.0.0-py3-none-any.whl", hash = "sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd", size = 159715, upload-time = "2026-09-28T06:01:17.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/3b8adde1b32a6f540758bff734d3442c621734c83708e5f02b0ba2f43b84/oauthlib-4.0.1-py3-none-any.whl", hash = "sha256:498a298876e996301950a5b053d12c31628bb7b895db03666430c69ce182eb61", size = 161934, upload-time = "2026-10-31T09:52:56.206Z" }, ] [[package]]