Skip to content

feat(google): preserve detailed usage metadata - #403

Merged
Alex Z (CLowbrow) merged 1 commit into
mainfrom
abhi-feat-preserve-google-usage-details
Aug 5, 2026
Merged

feat(google): preserve detailed usage metadata#403
Alex Z (CLowbrow) merged 1 commit into
mainfrom
abhi-feat-preserve-google-usage-details

Conversation

@AbhiPrasad

@AbhiPrasad Abhijeet Prasad (AbhiPrasad) commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

Preserve detailed Google token usage without introducing provider-specific marker fields in the universal format.

This PR:

  • Adds nested input_details and output_details to UniversalUsage.
  • Adds reusable TokenBreakdown, InputTokenDetails, and OutputTokenDetails types.
  • Preserves provider-reported total_tokens, with an aggregate-derived fallback when absent.
  • Maps compatible cache, modality, reasoning, and total-token details across Google, OpenAI Chat Completions, OpenAI Responses, Anthropic, and Bedrock Converse.
  • Updates payload snapshots and expected differences for providers that cannot represent particular details.

Google token accounting

Google reports tool-use prompts and thoughts separately from its normal prompt and candidate counts:

prompt_tokens     = promptTokenCount + toolUsePromptTokenCount
completion_tokens = candidatesTokenCount + thoughtsTokenCount
reasoning_tokens  = thoughtsTokenCount
total_tokens      = totalTokenCount

The captured code-interpreter response demonstrates this accounting:

9 prompt + 96 tool prompt + 55 candidate + 32 thoughts = 192 total

When converting back to Google, the retained tool-prompt subset is subtracted from canonical prompt tokens and reasoning is subtracted from canonical completion tokens. This restores the native buckets without double-counting.

Universal representation

UniversalUsage
├── prompt_tokens
├── completion_tokens
├── total_tokens
├── input_details
│   ├── content_by_modality
│   ├── cached
│   ├── cache_creation
│   └── tool_prompt
└── output_details
    ├── content_by_modality
    └── reasoning

Cache, tool-prompt, and reasoning counts remain subsets of the aggregate input/output totals. Modality counts remain breakdowns rather than additional totals.

Validation

  • cargo test -p lingua — 1,216 passed
  • make test-payloads — 1,730 passed, 65 skipped
  • Cross-provider compatibility test passed
  • Local and branch typed-boundary checks passed
  • Current Braintrust main gateway compiled against this Lingua branch without Braintrust changes
  • codeInterpreterToolParam coverage: 80/91; remaining failures are existing Bedrock limitations

Google UsageMetadata reference: https://ai.google.dev/api/generate-content#UsageMetadata

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch 2 times, most recently from 427b09b to 12a0118 Compare July 31, 2026 19:47
@CLowbrow
Alex Z (CLowbrow) marked this pull request as ready for review August 4, 2026 17:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12a0118e29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

}

