Module: OmniAI::Google::Chat::UsageSerializer

Defined in:
lib/omniai/google/chat/usage_serializer.rb

Overview

Overrides usage serialize / deserialize.

Class Method Summary collapse

Class Method Details

.deserialize(data) ⇒ OmniAI::Chat::Usage?

Returns nil when the payload carries no token counts at all. A truncated stream still assembles a usageMetadata — Gemini sends one on every chunk, carrying only trafficType until the terminal chunk — so the presence of the key is not the presence of usage. Building a Usage from it would report every count as nil, which arithmetic downstream silently turns into zero.

The test is strictly "no count is present", never "the counts are falsy": a reported 0 is a count.

Parameters:

  • data (Hash)

Returns:

  • (OmniAI::Chat::Usage, nil)


42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/omniai/google/chat/usage_serializer.rb', line 42

def self.deserialize(data, *)
  input_tokens = data["promptTokenCount"]
  candidates_tokens = data["candidatesTokenCount"]
  thinking_tokens = data["thoughtsTokenCount"]
  total_tokens = data["totalTokenCount"]

  return if [input_tokens, candidates_tokens, thinking_tokens, total_tokens].all?(&:nil?)

  output_tokens = (candidates_tokens || 0) + (thinking_tokens || 0) if candidates_tokens || thinking_tokens

  OmniAI::Chat::Usage.new(input_tokens:, output_tokens:, total_tokens:, thinking_tokens:)
end

.serialize(usage) ⇒ Hash

Gemini reports thinking separately from the answer: candidatesTokenCount covers the answer only, while thoughtsTokenCount covers internal reasoning. Google bills both as output and totalTokenCount includes both, so output_tokens is the sum — matching Anthropic and OpenAI, where reasoning is already folded into the output count and reported back only as a breakdown.

Parameters:

  • usage (OmniAI::Chat::Usage)

Returns:

  • (Hash)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/omniai/google/chat/usage_serializer.rb', line 15

def self.serialize(usage, *)
  thinking_tokens = usage.thinking_tokens
  candidates_tokens = usage.output_tokens
  # `thinking_tokens` is a subset of `output_tokens`, so this cannot go negative for any Usage this gem
  # builds. Clamp anyway: a hand-constructed Usage that violates the subset invariant should not produce a
  # negative token count on the wire.
  candidates_tokens = [candidates_tokens - thinking_tokens, 0].max if candidates_tokens && thinking_tokens

  data = {
    promptTokenCount: usage.input_tokens,
    candidatesTokenCount: candidates_tokens,
    totalTokenCount: usage.total_tokens,
  }
  # Gemini omits the key entirely when nothing was thought; only emit it when there is a value to report.
  data[:thoughtsTokenCount] = thinking_tokens unless thinking_tokens.nil?
  data
end