Skip to content

fix(vertexai): include thinking tokens in ChatVertexAI output_tokens - #1932

Open
kurniawan (codechrl) wants to merge 2 commits into
langchain-ai:mainfrom
codechrl:fix/vertexai-gemini-thought-tokens-in-output-tokens
Open

fix(vertexai): include thinking tokens in ChatVertexAI output_tokens#1932
kurniawan (codechrl) wants to merge 2 commits into
langchain-ai:mainfrom
codechrl:fix/vertexai-gemini-thought-tokens-in-output-tokens

Conversation

@codechrl

Copy link
Copy Markdown

Why

_get_usage_metadata_gemini (libs/vertexai/langchain_google_vertexai/chat_models.py) sets output_tokens from candidates_token_count alone:

output_tokens = raw_metadata.get("candidates_token_count", 0)
...
thought_tokens = raw_metadata.get("thoughts_token_count", 0)
...
if thought_tokens > 0:
    return UsageMetadata(
        input_tokens=input_tokens,
        output_tokens=output_tokens,       # thinking tokens not included
        total_tokens=total_tokens,         # but total_tokens does include them
        ...
    )

total_token_count from the raw Gemini API response already includes thinking tokens, so for any thinking-enabled Gemini call (gemini-2.5-flash/-pro) through ChatVertexAI, input_tokens + output_tokens != total_tokens — breaking the UsageMetadata contract — and anything pricing off output_tokens undercounts completion cost.

The sibling ChatGoogleGenerativeAI._response_to_result (libs/genai/langchain_google_genai/chat_models.py) in this same repo already handles this correctly:

output_tokens = (
    response.usage_metadata.candidates_token_count or 0
) + thought_tokens

What

Fold thought_tokens into output_tokens in _get_usage_metadata_gemini, mirroring the working genai implementation. output_token_details={"reasoning": ...} is unchanged — it already correctly reports the reasoning subset.

Tests

Added test_get_usage_metadata_gemini_includes_thought_tokens_in_output_tokens to tests/unit_tests/test_chat_models.py, asserting output_tokens folds in thinking tokens and input_tokens + output_tokens == total_tokens. Confirmed test-first: fails on main (5 == 25), passes with the fix. Full test_chat_models.py: 116 passed. ruff check/ruff format --check clean.

Areas worth a careful look

  • _get_usage_metadata_gemini is used by both the non-streaming (_gemini_response_to_chat_result) and streaming (_gemini_chunk_to_generation_chunk) paths, so this fixes both at once.
  • The non-thinking branch (thought_tokens == 0) is untouched — output_tokens there was already correct.

Disclaimer: this PR was prepared with the assistance of an AI agent (Claude Code). All code and test changes were reviewed by the author before submission.

kurniawan added 2 commits August 7, 2026 16:55
_get_usage_metadata_gemini set output_tokens from candidates_token_count
alone, leaving out thoughts_token_count even though total_tokens (the raw
API's total) does include it. For any thinking-enabled Gemini call through
ChatVertexAI, this breaks the UsageMetadata contract that
input_tokens + output_tokens == total_tokens, and undercounts completion
cost for anything priced off output_tokens.

ChatGoogleGenerativeAI's _response_to_result in the same repo already adds
thought_tokens into output_tokens correctly; mirror that here.

Added a unit test asserting output_tokens folds in thinking tokens and that
input_tokens + output_tokens == total_tokens holds.
…okens

The integration tests assumed output_tokens excludes reasoning tokens and
added them again when checking total_tokens. Per the UsageMetadata contract,
output_tokens already includes reasoning tokens, so total_tokens must equal
input_tokens + output_tokens exactly, not exceed it. These tests encoded the
same bug this PR's source fix addresses.

Verified live against gemini-2.5-flash on Vertex AI: all 6 thinking-related
tests pass, including the two negative controls (thinking_budget=0) that
were already using the correct assertion.
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