Token tracking - #174
Conversation
Wire onEventCallback and afterRunCallback into Runner.runLiveImpl so the plugin lifecycle runs for live/bidi sessions, matching the non-live path. Previously live events were appended to the session but never passed through any plugin hook, so token usage emitted by Gemini Live (including audio modality breakdowns) could not be captured by plugins. Add LiveTokenTrackingPlugin which observes usageMetadata on each event and logs per-invocation totals on run completion. Live reports cumulative counts, so the plugin keeps the latest value per field rather than summing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWJMsUqoQgsxLScGRk7uB2
Runnable example that drives a Gemini Live BIDI session with LiveTokenTrackingPlugin registered, demonstrating that usageMetadata (including audio modality breakdown) reaches onEventCallback and that afterRunCallback receives the aggregated per-session totals. Requires GOOGLE_API_KEY; model selectable via -Dmodel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWJMsUqoQgsxLScGRk7uB2
Log every usageMetadata event with a sequence number and run two turns in one session. Empirically Gemini Live emits exactly one usageMetadata event per turn, and each event reports that turn's own totals (total = prompt + candidates) rather than a session-cumulative running total. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWJMsUqoQgsxLScGRk7uB2
A live two-turn session showed Gemini Live emits one usageMetadata event per turn, each reporting that turn's own usage (total = prompt + candidates) rather than a session-cumulative running total. Keeping the latest value therefore dropped all earlier turns and undercounted multi-turn sessions. Accumulate by summing scalar counts and per-modality token counts across events so the aggregate reflects true session totals. Verified end-to-end against a live session: per-turn totals 892 and 1044 sum to 1936. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWJMsUqoQgsxLScGRk7uB2
…s/adk-java into token_tracking
Sandeep-BA
left a comment
There was a problem hiding this comment.
@alfredjimmy-redbus : Pls validate and update the review comments.
| return Optional.empty(); | ||
| } else if (message.goAway().isPresent()) { | ||
| logger.debug("Received go away: {}", message.goAway().get()); | ||
| return Optional.empty(); |
There was a problem hiding this comment.
Should we be really sending optional.empty on goAway or voiceactivity detectionSignal()?
There was a problem hiding this comment.
Yes, keeping them as Optional.empty() is the right approach for this specific layer (convertToServerResponse).
This method's job is to map server events into client-facing responses (like content, tools, and usage). Because goAway and voiceActivity are infrastructure/connection-level signals, they don't have a representation in the client response model.
If we don't return Optional.empty(), we'd have to map them to an error event, which we previously found was incorrect and causing issues.
| () -> { | ||
| Usage usage = usageByInvocation.remove(invocationContext.invocationId()); | ||
| if (usage == null) { | ||
| return; |
There was a problem hiding this comment.
Can we check what this return and return type is completable?
There was a problem hiding this comment.
afterRunCallback always returns a Completable.
The inner return just means “skip logging” — e.g. live session ended with no usageMetadata events, so nothing was accumulated.
|
|
||
| @Override | ||
| public synchronized String toString() { | ||
| return "Usage{" |
There was a problem hiding this comment.
Should we really create a new class Usage?
There was a problem hiding this comment.
Removed LiveTokenTrackingPlugin since this plugin can only log the usage, and moved plugin to sdk for persisting in DB
c4c91b3 to
bcb703f
Compare
ADK-Java changes summary
Branch / context
Branch:
token_tracking(merge ofclaude/live-bidi-token-tracking)Purpose: Enable live BIDI (Gemini Live) token tracking via plugins, and fix live WebSocket message handling so usage events are not masked by spurious errors.
1.
Runner.runLive— plugin callbacks for live sessionsFile:
core/src/main/java/com/google/adk/runner/Runner.javaProblem: Non-live runs already invoked
onEventCallback/afterRunCallback;runLivedid not. Plugins likeRedBusTrackingPluginandLiveTokenTrackingPluginnever saw live events, so BIDI token usage was never tracked.Change: In
runLiveImpl, for each live event:pluginManager.onEventCallback(invocationContext, event)— per event (e.g. eachusageMetadataturn)pluginManager.afterRunCallback(invocationContext)— once when the live stream completesMirrors the existing non-live
runImplpath. Session persistence (appendEvent) is unchanged; plugin callbacks are observational only.2.
GeminiLlmConnection— handle new Live API control messagesFile:
core/src/main/java/com/google/adk/models/GeminiLlmConnection.javaProblem:
google-genai1.58+ delivers control messages (sessionResumptionUpdate,goAway, voice-activity signals) that ADK did not recognize. They fell through to the “unknown server message” branch, producing errorLlmResponses and fake ADK events — observed in RAE as event#123withhasContent=falseright after:Change: Treat these as no-op control messages (return
Optional.empty(), debug log only):sessionResumptionUpdategoAwayvoiceActivityDetectionSignalvoiceActivityUnchanged: Standalone
usageMetadatamessages still becomeLlmResponses with usage attached (existing behavior at lines 189–197).3. Test coverage
File:
core/src/test/java/com/google/adk/models/GeminiLlmConnectionTest.javaconvertToServerResponse_withSessionResumptionUpdate_returnsEmpty— confirms resumption updates do not emit error events.Existing tests still cover:
usageMetadataWhy this matters for RAE / BIDI voice
Dependency: Requires
google-genai≥ 1.58.0 (RAE already on 1.58.0).Downstream (not in ADK):
genai-sdkRedBusTrackingPluginpersists per-event usage torae_tokens;LiveAudioSessionlogsUSAGE_METADATAfor confirmation.