Skip to content

fix: send personGeneration and stop swallowing generate_audio on the Gemini Veo route (NOL-286) - #35

Merged
blackflame007 merged 1 commit into
litellm_internal_stagingfrom
litellm_nol286_veo_person_audio
Aug 1, 2026
Merged

fix: send personGeneration and stop swallowing generate_audio on the Gemini Veo route (NOL-286)#35
blackflame007 merged 1 commit into
litellm_internal_stagingfrom
litellm_nol286_veo_person_audio

Conversation

@blackflame007

@blackflame007 blackflame007 commented Aug 1, 2026

Copy link
Copy Markdown

TLDR

Problem this solves:

  • personGeneration was never sent on any Veo request
  • Image-bearing Veo 3.x accepts exactly one value; we sent none
  • generate_audio was silently swallowed by the pydantic params model
  • Asking Veo for silent video returned audio, with no error

How it solves it:

  • Image-bearing Veo 3.x now sends personGeneration: allow_adult
  • Text-to-video left unset; allow_adult is a documented 400 there
  • generate_audio is consumed deliberately, not dropped by accident
  • generate_audio: false now raises instead of quietly disagreeing

Relevant issues

Linear ticket

Resolves NOL-286

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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:9099 that records the request body Veo would have received. The runbook to repeat it against real Veo is below

Proxy started from each commit with a config whose only model is veo-3.1-fast mapped to gemini/veo-3.1-fast-generate-preview with api_base: http://127.0.0.1:9099

curl -s -w "\nHTTP %{http_code}\n" http://127.0.0.1:4000/v1/videos \
  -H "Authorization: Bearer sk-nol286-local" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "veo-3.1-fast",
    "prompt": "Creator lifts the lid off the box and reacts",
    "input_reference": "http://127.0.0.1:9099/images/start.png",
    "image_urls": ["http://127.0.0.1:9099/images/ref-a.png","http://127.0.0.1:9099/images/ref-b.png"],
    "aspect_ratio": "9:16",
    "seconds": "8",
    "generate_audio": true,
    "resolution": "720p"
  }'

Before, at 8f84815a, the NOL-252 merge this branches from. The body the proxy sent upstream, as recorded by the stub:

{"path": "/v1beta/models/veo-3.1-fast-generate-preview:predictLongRunning",
 "instance_keys": ["image", "prompt", "referenceImages"],
 "image_field": {"bytesBase64Encoded": "<base64 96 chars>", "mimeType": "image/png"},
 "n_reference_images": 2,
 "parameters": {"aspectRatio": "9:16", "durationSeconds": 8, "resolution": "720p"}}

No personGeneration, and the generate_audio: true that went in appears nowhere

After, at cf813955, same request, same stub:

{"path": "/v1beta/models/veo-3.1-fast-generate-preview:predictLongRunning",
 "instance_keys": ["image", "prompt", "referenceImages"],
 "image_field": {"bytesBase64Encoded": "<base64 96 chars>", "mimeType": "image/png"},
 "n_reference_images": 2,
 "parameters": {"aspectRatio": "9:16", "durationSeconds": 8, "resolution": "720p", "personGeneration": "allow_adult"}}

Both responses were HTTP 200 with a video_ id and status: processing. personGeneration is now populated, and generate_audio is still absent from the outbound body, which is now correct rather than incidental; see the audio note under Changes

The second case is the one that changes caller-visible behavior. Asking Veo for silent video, before at 8f84815a:

curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:4000/v1/videos \
  -H "Authorization: Bearer sk-nol286-local" -H "Content-Type: application/json" \
  -d '{"model":"veo-3.1-fast","prompt":"Silent beauty shot",
       "input_reference":"http://127.0.0.1:9099/images/start.png",
       "seconds":"8","generate_audio":false}'

HTTP 200

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 instead

{"error":{"message":"litellm.APIConnectionError: generate_audio=false is not supported for Veo 3.x
on the Gemini video route: audio is generated natively and always on, and the Gemini API exposes no
field to disable it. Route to a model that renders silent video if you need no audio track." ...

Type

🐛 Bug Fix

Changes

personGeneration is 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 send allow_adult

Worth being precise about the posture, since this flag governs whether people can be generated. allow_adult is 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_all is the more permissive value and is the text-to-video one

Text-to-video is deliberately left unset. Sending allow_adult there 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 default

The audio finding inverts the premise this was filed on. The worry was that a dropped generate_audio meant 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 a generateAudio boolean, defaulting true

So generate_audio: true was already being honored, by accident, precisely because the pydantic model dropped it. Adding generateAudio to GeminiVideoGenerationParameters, 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 Google

generate_audio: false is the one case where a caller's intent genuinely cannot be met, so it raises rather than returning audio and saying nothing. ValueError matches the guard #34 added a few lines above and every other video transform in the tree (fal, vertex). It surfaces through the proxy as APIConnectionError and HTTP 400; that wrapping is pre-existing for all transform-level guards here, not something this PR introduces

person_generation also joins the snake_case normalization alongside aspect_ratio and negative_prompt. The platform sends snake_case, and the camelCase-only model was discarding an explicitly requested value

Tests are eight additions to the mapped test file, all mocked, no live calls: personGeneration present 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_audio consumed rather than forwarded, and the false path 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 adding generateAudio to the model

What 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 personGeneration as optional but states no default, so whether omitting it on an image request is equivalent to sending allow_adult is unverified. Vertex documents a default of allow_adult; this sends it explicitly rather than trusting that inference to carry across surfaces

The 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 as allow_all only, 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 unset

Community threads report allow_adult on 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 rejected

The Vertex docs contradict themselves on value spellings (allowAll vs allow_all, disallow vs dont_allow). Irrelevant here since only the Gemini path is touched, but it is a trap for anyone extending the Vertex config later

QA runbook

To repeat the proof against real Veo rather than the stub, which does cost money and is currently held under a spend freeze:

  1. Put a real GEMINI_API_KEY in .env and drop the api_base override so the route resolves to https://generativelanguage.googleapis.com
  2. Start the proxy: python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log
  3. Run the first curl above with input_reference set to any publicly reachable image URL, keeping seconds: "8" since reference-image runs require 8, and drop image_urls for the cheapest possible check
  4. Expect HTTP 200 with a video_ id, then poll GET /v1/videos/{id} until completed
  5. Confirm in litellm.log that the outbound parameters carry personGeneration: allow_adult and that no generateAudio or generate_audio key is present
  6. Download the result and confirm it has an audio track, which is what settles the audio question empirically

Steps 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

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

…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.
@blackflame007
blackflame007 merged commit bda9a97 into litellm_internal_staging Aug 1, 2026
77 checks passed
@blackflame007
blackflame007 deleted the litellm_nol286_veo_person_audio branch August 1, 2026 21:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

blackflame007 added a commit that referenced this pull request Aug 3, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant