Skip to content

fix(ingest): keep TypeScript ADK's Gemini thinking whole - #1173

Merged
JeremyFunk merged 1 commit into
mainfrom
fix/ingest-gemini-output-excludes-thinking
Sep 30, 2026
Merged

JeremyFunk merged 1 commit into
mainfrom
fix/ingest-gemini-output-excludes-thinking

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Usage::read (apps/ingest/src/ai_session/usage.rs) assumes the semconv convention, where the completion figure already contains the reasoning tokens. It clamps reasoning to the completion and subtracts it. An emitter whose completion is only the visible candidates loses its thinking. For example, output 100 + thoughts 300 is stamped as output 0, reasoning 100. EU prod verify2-adk-ts-synthetic shows this.

This PR adds a per-emitter "output excludes reasoning" rule next to input_excludes_cache. Like that rule, it is keyed on the emitter, never on gen_ai.provider.name. For an exclusive emitter, visible output = the completion and reasoning = the reasoning figure, with no clamp. Every other emitter keeps the clamp.

Which emitters exclude reasoning

The source shows that only one Gemini path found reports a completion without the thoughts.

Emitter output_tokens Evidence Rule
TypeScript ADK (@google/adk 2.1.0, latest) + the setup guide's AdkSpanProcessor candidatesTokenCount only; the processor adds thoughtsTokenCount as gen_ai.usage.reasoning.output_tokens and does not touch the output adk-js core/src/telemetry/tracing.ts (main): gen_ai.usage.output_tokens = usageMetadata.candidatesTokenCount; the guide's processor code; the prod synthetic span (output 100, reasoning 300) exclusive
Python ADK 2.6.2 / 2.10.0 (generate_content span) candidates_token_count + thoughts_token_count google/adk/telemetry/_token_usage.py: TokenUsage output = candidates + thoughts (2.10 docstring: "candidate plus reasoning"); reasoning.output_tokens = thoughts inclusive, clamp kept
opentelemetry-instrumentation-google-genai 1.2b0 candidates + thoughts generate_content.py ~L432: "candidates_token_count excludes thoughts; output_tokens must be the full output count including reasoning tokens" inclusive
openinference-instrumentation-google-genai 1.4.8 llm.token_count.completion = candidates + thoughts _utils.py _get_token_count_attributes_from_usage_metadata inclusive
langchain-google-genai (via LangChain instrumentations) candidates + thoughts chat_models.py ~L2052 inclusive
Vercel AI SDK 5 + @ai-sdk/google 2.0.100 candidates only outputTokens: candidatesTokenCount; reasoningTokens is not written to spans in ai@5 generate-text.ts not affected (no reasoning figure, so no clamp); the thinking is simply unreported
AI SDK 6+ @ai-sdk/google total = candidates + thoughts, text detail convert-google-usage.ts already read through ai.usage.outputTokenDetails.textTokens

The trace-capture recordings have no Gemini-native model call. All ADK captures route through OpenRouter (openrouter/... ids), so the Python ADK and Google GenAI evidence comes from source. On EU prod, the only Gemini spans that carry a reasoning figure are the two synthetic verification spans.

The rule

output_excludes_reasoning(vendor, span) returns vendor == "google_adk" && span.name == "call_llm".

Tests

  • google_adk_ts_call_llm_owns_usage: now carries the reasoning figure. Output 100 + thoughts 300 gives [600, 400, 0, 100, 300].
  • New, one per inclusive emitter, each built from the attributes its source writes. Each checks that the thinking comes out of the completion:
    • google_adk_python_gemini_output_includes_thoughts
    • otel_google_genai_output_includes_thoughts
    • openinference_google_genai_completion_includes_thoughts
  • The semconv clamp regressions still pass unchanged:
    • langchain_js_reasoning_is_clamped_to_the_completion
    • openai_agents_ts_reasoning_is_clamped_to_the_completion
    • vercel_v7_reasoning_past_the_completion_is_clamped
    • number_values_of_any_scalar_type
  • cargo test --lib ai_session: 82 passed. Clippy reports nothing new in usage.rs.

Merge order

