feat(vertex_ai): add Google Cloud Speech-to-Text Chirp 3 transcription support - #32274
Conversation
… body httpx form-encodes dicts passed via data= and silently ignores json=, so the generic audio transcription path never actually sent a JSON body. No provider hit this before; JSON-body speech APIs need it.
…n support Adds a VertexAIAudioTranscriptionConfig wired through ProviderConfigManager so vertex_ai/chirp_3 works on /v1/audio/transcriptions (sync and async) via the Speech-to-Text v2 recognize API. Auth reuses the standard Vertex credential resolution (vertex_project/vertex_location/vertex_credentials or ADC); the location defaults to the us multi-region since chirp_3 is only served from the us and eu multi-regions, and non-global locations use the regional <location>-speech.googleapis.com host. Maps language to languageCodes (auto language detection by default), joins all result alternatives into the transcript, and tracks cost from totalBilledDuration with a vertex_ai/chirp_3 price entry at Google's published $0.016/min.
Greptile SummaryThis PR adds Google Cloud Speech-to-Text Chirp 3 transcription support via
Confidence Score: 5/5Safe to merge — the changes are well-isolated, all new code paths are covered by mocked tests, and the security guards on client-controllable inputs are validated by parametrized injection tests. The new Chirp 3 integration follows the established BaseAudioTranscriptionConfig pattern exactly, the httpx dict-body fix is mechanically correct and symmetrically applied to sync and async paths, and the cost map entry is structured to avoid the silent zero-cost trap. No pre-existing behavior is altered for other providers. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/vertex_ai/audio_transcription/transformation.py | New VertexAIAudioTranscriptionConfig implementing the full request/response transformation, URL construction, and security validation for Chirp 3 STT. Logic is sound: location and project validated before URL interpolation, language normalization via shared helper, billed duration parsed for cost tracking. |
| litellm/llms/custom_httpx/llm_http_handler.py | Fixes latent bug where dict bodies were passed to both data= and json= in httpx calls; now routes dict data exclusively through json= when no files are present. Both sync and async paths corrected symmetrically. |
| litellm/llms/vertex_ai/common_utils.py | Extracts validate_vertex_location as a shared helper called by both the chat URL builder (get_vertex_base_url) and the new speech URL builder, eliminating duplicated validation logic. Behavior preserved; empty string and None are rejected as before. |
| litellm/litellm_core_utils/audio_utils/utils.py | Adds normalize_transcription_language_to_bcp47 shared helper and BARE_ISO_639_1_TO_BCP47 mapping table; unknown bare codes pass through unchanged so the downstream provider returns a clear error. |
| litellm/types/llms/vertex_ai_speech_to_text.py | New TypedDicts and Pydantic models for the Speech-to-Text v2 recognize request/response schema. Optional fields with defaults handle missing keys in API responses gracefully. |
| litellm/utils.py | Routes VERTEX_AI provider to the new VertexAIAudioTranscriptionConfig in ProviderConfigManager.get_provider_audio_transcription_config; lazy import keeps google-auth optional. |
| model_prices_and_context_window.json | Adds vertex_ai/chirp_3 entry with input_cost_per_second only (no output_cost_per_second: 0.0), correctly avoiding the silent zero-cost bug in the shared cost_per_second calculator. |
| tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py | Comprehensive mock-based test suite covering URL construction, security validation, request body shape, language mapping, response parsing, provider routing, and cost map integrity. No network calls. |
| tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py | Adds regression tests for the dict-body-as-JSON fix using httpx.MockTransport in both sync and async paths; correctly asserts content-type and parsed body. |
| tests/test_litellm/test_cost_calculator.py | Adds regression test for chirp_3 cost calculation that would have caught the output_cost_per_second:0.0 zero-cost bug; uses LITELLM_LOCAL_MODEL_COST_MAP to avoid network access. |
Reviews (3): Last reviewed commit: "fix(vertex_ai): reject unsupported trans..." | Re-trigger Greptile
Greptile SummaryAdds Vertex AI Chirp 3 speech-to-text support via the Google Speech-to-Text v2
Confidence Score: 4/5Safe to merge; the new provider integration is well-isolated, reuses existing Vertex auth infrastructure, and is covered by thorough mock tests. The HTTP handler change is a genuine bug fix with regression tests. The core implementation is solid and the httpx bug fix is correct. The main gap is that litellm/llms/vertex_ai/audio_transcription/transformation.py —
|
| Filename | Overview |
|---|---|
| litellm/llms/vertex_ai/audio_transcription/transformation.py | New Vertex AI Speech-to-Text Chirp 3 transformation config; response_format is advertised as supported in get_supported_openai_params but is never read in the request or response transformation |
| litellm/llms/custom_httpx/llm_http_handler.py | Fixes latent bug where dict request body was sent to httpx's data= (form-encoding it) instead of json=; both sync and async paths corrected symmetrically |
| litellm/types/llms/vertex_ai_speech_to_text.py | New Pydantic/TypedDict types for Google Speech-to-Text v2 request/response; fields have appropriate Optional defaults for graceful handling of partial responses |
| litellm/utils.py | Wires VertexAIAudioTranscriptionConfig into ProviderConfigManager.get_provider_audio_transcription_config alongside existing providers |
| tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py | Comprehensive mock-only unit tests covering URL construction, request body shaping, response transformation, auth header population, provider routing, and cost-map entries |
| tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py | Adds regression tests for the JSON body fix; both sync and async paths are covered with a mock transport that captures content-type and body |
| model_prices_and_context_window.json | Adds vertex_ai/chirp_3 cost entry at Google's published $0.016/min, correctly represented as per-second pricing |
Reviews (2): Last reviewed commit: "feat(vertex_ai): add Google Cloud Speech..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
…h-to-Text
OpenAI clients send language codes like "en", which Google rejects with 400
("not supported by the model chirp_3 in the location us"); Speech-to-Text
wants region-qualified BCP-47 like "en-US". Adds a shared
normalize_transcription_language_to_bcp47 helper in audio_utils (NVIDIA Riva's
transcription config already hand-rolled the same table privately) that maps
common bare codes and passes region-qualified ones through, and applies it in
the Vertex transcription request. Also narrows the response JSON parse guard
to ValueError.
…king works cost_per_second prefers output_cost_per_second whenever it is not None, so the 0.0 in the chirp_3 entry priced every transcription at $0.00 instead of using input_cost_per_second. Remove it from both cost maps and pin the behavior with a regression test computing 18s of chirp_3 audio to ~$0.0048.
… in Speech-to-Text get_complete_url interpolated vertex_location straight into the request host, and vertex_location is client-controllable on the proxy (it flows from the request body and is not on the request-body blocklist). An authenticated caller could send vertex_location="attacker.example/" to point the host at their own server, so the proxy would POST the audio plus its admin-minted Google bearer token and x-goog-user-project header to the attacker, exfiltrating a cloud-platform-scoped OAuth token minted from the admin's credentials. Factor the location validation the rest of vertex_ai already applied in get_vertex_base_url (^[a-z][a-z0-9-]*$ plus the global allowance) into a shared validate_vertex_location helper in common_utils and call it from both the chat host builder and the new speech host builder. Invalid locations now raise a 400 VertexAIError instead of building a host. Also reject vertex_project values that carry URL-structural characters, since it lands in the URL path. Regression tests assert on the parsed netloc so the security property is pinned: valid locations always resolve to a *speech.googleapis.com host and injection inputs are rejected.
…es instead of silently ignoring
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Vertex transcription routes all models
- Gated the VERTEX_AI branch of
get_provider_audio_transcription_configonmodel_cost_entry.get("mode") == "audio_transcription"so only entries marked as transcription models (e.g.vertex_ai/chirp_3) are routed to Google Speech-to-Text.
- Gated the VERTEX_AI branch of
You can send follow-ups to the cloud agent here.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2b87067. Configure here.
| VertexAIAudioTranscriptionConfig, | ||
| ) | ||
|
|
||
| return VertexAIAudioTranscriptionConfig() |
There was a problem hiding this comment.
Vertex transcription routes all models
Medium Severity
get_provider_audio_transcription_config returns VertexAIAudioTranscriptionConfig for every vertex_ai transcription call, without checking that the model is an audio-transcription entry (for example mode in the cost map). Before this change those calls failed as unmapped; now they are sent to Google Speech-to-Text with the chat model id in the payload.
Reviewed by Cursor Bugbot for commit 2b87067. Configure here.
There was a problem hiding this comment.
every provider branch there routes unconditionally (OpenAI sends any model to whisper), nothing working is shadowed, and misuse now gets Google's clear 400 instead of "Unmapped provider"
ffb16b7 to
2b87067
Compare
|
api_base on the new STT endpoint is client-controllable and wholesale replaces the validated host — so the SSRF fix that constrained vertex_location is bypassable by sending api_base="http://attacker.example". Proxy would POST the audio + its Google bearer + x-goog-user-project header to the attacker. |
api_base from /audio/transcriptions form data is rejected by the proxy request-body safety check unless explicitly enabled by admin configuration |
|
hi @mateo-berri , how is it going? from what i know, google lets developer to build input streaming STT pipeline with https://docs.cloud.google.com/python/docs/reference/speech/latest/google.cloud.speech_v1.types.StreamingRecognitionConfig is it available in LiteLLM? |


