Skip to content

fix: count toolUsePrompt as input and normalize stream/chat usage - #2947

Closed
seefs001 wants to merge 1 commit into
QuantumNous:mainfrom
seefs001:fix/gemini-usage-tool-use
Closed

fix: count toolUsePrompt as input and normalize stream/chat usage#2947
seefs001 wants to merge 1 commit into
QuantumNous:mainfrom
seefs001:fix/gemini-usage-tool-use

Conversation

@seefs001

@seefs001 seefs001 commented Feb 13, 2026

Copy link
Copy Markdown
Collaborator

fix #2935
input = promptTokenCount + toolUsePromptTokenCount
output = candidatesTokenCount + thoughtsTokenCount / totalTokenCount - input
针对流和非流两种情况

Summary by CodeRabbit

  • Bug Fixes

    • Improved token usage tracking and calculation from Gemini API responses with enhanced fallback logic.
    • Fixed token count aggregation to properly exclude tool-use tokens and ensure accurate completion token computation.
  • Tests

    • Added comprehensive test coverage for Gemini usage metadata handling and token calculation scenarios.

@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds tool-use token tracking fields to GeminiUsageMetadata DTO, introduces helper functions to centralize usage construction from Gemini response metadata, and implements fallback logic for token count derivation across response handlers.

Changes

Cohort / File(s) Summary
DTO Extensions
dto/gemini.go
Added ToolUsePromptTokenCount and ToolUsePromptTokensDetails fields to track tool-use token counts separately in usage metadata.
Usage Helper Functions
relay/channel/gemini/usage.go
New file introducing hasGeminiUsageMetadata() and buildUsageFromGeminiMetadata() to centralize token extraction and usage object construction with fallback logic for missing counts, including clamping and modality-based token mapping.
Usage Tests
relay/channel/gemini/usage_test.go
New test file validating buildUsageFromGeminiMetadata() behavior: tool-use exclusion, fallback aggregation, clamping of negative values, and detail field mapping.
Response Handler Refactoring
relay/channel/gemini/relay-gemini-native.go, relay/channel/gemini/relay-gemini.go
Replaced manual usage metric construction with calls to buildUsageFromGeminiMetadata(). Enhanced token/usage logic with fallback assignments: TextTokens fallback to PromptTokens, TotalTokens recomputed from prompt and completion when missing, CompletionTokens derived robustly across empty-candidates and final response paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

Poem