fn openai_chat_usage_view(usage: &Value) -> OpenAIChatUsageView {
serde_json::from_value::<OpenAIChatUsageView>(usage.clone()).unwrap_or_default()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate typed usage parse failures

When an otherwise valid Chat Completions usage object contains a malformed optional field such as "prompt_tokens_details": "invalid", deserializing the whole view fails and this fallback silently replaces every valid top-level counter with None; the previous field-by-field parser preserved prompt_tokens, completion_tokens, and total_tokens. Return a contextual error or model malformed optional details without discarding the rest of the usage.

AGENTS.md reference: AGENTS.md:L255-L259

Useful? React with 👍 / 👎.

Comment thread crates/coverage-report/src/responses_expected_differences.json
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 12a0118 to 9754e5f Compare August 4, 2026 18:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9754e5f196

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment on lines +587 to +589
prompt_tokens: Option<i64>,
completion_tokens: Option<i64>,
total_tokens: Option<i64>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid chat usage counters on parse errors

When a Chat Completions/Mistral usage payload has any malformed top-level counter (for example completion_tokens: "8" while prompt_tokens and total_tokens are valid), these plain Option<i64> fields make serde_json::from_value::<OpenAIChatUsageView> fail and the helper below returns Default, so all valid usage counters are silently dropped; the previous field-by-field parser would only ignore the bad field. Fresh evidence beyond the earlier optional-details issue is that the top-level counters still lack the tolerant deserialize_optional_i64 used for Anthropic, so please apply per-field tolerant parsing or return a contextual error instead.

AGENTS.md reference: AGENTS.md:L255-L259

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 9754e5f to 12ac79d Compare August 4, 2026 18:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12ac79db91

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

// In the universal format, completion_tokens includes reasoning (matching OpenAI convention).
// Google separates candidatesTokenCount and thoughtsTokenCount, so we add them.
completion_tokens: Some(candidates + thoughts),
total_tokens: usage.total_token_count,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a fallback for missing Google totals

When a Google response omits totalTokenCount but still includes promptTokenCount, toolUsePromptTokenCount, candidatesTokenCount, or thoughtsTokenCount (all modeled as optional in UsageMetadata), this leaves UniversalUsage.total_tokens as None even though the aggregate is computable from the component counts. Other provider parsers added in this change fall back to prompt plus completion, so callers that serialize or inspect the universal response directly will lose the total only for Google payloads with an absent total; compute the same fallback here after deriving the inclusive prompt and completion counts.

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 12ac79d to 4d97054 Compare August 4, 2026 19:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d97054287

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment thread crates/lingua/src/universal/response.rs Outdated
}),
ProviderFormat::Converse => {
let mut map = serde_json::Map::new();
map.insert("inputTokens".into(), serde_json::json!(provider_prompt));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit exclusive Converse inputTokens with cache buckets

When a universal usage object comes from OpenAI/Responses, prompt_tokens is already inclusive of cached/cache-write tokens; this line still writes that inclusive value as Converse inputTokens while the new code below also emits cacheReadInputTokens/cacheWriteInputTokens. The Converse importer treats those cache buckets as excluded from inputTokens and adds them back, so a Chat/Responses → Bedrock → OpenAI roundtrip double-counts cached prompt tokens; use exclusive_prompt_tokens() for Converse inputTokens before emitting cache fields.

AGENTS.md reference: AGENTS.md:L184-L189

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 4d97054 to ff361db Compare August 4, 2026 19:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff361dbead

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment on lines +809 to +812
cache_creation: usage_token_breakdown(
input_details.cache_write_tokens,
None,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the Responses→Google streaming cache-write exception

When a Responses streaming event includes input_tokens_details.cache_write_tokens, this new universal input_details.cache_creation field is populated, but the Google streaming serializer has no cache-write usage field and drops it. The non-streaming expected-difference file accounts for ResponsesGoogle, but streaming_expected_differences.json still only whitelists usage.prompt_cache_creation_tokens, so the cross-provider streaming guard will report an unexpected usage.input_details.cache_creation diff for those cases; add the matching narrow streaming expected difference or preserve the field through Google.

AGENTS.md reference: AGENTS.md:L142-L146

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch 2 times, most recently from 406219d to 5db4064 Compare August 4, 2026 21:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5db4064c9b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment thread crates/lingua/src/universal/response.rs Outdated
Comment on lines 1181 to 1183
if inclusive_prompt.is_some() || self.completion_tokens.is_some() {
map.insert(
"totalTokenCount".into(),
serde_json::json!(prompt + completion),
);
map.insert("totalTokenCount".into(), serde_json::json!(total));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve detail-backed Google usage fields

When callers use the public UniversalUsage::to_provider_value(ProviderFormat::Google) with a usage value restored from the new universal JSON shape that only has input_details/output_details, this Google arm still only writes the aggregate totals and later reads legacy fields such as self.prompt_cached_tokens/self.completion_reasoning_tokens, so cachedContentTokenCount, thoughtsTokenCount, and modality/detail buckets are dropped. The Google adapter uses the new UsageMetadata::from(self) path, but this public conversion helper should do the same or reuse the detail-aware locals already computed above.

AGENTS.md reference: AGENTS.md:L11-L13

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 5db4064 to 2c72543 Compare August 4, 2026 22:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c72543396

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment on lines +1197 to +1200
if let Some(cache_write) = cache_creation_tokens {
map.insert(
"cacheWriteInputTokens".into(),
serde_json::json!(cache_write),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Converse cache buckets in streaming output

Fresh evidence beyond the earlier Converse usage comment is that the current Bedrock streaming serializer still deserializes usage.to_provider_value(...) into BedrockTokenUsage at crates/lingua/src/providers/bedrock/adapter.rs:701, and that type only has input/output/total fields at crates/lingua/src/providers/bedrock/response.rs:229, so usage-only streaming chunks with prompt_cached_tokens or prompt_cache_creation_tokens discard the cacheReadInputTokens/cacheWriteInputTokens inserted here before the metadata event is serialized. Non-streaming Bedrock now preserves these buckets, but streaming Bedrock roundtrips still lose cache usage; extend the Bedrock streaming usage type/path to carry the new fields.

AGENTS.md reference: AGENTS.md:L11-L13

Useful? React with 👍 / 👎.

@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from 2c72543 to a9665b9 Compare August 4, 2026 22:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

finish_reason: Some("stop".to_string()),

P2 Badge Do not hard-code stop on metrics-only chunks

For Bedrock Anthropic streams that end with tool_use or max_tokens, the captured stream already carries that stop reason on the preceding message_delta while the following message_stop only adds amazon-bedrock-invocationMetrics (e.g. payloads/snapshots/toolCallRequest/bedrock-anthropic/response-streaming.json:96-109). This new metrics branch emits a second terminal choice with finish_reason: "stop", so OpenAI/Responses transforms can overwrite/report the final reason as stop and duplicate usage; make the metrics event a usage-only chunk or preserve the actual prior stop reason instead.

AGENTS.md reference: AGENTS.md:L11-L11

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Google reports aggregate usage, per-modality counts, and a separate tool-use prompt bucket. Tool
prompt tokens are input tokens but are not included in `promptTokenCount`, even though they are
included in `totalTokenCount`:

    prompt_tokens     = promptTokenCount + toolUsePromptTokenCount
    completion_tokens = candidatesTokenCount + thoughtsTokenCount
    reasoning_tokens  = thoughtsTokenCount
    total_tokens      = prompt_tokens + completion_tokens

A captured code-interpreter response demonstrates the accounting:

    9 prompt + 96 tool prompt + 55 candidate + 32 thoughts = 192 total

Preserve the complete breakdown through provider-independent modality types and tool-prompt fields:

    Google UsageMetadata
      ├── promptTokensDetails[]
      ├── cacheTokensDetails[]
      ├── candidatesTokensDetails[]
      └── toolUsePromptTokenCount + details[]
                    │
                    ▼
             UniversalUsage

Subtract the retained tool-prompt subset when converting back to Google so both native fields
roundtrip without double-counting. Derive UniversalUsage serialization so all fields have one source
of truth, and document intentional losses for providers without equivalent usage fields.

Google UsageMetadata reference:
https://ai.google.dev/api/generate-content#UsageMetadata
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) force-pushed the abhi-feat-preserve-google-usage-details branch from a9665b9 to 3a9fcce Compare August 4, 2026 22:46
@CLowbrow
Alex Z (CLowbrow) merged commit 8b0330b into main Aug 5, 2026
7 checks passed
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.

2 participants