Relevant issues
Linear ticket
Resolves LIT-3682
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live before/after demonstration on a local proxy hitting the real Google Speech-to-Text v2 API with application default credentials,
LITELLM_LOCAL_MODEL_COST_MAP=True, and the 17.6 secondtests/gettysburg.wavclip. The proxy config mapschirp-3tovertex_ai/chirp_3with onlyvertex_projectset, so theuslocation default is what gets exercisedBefore commit:
b4a10fb134ac0de14a642efc7a5a5adde95c7b6e(merge base, feature absent). After commit:2b8706776f4b25f5745e819232166b8baf04a3cf(branch head). The transcription and security command outputs below were captured ataaf8b1f0abaa36cde7afb511c2661aa7af9178fb, the security-fix commit; the head commit only adds response_format validation on top and leaves these results unchangedBefore, on the merge base, the request fails with an unmapped provider error:
After, on this branch, the identical request returns HTTP 200 with the transcript in the OpenAI response shape:
An explicit OpenAI style bare ISO-639-1
languagealso works and gets normalized to the BCP-47 code Google requires, solanguage="en"returns 200 with the language echoed back asen-US:Cost tracking prices the 18 billed seconds at Google's $0.016 per minute rate; both successful requests in the run logged the same figure:
Security: client-controlled location is validated
vertex_locationis client-controllable on the proxy and lands in the request host, so a crafted value could otherwise redirect the outbound call, along with the admin-minted Google bearer token, to an attacker host. On this branch every non-conforming location is rejected with a 400 before any request is built, and the token never leaves the proxy. Three malicious values, a hostname with a trailing slash, one with a fragment, and one pointing at a local listener, all return the same 400:To prove no egress, a throwaway listener ran on
127.0.0.1:61688and its log was truncated to empty before firing the127.0.0.1:61688/case above. After the malicious requests the listener log is still empty, so the token never reached it:The proxy debug log confirms the only outbound Speech-to-Text hosts were Google's own regional endpoints; the attacker strings appear solely as echoes of the rejected input, never in an outbound URL:
A well-formed location is still honored:
vertex_location="us-central1"builds a realus-central1-speech.googleapis.comrequest, which Google answers with a benign 400 because chirp_3 is not served from that region, not an SSRF:Type
🆕 New Feature
Changes
Adds Google Cloud Speech-to-Text support for Chirp 3, so
vertex_ai/chirp_3works on/v1/audio/transcriptionsthrough bothlitellm.transcription()andlitellm.atranscription()(the async path is what the proxy uses). The newVertexAIAudioTranscriptionConfigis wired throughProviderConfigManagerand calls the Speech-to-Text v2recognizeendpoint. Authentication reuses the standard Vertex AI credential resolution (vertex_project,vertex_location,vertex_credentials, or application default credentials), so users configure it exactly like any other vertex_ai modelThe location defaults to the
usmulti-region because Google serves chirp_3 only from theusandeumulti-regions, and non-global locations are routed to the regional<location>-speech.googleapis.comhost. The OpenAIlanguageparam maps tolanguageCodes, defaulting toauto(Chirp 3's language-agnostic transcription). Because OpenAI clients send bare ISO-639-1 codes likeenwhile Google rejects anything that is not region-qualified BCP-47, a sharednormalize_transcription_language_to_bcp47helper inlitellm_core_utils/audio_utilsmaps common bare codes (enbecomesen-US) and passes region-qualified codes through unchanged; the NVIDIA Riva transcription config had already hand-rolled the same mapping privately, so the shared helper gives future providers one place to reuse. The response joins the top alternative of every result into the transcript, anddurationcomes from thetotalBilledDurationGoogle returns, so cost tracking uses Google's own billing numberA
vertex_ai/chirp_3cost map entry is added at Google's published $0.016 per minute for V2 standard recognition. The entry deliberately omitsoutput_cost_per_second: the sharedcost_per_secondcalculator prefersoutput_cost_per_secondwhenever it is not None, so a 0.0 there (the shape several existing STT entries use) silently prices every request at $0.00. A regression test computes the chirp_3 transcription cost from model info so that exact failure cannot recurBecause
vertex_locationis client-controllable on the proxy and lands in the request host,get_complete_urlvalidates it before use so a caller cannot steer the request (and its admin-minted Google bearer token) at an attacker host. The^[a-z][a-z0-9-]*$plusglobalcheck that the rest of vertex_ai already applied throughget_vertex_base_urlis factored into a sharedvalidate_vertex_locationhelper incommon_utils.pyand called from both the chat host builder and the new speech host builder, so there is one validation, not two copies. An invalid location raises a 400VertexAIErrorrather than falling back silently, andvertex_project(which lands in the URL path) is likewise rejected if it carries path-structural characters. Tests assert on the parsed netloc so the security property is pinned, not just the stringLike every other vertex_ai flow, authentication needs
google-authat request time, which is not part of the base install; bare proxy installs should use thegoogleextra (pip install "litellm[google]"), the same requirement vertex_ai chat and TTS models already haveWiring this end to end surfaced a latent bug in the shared audio transcription HTTP handler: dict request bodies were passed to httpx's
data=parameter, which form-encodes them and silently ignores thejson=argument the handler also passed, so the JSON body path had never actually worked (no existing provider on the generic path returned dict data). The handler now sends JSON when a provider returns dict data without files, in both the sync and async paths; providers using multipart or raw bytes are unaffected. Regression tests pin the JSON body behavior at the handler level and the full request/response transformation (regional URL building, language mapping, transcript joining, billed duration parsing, provider routing) at the Vertex config levelExample usage:
Note
Medium Risk
Touches proxy-facing Vertex URL construction and OAuth-backed outbound calls, with deliberate SSRF guards; the shared transcription HTTP change affects all JSON-body providers but is covered by regression tests.
Overview
Adds
vertex_ai/chirp_3on/v1/audio/transcriptionsvia a newVertexAIAudioTranscriptionConfigthat calls Google Speech-to-Text v2recognize(base64 JSON body, OAuth headers, regional*-speech.googleapis.comURLs). OpenAIlanguagemaps tolanguageCodes(defaultauto); bare ISO codes are normalized through sharednormalize_transcription_language_to_bcp47. Responses are shaped as OpenAI transcripts with optionallanguageand billedduration.Security:
validate_vertex_locationis centralized incommon_utils(used by chat base URLs and speech URLs) so client-controlledvertex_location/vertex_projectcannot inject hosts or path segments before outbound Google calls.HTTP fix: Sync/async transcription handlers now send dict payloads with
json=only (notdata=alongsidejson=), fixing JSON-body providers.Pricing:
vertex_ai/chirp_3is registered withinput_cost_per_secondfor transcription billing; tests cover cost from duration.Reviewed by Cursor Bugbot for commit 2b87067. Bugbot is set up for automated code reviews on this repo. Configure here.