🐰 With whiskers twitched and tokens bright,
Tool-use counts now sorted right,
No more billing chaos here,
Usage flows crystal clear!

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly identifies the main fix: counting toolUsePrompt tokens as input and normalizing usage calculation across streaming and non-streaming handlers.
Linked Issues check ✅ Passed Changes correctly implement the fix for issue #2935: toolUsePromptTokenCount is now counted as input tokens, and usage formulas are normalized for both stream/chat scenarios.
Out of Scope Changes check ✅ Passed All changes are focused on fixing Gemini token usage calculation and introducing supporting helper functions; no unrelated modifications were introduced.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@relay/channel/gemini/usage_test.go`:
- Around line 26-33: buildUsageFromGeminiMetadata currently sets promptTokens :=
meta.PromptTokenCount but never adds meta.ToolUsePromptTokenCount and never
iterates meta.ToolUsePromptDetails, so PromptTokens and
PromptTokensDetails.TextTokens/CachedTokens are missing tool-use contributions;
update buildUsageFromGeminiMetadata to add meta.ToolUsePromptTokenCount into the
promptTokens total (so usage.PromptTokens = meta.PromptTokenCount +
meta.ToolUsePromptTokenCount) and iterate meta.ToolUsePromptDetails (similar to
how meta.PromptTokensDetails are processed) to accumulate TEXT into
PromptTokensDetails.TextTokens and CACHED into PromptTokensDetails.CachedTokens
(and any other relevant token categories) before assigning to
usage.PromptTokensDetails and usage.PromptTokens.

In `@relay/channel/gemini/usage.go`:
- Around line 16-57: In buildUsageFromGeminiMetadata, promptTokens is set from
meta.PromptTokenCount but never includes meta.ToolUsePromptTokenCount and
meta.ToolUsePromptDetails are never applied; update the promptTokens calculation
to add meta.ToolUsePromptTokenCount (when present) and iterate
meta.ToolUsePromptDetails to accumulate their TokenCount into
usage.PromptTokensDetails (e.g., a ToolUseTokens field or add into modality
buckets) and into promptTokens so the input = promptTokenCount +
toolUsePromptTokenCount invariant holds; adjust the existing loop over
meta.PromptTokensDetails to also incorporate tool-use modality breakdowns (map
AUDIO/TEXT to usage.PromptTokensDetails.AudioTokens/TextTokens or to a
ToolUseTokens field) and ensure downstream derived values (InputTokens,
PromptTokensDetails.TextTokens fallback) reflect the added tool-use tokens.
🧹 Nitpick comments (1)
relay/channel/gemini/relay-gemini.go (1)

1287-1289: Minor: TextTokens fallback is duplicated from buildUsageFromGeminiMetadata.

Lines 55–57 of usage.go already apply the same TextTokens = PromptTokens fallback. This code is harmless but redundant in the streaming path since *usage = latestUsage already includes the fallback result.

Comment on lines +26 to +33
usage := buildUsageFromGeminiMetadata(meta, 0)

require.Equal(t, 18480, usage.PromptTokens)
require.Equal(t, 2209, usage.CompletionTokens)
require.Equal(t, 20689, usage.TotalTokens)
require.Equal(t, 1120, usage.CompletionTokenDetails.ReasoningTokens)
require.Equal(t, 17, usage.PromptTokensDetails.CachedTokens)
require.Equal(t, 18480, usage.PromptTokensDetails.TextTokens)

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.

⚠️ Potential issue | 🔴 Critical

Tests will fail: buildUsageFromGeminiMetadata does not add ToolUsePromptTokenCount to PromptTokens.

Tests 1–3 all expect PromptTokens = PromptTokenCount + ToolUsePromptTokenCount, but usage.go line 17 only sets promptTokens := meta.PromptTokenCount without adding meta.ToolUsePromptTokenCount. For example, test 1 expects PromptTokens=18480 (151+18329) but the code will produce 151.

Similarly, Line 33 expects TextTokens=18480, but the code only iterates meta.PromptTokensDetails (yielding TEXT=151) and the fallback on line 55–57 of usage.go won't trigger since TextTokens != 0. The ToolUsePromptDetails slice is never iterated.

The root cause is in usage.go — see the comment there for the proposed fix.

🤖 Prompt for AI Agents
In `@relay/channel/gemini/usage_test.go` around lines 26 - 33,
buildUsageFromGeminiMetadata currently sets promptTokens :=
meta.PromptTokenCount but never adds meta.ToolUsePromptTokenCount and never
iterates meta.ToolUsePromptDetails, so PromptTokens and
PromptTokensDetails.TextTokens/CachedTokens are missing tool-use contributions;
update buildUsageFromGeminiMetadata to add meta.ToolUsePromptTokenCount into the
promptTokens total (so usage.PromptTokens = meta.PromptTokenCount +
meta.ToolUsePromptTokenCount) and iterate meta.ToolUsePromptDetails (similar to
how meta.PromptTokensDetails are processed) to accumulate TEXT into
PromptTokensDetails.TextTokens and CACHED into PromptTokensDetails.CachedTokens
(and any other relevant token categories) before assigning to
usage.PromptTokensDetails and usage.PromptTokens.

Comment on lines +16 to +57
func buildUsageFromGeminiMetadata(meta dto.GeminiUsageMetadata, estimatedPromptTokens int) dto.Usage {
promptTokens := meta.PromptTokenCount
if promptTokens <= 0 {
promptTokens = estimatedPromptTokens
}

completionTokens := meta.CandidatesTokenCount + meta.ThoughtsTokenCount
if completionTokens <= 0 && meta.TotalTokenCount > 0 {
completionTokens = meta.TotalTokenCount - meta.PromptTokenCount - meta.ToolUsePromptTokenCount
}
if completionTokens < 0 {
completionTokens = 0
}

totalTokens := meta.TotalTokenCount
if totalTokens <= 0 {
totalTokens = promptTokens + completionTokens
}

usage := dto.Usage{
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TotalTokens: totalTokens,
InputTokens: promptTokens,
OutputTokens: completionTokens,
}

usage.CompletionTokenDetails.ReasoningTokens = meta.ThoughtsTokenCount
usage.PromptTokensDetails.CachedTokens = meta.CachedContentTokenCount

for _, detail := range meta.PromptTokensDetails {
switch detail.Modality {
case "AUDIO":
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
case "TEXT":
usage.PromptTokensDetails.TextTokens = detail.TokenCount
}
}

if usage.PromptTokensDetails.TextTokens == 0 && usage.PromptTokens > 0 {
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
}

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.

⚠️ Potential issue | 🔴 Critical

Critical: ToolUsePromptTokenCount is never added to promptTokens, defeating the purpose of this PR.

The PR's stated objective is input = promptTokenCount + toolUsePromptTokenCount, but line 17 only assigns meta.PromptTokenCount without adding meta.ToolUsePromptTokenCount. Additionally, meta.ToolUsePromptDetails is never iterated, so tool-use modality breakdowns are lost.

All three tests that include non-zero ToolUsePromptTokenCount will fail.

🐛 Proposed fix
 func buildUsageFromGeminiMetadata(meta dto.GeminiUsageMetadata, estimatedPromptTokens int) dto.Usage {
 	promptTokens := meta.PromptTokenCount
 	if promptTokens <= 0 {
 		promptTokens = estimatedPromptTokens
 	}
+	promptTokens += meta.ToolUsePromptTokenCount
 
 	completionTokens := meta.CandidatesTokenCount + meta.ThoughtsTokenCount
 	if completionTokens <= 0 && meta.TotalTokenCount > 0 {
 		completionTokens = meta.TotalTokenCount - meta.PromptTokenCount - meta.ToolUsePromptTokenCount
 	}
 	if completionTokens < 0 {
 		completionTokens = 0
 	}
 
 	totalTokens := meta.TotalTokenCount
 	if totalTokens <= 0 {
 		totalTokens = promptTokens + completionTokens
 	}
 
 	usage := dto.Usage{
 		PromptTokens:     promptTokens,
 		CompletionTokens: completionTokens,
 		TotalTokens:      totalTokens,
 		InputTokens:      promptTokens,
 		OutputTokens:     completionTokens,
 	}
 
 	usage.CompletionTokenDetails.ReasoningTokens = meta.ThoughtsTokenCount
 	usage.PromptTokensDetails.CachedTokens = meta.CachedContentTokenCount
 
 	for _, detail := range meta.PromptTokensDetails {
 		switch detail.Modality {
 		case "AUDIO":
 			usage.PromptTokensDetails.AudioTokens = detail.TokenCount
 		case "TEXT":
 			usage.PromptTokensDetails.TextTokens = detail.TokenCount
 		}
 	}
+	for _, detail := range meta.ToolUsePromptDetails {
+		switch detail.Modality {
+		case "AUDIO":
+			usage.PromptTokensDetails.AudioTokens += detail.TokenCount
+		case "TEXT":
+			usage.PromptTokensDetails.TextTokens += detail.TokenCount
+		}
+	}
 
 	if usage.PromptTokensDetails.TextTokens == 0 && usage.PromptTokens > 0 {
 		usage.PromptTokensDetails.TextTokens = usage.PromptTokens
 	}
 
 	return usage
 }
🤖 Prompt for AI Agents
In `@relay/channel/gemini/usage.go` around lines 16 - 57, In
buildUsageFromGeminiMetadata, promptTokens is set from meta.PromptTokenCount but
never includes meta.ToolUsePromptTokenCount and meta.ToolUsePromptDetails are
never applied; update the promptTokens calculation to add
meta.ToolUsePromptTokenCount (when present) and iterate
meta.ToolUsePromptDetails to accumulate their TokenCount into
usage.PromptTokensDetails (e.g., a ToolUseTokens field or add into modality
buckets) and into promptTokens so the input = promptTokenCount +
toolUsePromptTokenCount invariant holds; adjust the existing loop over
meta.PromptTokensDetails to also incorporate tool-use modality breakdowns (map
AUDIO/TEXT to usage.PromptTokensDetails.AudioTokens/TextTokens or to a
ToolUseTokens field) and ensure downstream derived values (InputTokens,
PromptTokensDetails.TextTokens fallback) reflect the added tool-use tokens.

@seefs001 seefs001 closed this Feb 13, 2026
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.

Gemini 的 tooluse token 计费错误

1 participant