fix: send personGeneration and stop swallowing generate_audio on the Gemini Veo route (NOL-286) - #35
Conversation
…Gemini Veo route (NOL-286) Two silent gaps remained in the Gemini video transform after NOL-252 fixed the crash that kept every Veo 3.1 reference-image run from reaching Google. Neither raised; both changed request semantics quietly. personGeneration was never populated. Per https://ai.google.dev/gemini-api/docs/veo the accepted value is mode-scoped: text-to-video and extension take "allow_all" only, while image-to-video, interpolation, and reference-image runs on Veo 3.x take "allow_adult" only. Image-bearing Veo 3.x requests now carry "allow_adult"; that is the single value Google accepts for the shape, not a permissive default, and it is the stricter of the two (adults only). Text-to-video is deliberately left unset so the provider default applies, since sending "allow_adult" on t2v is a documented 400 and that path works today. Veo 2 accepts a wider set and is untouched. An explicit caller value always wins. generate_audio was dropped by the pydantic parameters model, which declares no such field and ignores extras. The premise turns out to be inverted: the Gemini surface has no audio toggle at all. Veo 3.x generates audio natively and always on, and Google's own SDK rejects the flag with "generate_audio parameter is not supported in Gemini API"; only Vertex exposes generateAudio. So generate_audio: true was already being honored by accident, and adding the field to the model would have made Google reject every request carrying it. The flag is now consumed deliberately rather than by accident, and generate_audio: false raises instead of silently returning a video with an audio track the caller asked not to have. person_generation also joins the snake_case normalization, since the platform sends snake_case and the camelCase-only model was discarding it. Tests assert both fields in the outbound body: personGeneration on i2v and on reference-image runs, absent on t2v and on Veo 2, explicit values preserved, generate_audio consumed rather than forwarded, and the false path raising. All mocked; no live provider calls.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf813955ed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| wants_audio = _audio_preference(params_copy) | ||
| for audio_key in _AUDIO_PARAM_KEYS: | ||
| params_copy.pop(audio_key, None) | ||
| if wants_audio is False and _is_veo_3x(model): |
There was a problem hiding this comment.
Reject audio requests that Veo 2 cannot honor
For the supported gemini/veo-2.0-generate-001 model, generate_audio=true reaches this block, both audio aliases are removed, and no exception is raised because the guard only handles false on Veo 3.x. Since Veo 2 does not generate native audio, the paid request succeeds with a silent video despite the caller explicitly requesting audio—the same silent disagreement this change aims to prevent. Reject true for models without native audio and cover the Veo 2 case with a provider regression test.
AGENTS.md reference: AGENTS.md:L13-L13
Useful? React with 👍 / 👎.
Base moved four commits ahead (MiniMax video/image providers #31, the blind-catch narrowing #33, and the two Gemini Veo route fixes #34/#35). Everything auto-merged except tests/test_litellm/interactions/test_openapi_compliance.py, where both sides had independently loosened the same Content-discriminator assertion after Google's spec dropped the keyword: base via #31, upstream via BerriAI#35161. Kept upstream's version, which is a strict superset (it accepts a discriminator mapping *or* a per-variant `type` const/1-item enum, asserts the values are distinct, and pins TextContent to "text") and matches the `_declared_type_value` helper already in the file. Budget ceilings: LIT002 (27511 -> 27678) and TRY004 (98 -> 100) were the only two rules over limit on the merged tree with a count above the base, so the gates would have failed. The merge adds no net-new violations: every file's LIT002 count in the merged tree equals one of its two parents, so the overage is purely the union of ceilings both sides had ratcheted down independently since the branch point. Raised those two to the merged tree's actual counts; every other ratchet is untouched. Also regenerated model_prices_and_context_window.schema.json, which the new upstream sync check flagged as stale (missing `output_cost_per_audio`) already before this merge. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
TLDR
Problem this solves:
personGenerationwas never sent on any Veo requestgenerate_audiowas silently swallowed by the pydantic params modelHow it solves it:
personGeneration: allow_adultallow_adultis a documented 400 theregenerate_audiois consumed deliberately, not dropped by accidentgenerate_audio: falsenow raises instead of quietly disagreeingRelevant issues
Linear ticket
Resolves NOL-286
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
A real-provider run is deliberately not included; the organization is under a hard spend freeze on generation calls. As in #34, this was proven against a local proxy pointed at a stub upstream on
127.0.0.1:9099that records the request body Veo would have received. The runbook to repeat it against real Veo is belowProxy started from each commit with a config whose only model is
veo-3.1-fastmapped togemini/veo-3.1-fast-generate-previewwithapi_base: http://127.0.0.1:9099Before, at
8f84815a, the NOL-252 merge this branches from. The body the proxy sent upstream, as recorded by the stub:No
personGeneration, and thegenerate_audio: truethat went in appears nowhereAfter, at
cf813955, same request, same stub:Both responses were HTTP 200 with a
video_id andstatus: processing.personGenerationis now populated, andgenerate_audiois still absent from the outbound body, which is now correct rather than incidental; see the audio note under ChangesThe second case is the one that changes caller-visible behavior. Asking Veo for silent video, before at
8f84815a:That 200 is the bug: the caller got a video carrying an audio track it explicitly asked not to have. After, at
cf813955, the same request returns the reason insteadType
🐛 Bug Fix
Changes
personGenerationis mode-scoped, not one global value. Per the Veo docs, text-to-video and extension accept"allow_all"only, while image-to-video, interpolation, and reference-image runs on Veo 3.x accept"allow_adult"only. Image-bearing Veo 3.x requests now sendallow_adultWorth being precise about the posture, since this flag governs whether people can be generated.
allow_adultis not a permissive default anyone picked; it is the only value Google accepts for this request shape, and it is the stricter of the two, allowing adults while excluding minors.allow_allis the more permissive value and is the text-to-video oneText-to-video is deliberately left unset. Sending
allow_adultthere is a documented 400 ("allow_adult for personGeneration is currently not supported"), and t2v works today on the provider default, so this PR does not touch it. Veo 2 accepts a wider set (allow_all,allow_adult,dont_allow) and is left alone. An explicit caller value always wins over the defaultThe audio finding inverts the premise this was filed on. The worry was that a dropped
generate_audiomeant callers asking for audio got silent video. The opposite is true: the Gemini surface has no audio toggle at all. Veo 3.x generates audio natively and always on, and Google's own SDK rejects the flag with "generate_audio parameter is not supported in Gemini API". Only Vertex exposes agenerateAudioboolean, defaulting trueSo
generate_audio: truewas already being honored, by accident, precisely because the pydantic model dropped it. AddinggenerateAudiotoGeminiVideoGenerationParameters, which is the obvious reading of "stop dropping it", would have made Google reject every request carrying the flag. The field is instead consumed in the transform on purpose, with a note on the model so a later change does not "fix" it by adding the field back. A regression test pins that: the flag must not reach Googlegenerate_audio: falseis the one case where a caller's intent genuinely cannot be met, so it raises rather than returning audio and saying nothing.ValueErrormatches the guard #34 added a few lines above and every other video transform in the tree (fal, vertex). It surfaces through the proxy asAPIConnectionErrorand HTTP 400; that wrapping is pre-existing for all transform-level guards here, not something this PR introducesperson_generationalso joins the snake_case normalization alongsideaspect_ratioandnegative_prompt. The platform sends snake_case, and the camelCase-only model was discarding an explicitly requested valueTests are eight additions to the mapped test file, all mocked, no live calls:
personGenerationpresent on image-to-video and on reference-image runs, absent on text-to-video and on Veo 2, an explicit caller value preserved, the snake_case alias normalized,generate_audioconsumed rather than forwarded, and thefalsepath raising. With the source change reverted, the four that assert new behavior fail; the other four are guards that lock in what must not regress, including the one that would catch someone addinggenerateAudioto the modelWhat the docs do not settle
None of this has been confirmed against a live response; the route has never returned one, so being explicit about the edges
The Gemini surface documents
personGenerationas optional but states no default, so whether omitting it on an image request is equivalent to sendingallow_adultis unverified. Vertex documents a default ofallow_adult; this sends it explicitly rather than trusting that inference to carry across surfacesThe regional rule and the mode rule collide, and the docs do not reconcile them. EU/UK/CH/MENA are restricted to
allow_adult, while text-to-video is documented asallow_allonly, which leaves a caller in those regions doing Veo 3.x t2v with no documented valid value. This PR does not try to resolve that; it just leaves t2v unsetCommunity threads report
allow_adulton Veo 3.1 image-to-video needing allowlist access on some projects. That is not in official docs and is unverified, but it is a plausible cause if the first live run still comes back rejectedThe Vertex docs contradict themselves on value spellings (
allowAllvsallow_all,disallowvsdont_allow). Irrelevant here since only the Gemini path is touched, but it is a trap for anyone extending the Vertex config laterQA runbook
To repeat the proof against real Veo rather than the stub, which does cost money and is currently held under a spend freeze:
GEMINI_API_KEYin.envand drop theapi_baseoverride so the route resolves tohttps://generativelanguage.googleapis.compython litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.loginput_referenceset to any publicly reachable image URL, keepingseconds: "8"since reference-image runs require 8, and dropimage_urlsfor the cheapest possible checkvideo_id, then pollGET /v1/videos/{id}untilcompletedlitellm.logthat the outboundparameterscarrypersonGeneration: allow_adultand that nogenerateAudioorgenerate_audiokey is presentSteps 4 through 6 are the first genuine Google response this route will ever have received. A provider-side rejection there is new information rather than a regression from this PR, and the two most likely causes are the allowlist question and the no-documented-default question above
Final Attestation
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.