This touches usage.rs next to #1170's change (is_model_call's call_llm arm). #1170 is already on main and this branch is cut from it, so there is nothing left to order.


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

Summary by CodeRabbit

  • Bug Fixes
    • Token usage reporting now handles Google ADK call_llm spans where completion counts already exclude reasoning tokens. Reasoning remains reported separately, and visible output totals are no longer reduced by subtracting those tokens again.
    • Other supported emitters retain their existing usage calculations, including cases where completion totals include reasoning tokens.

TypeScript ADK records Gemini's candidatesTokenCount as
gen_ai.usage.output_tokens and the Maple span processor adds
thoughtsTokenCount as the reasoning figure, so the completion excludes the
reasoning. Usage::read assumed the semconv convention and clamped reasoning to
the completion: 100 output + 300 thoughts became 0 output, 100 reasoning.
Every other Gemini emitter checked (Python ADK, the OTel and OpenInference
Google GenAI instrumentations, langchain-google-genai) reports candidates plus
thoughts and keeps the clamp.
@maple-review-bot

maple-review-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 4/5 · likely safe to merge
The rule rests on call_llm being the TS ADK model call and never Python ADK's; one synthetic prod span and the captures back that, not more.
quality 100/100 · no findings · tests covered · risk medium

Adds an output_excludes_reasoning rule so TypeScript ADK's Gemini call_llm keeps its thoughts out of the visible output instead of being clamped away, while every other emitter keeps the clamp. Contained to the ingest usage stamping, with tests on both sides.

  • output_excludes_reasoning is true only for vendor google_adk on span call_llm
  • Usage::read takes the flag and skips the reasoning clamp for that emitter
  • New tests cover the exclusive TS ADK call and three inclusive Gemini emitters
What was checked
  • Exclusive branch does not double count: output plus reasoning still equals the billed total (usage.rs:287)
  • A call_llm becomes the model call only through an inference op, so Python ADK's wrapper stays a wrapper (usage.rs:183)
  • Both Usage::read call sites updated; no other caller of the clamp exists (usage.rs:157, usage.rs:1749)

db32f23 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 432b1848-b1b5-45bf-aac0-1fec13a32686

📥 Commits

Reviewing files that changed from the base of the PR and between affb30b and db32f23.

📒 Files selected for processing (1)
  • apps/ingest/src/ai_session/usage.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Usage normalization now distinguishes Google ADK TypeScript call_llm spans, whose completion excludes reasoning, from other emitters, whose completion includes reasoning. The normalizer retains separate reasoning counts and derives visible output according to the applicable convention. Tests cover Python ADK, Google GenAI, and OpenInference Google GenAI.

Changes

Usage normalization

Layer / File(s) Summary
Emitter-specific normalization and tests
apps/ingest/src/ai_session/usage.rs
Usage::read now handles exclusive completion counts for Google ADK call_llm spans and inclusive completion counts for other emitters. Tests cover the distinct normalization results for TypeScript ADK, Python ADK, Google GenAI, and OpenInference Google GenAI.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to db32f

The change preserves TypeScript ADK output and reasoning counts separately while retaining existing normalization for other emitters. No concrete merge-blocking risk is established; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to db32f

The change is narrowly scoped to separating reported output and reasoning tokens. No introduced access-control or privilege issue was identified. Downstream use of these counts for billing or quota enforcement remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated effect is changed output/reasoning attribution on matching ingested spans. The inspected path shows no new authority or cross-service call, but the available relationships do not establish whether downstream billing or quota systems consume these buckets.

Trust Boundaries and Controls

  • observed — Emitter metadata selects arithmetic, not authentication or authorization. The model-call classification gate runs before normalization, and the exclusive rule is limited to one vendor/span-name combination. This predicate is a telemetry interpretation rule, not an identity control.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the ingest fix for TypeScript ADK Gemini reasoning tokens. It is concise and specific to the main change.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.98.1)

Clippy execution failed


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.

@JeremyFunk
JeremyFunk merged commit 2e91012 into main Sep 30, 2026
38 checks passed
@JeremyFunk
JeremyFunk deleted the fix/ingest-gemini-output-excludes-thinking branch September 30, 2026 12:00
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