Skip to content

fix(vertex_ai): return a truncated choice for content-less Gemini candidates - #38301

Closed
yukimaru77 wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
yukimaru77:fix-vertex-contentless-candidate-rebase
Closed

yukimaru77 wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
yukimaru77:fix-vertex-contentless-candidate-rebase

Conversation

@yukimaru77

Copy link
Copy Markdown

TLDR

Problem this solves:

  • Content-less Gemini candidates become an empty choices list
  • Callers cannot inspect the model's truncation result

How it solves it:

  • Preserve each content-less candidate as an empty choice
  • Map provider finish reasons to OpenAI-compatible values

User Flow

Before: a developer receives no choice when Gemini exhausts its token budget during reasoning

  1. They send POST https://litellm-domain/v1/chat/completions to a Gemini thinking model with a small max_tokens
  2. Gemini returns one candidate with finishReason: "MAX_TOKENS" and no content
  3. The response contains "choices": [], so their application cannot inspect the first choice

After: the same request returns a choice that identifies token-limit truncation

  1. They send POST https://litellm-domain/v1/chat/completions to a Gemini thinking model with a small max_tokens
  2. Gemini returns one candidate with finishReason: "MAX_TOKENS" and no content
  3. The response contains one empty choice with finish_reason: "length"

Relevant issues

Fixes #36881

Supersedes #36870 by applying its two commits to the latest litellm_internal_staging. Both commits retain the original author's attribution

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • The test file covering my change passes locally
  • My PR passes all required CI/CD checks
  • 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

Screenshots / Proof of Fix

A stable real-provider Before/After run is not included. Repeated Vertex requests returned empty content objects rather than omitting the content key, so the reported upstream shape did not recur

The focused regression suite passes with 149 tests:

149 passed in 0.57s

Type

🐛 Bug Fix

Caveats (if any)

Low

  • The provider response shape is nondeterministic and was not reproduced live

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

… no content

A content-less candidate (a thinking model that spends its whole token budget
on reasoning returns finishReason MAX_TOKENS with no content) was skipped,
leaving choices empty so callers hit IndexError on the first choice. Emit an
empty-content choice, mapping finishReason through the same path the mainline
uses; a missing finishReason is reported as length, never a false stop.
…endently

Assert each content-less candidate keeps its index and maps its own
finishReason (MAX_TOKENS -> length, SAFETY -> content_filter), so none is
dropped when several arrive together.
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR preserves non-streaming Gemini candidates that omit content as empty OpenAI-compatible choices and maps supplied provider finish reasons. It also adds focused tests for token truncation, missing finish reasons, and multiple content-less candidates.

Confidence Score: 4/5

The PR appears safe to merge after considering the non-blocking semantic issue that reason-less candidates are labeled as token-limit truncations.

The intended MAX_TOKENS and SAFETY mappings are preserved correctly, but the new fallback reports length without a provider finish reason and can therefore mislead downstream completion handling.

Files Needing Attention: litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py; tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py

Important Files Changed

Filename Overview
litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py Adds the intended empty-choice rescue, but the missing-finish-reason fallback overstates an unknown termination cause as token truncation.
tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py Adds focused regression coverage, including an assertion that currently codifies the unsupported length fallback for a reason-less candidate.

Reviews (1): Last reviewed commit: "test(vertex_ai): cover multiple content-..." | Re-trigger Greptile

Comment on lines +2225 to +2230
mapped_finish_reason = (
VertexGeminiConfig._check_finish_reason(None, finish_reason)
if finish_reason is not None
else "length"
)
empty_message: ChatCompletionResponseMessage = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unsupported truncation fallback

A content-less candidate without finishReason is labeled length even though no provider signal establishes token exhaustion. This conflates unknown or metadata-only responses with truncation, leading clients to retry, continue generation, or display incorrect token-limit messaging.

Knowledge Base Used: Provider adapters and capabilities

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing yukimaru77:fix-vertex-contentless-candidate-rebase (60b4409) with litellm_internal_staging (3e2927d)

Open in CodSpeed

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ codechrl
❌ Yukito Nonaka


Yukito Nonaka seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yuneng-berri
yuneng-berri deleted the branch BerriAI:litellm_internal_staging September 13, 2026 04:45
@yuneng-berri yuneng-berri reopened this Sep 13, 2026
JoaVirtudes19 added a commit to JoaVirtudes19/litellm that referenced this pull request Sep 14, 2026
…ates

A Gemini candidate with no content - an empty turn after a tool result
(finishReason STOP), a thinking model that spent its budget (MAX_TOKENS),
or a malformed function call - was skipped by the non-streaming
transformation, leaving choices empty. That is not a valid OpenAI
response: callers reading choices[0] hit IndexError on an HTTP 200 and
lose the reason the turn was empty.

Emit an empty-content choice carrying the mapped finish_reason, and keep
the provider's own reason in provider_specific_fields, since the OpenAI
enum has no equivalent for MALFORMED_FUNCTION_CALL. Streaming already
recovers these in _apply_stream_candidates and is left untouched.

Supersedes BerriAI#38301 and BerriAI#36870 (same approach, rebased with tests for the
STOP-after-tool-result and MALFORMED_FUNCTION_CALL shapes).

Fixes BerriAI#36881
JoaVirtudes19 added a commit to JoaVirtudes19/litellm that referenced this pull request Sep 16, 2026
…ates

A Gemini candidate with no content - an empty turn after a tool result
(finishReason STOP), a thinking model that spent its budget (MAX_TOKENS),
or a malformed function call - was skipped by the non-streaming
transformation, leaving choices empty. That is not a valid OpenAI
response: callers reading choices[0] hit IndexError on an HTTP 200 and
lose the reason the turn was empty.

Emit an empty-content choice carrying the mapped finish_reason, and keep
the provider's own reason in provider_specific_fields, since the OpenAI
enum has no equivalent for MALFORMED_FUNCTION_CALL. Streaming already
recovers these in _apply_stream_candidates and is left untouched.

Supersedes BerriAI#38301 and BerriAI#36870 (same approach, rebased with tests for the
STOP-after-tool-result and MALFORMED_FUNCTION_CALL shapes).

Fixes BerriAI#36881
@mateo-berri

Copy link
Copy Markdown
Contributor

Superseded by #41892, merged in ff7dc86, which keeps content-less Gemini candidates as a choice with the mapped finish_reason; thanks for the earlier fix

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.

Vertex/Gemini: content-less candidate (thinking model at MAX_TOKENS) yields empty choices -> IndexError on choices[0]

5 participants