Skip to content

fix(gateway): storage cost double-billed reasoning - #3574

Closed
steebchen wants to merge 9 commits into
mainfrom
flex-tier-output-token-cost
Closed

steebchen wants to merge 9 commits into
mainfrom
flex-tier-output-token-cost

Conversation

@steebchen

@steebchen steebchen commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3486 — until that merges, this PR's diff also shows #3486's commits; the delta belonging to this PR is the single top commit (fix(gateway): provider-aware data storage cost).

Problem

calculateDataStorageCost sums prompt + completion + reasoning tokens unconditionally. But for most providers (OpenAI, Azure, Google, aws-mantle, Sakana, Meta, RanoAI) the completion/output token count already contains reasoning tokens — the reported reasoning_tokens is an informational subset, not an additional count. Whenever a reasoning count reached one of the storage call sites, retention storage billed those tokens twice.

This is the storage-cost sibling of the usage.total_tokens double-count that #3486 fixes: same root cause (derived numbers adding reasoning on top of an already-inclusive completion count), different consumer.

Approach

  • Thread the used provider into calculateDataStorageCost / toDataStorageCostNumber and every chat call site that has a reasoning count in scope.
  • Inside the calculation, skip the reasoning add-on when completionIncludesReasoning() (exported by fix(gateway): properly track reasoning tokens #3486) says the completion count already contains it. Providers that genuinely report reasoning outside the completion count (e.g. xAI) keep the additive behavior.
  • Callers without a provider in scope keep the legacy behavior, so nothing changes for paths that never had a reasoning count.
  • Docs: note on the data-retention page that reasoning tokens are only added separately for providers that report them outside the output count.

Verification

  • Live against real OpenAI (gpt-5.6-sol, flex tier, reasoning enabled), streaming and non-streaming, on /v1/chat/completions and /v1/responses: the log row and client-visible usage now agree end-to-end — e.g. 30 prompt + 142 completion (135 reasoning inside) → total_tokens 172, data_storage_cost computed on 172 (previously 307), output cost unchanged at the flex rate.
  • New unit tests pin both semantics: inclusive provider (openai) does not re-add reasoning; additive provider (xai) still does.
  • pnpm build and the gateway logs/costs spec suites pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Corrected token usage and cost calculations across providers.
    • Prevented reasoning tokens from being counted twice when already included in completion totals.
    • Added accurate fallback totals when providers omit overall token counts.
    • Updated streaming, cached, cancelled, and standard chat responses for consistent usage reporting.
  • Documentation

    • Clarified when reasoning tokens are billed separately from output tokens.

claude and others added 6 commits August 8, 2026 12:53
Output costs came out high on Anthropic reasoning requests. In costs.ts
the completionIncludesReasoning allow-list covered Google and the
OpenAI-style Responses providers but not anthropic/vertex-anthropic, so
those fell through to completionTokens + reasoningTokens — adding
reasoning that is already inside completionTokens.

extract-token-usage reads Anthropic's output_tokens straight into
completionTokens and builds totalTokens as prompt + output without
re-adding reasoning; its spec pins this (51 in, 136 out incl. 31
reasoning = 187). The extract layer and the cost layer disagreed; this
lines them up.

Verified against real upstream (Vertex Anthropic, claude-sonnet-4-5):
raw usage returned output_tokens 72 with thinking_tokens 65 for a
one-token text answer, confirming output_tokens includes thinking.
Before the fix a live request billed 179 output tokens for 93 completion
tokens; after, output cost is exactly completionTokens * price.

aws-bedrock bundles reasoning into outputTokens the same way and has no
separate reasoning source, so it is already correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6eFuuNRtA7bogrRtAHAYV
The reported total_tokens added reasoning on top of a completion count
that, for several providers, already contained it. A live Vertex
Anthropic request came back as prompt 64 + completion 135 + reasoning 78
= 277, when completion already included the reasoning and the total
should have been 199.

This contradicted extract-token-usage, whose spec pins the Anthropic
total at 51 + 136 = 187 rather than 218. The extract layer was right and
every downstream total-tokens site re-added reasoning behind its back.

Extract the provider predicate the cost path already used into an
exported completionIncludesReasoning(), add sumTotalTokens() alongside
it, and route every total-tokens site through it: the five in chat.ts
(cached streaming log, streaming fallback, streaming usage chunk,
non-streaming prompt-delta recompute, non-streaming response), the
cached-response log entry, the three parse-provider-response fallbacks,
and the transform-response-to-openai fallback.

This affects every provider whose completion count already includes
reasoning — Anthropic, Vertex Anthropic, Google, OpenAI, Azure, RanoAI,
Sakana, Meta, aws-mantle — not just Anthropic. Billing was already
correct; this only fixes the totals reported back to callers.

The chat-basic e2e asserted the old formula, so it encoded the bug; it
now asserts the same invariant via sumTotalTokens.

Verified against real upstream (Vertex Anthropic, claude-sonnet-4-5):
non-streaming 66 + 101 (72 reasoning) now totals 167 instead of 239, and
streaming 65 + 50 (43 reasoning) totals 115 instead of 158, with output
cost still exactly completionTokens * price.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6eFuuNRtA7bogrRtAHAYV
Probed all 25 providers with credentials against live upstream: a
completion count that already contains reasoning is the near-universal
convention, not the exception. Adds the 14 confirmed providers to
completionIncludesReasoning.

Canopywave is the one actively mis-billed: it reports reasoning_tokens at
the top level of usage, which is the only shape extract-token-usage reads
for OpenAI-format providers, so its reasoning reached the cost engine and
was billed twice (92% over) and inflated streaming total_tokens.

xAI stays off the list — grok-4 answers "9" with completion_tokens 1,
reasoning_tokens 118 and total_tokens 324, so its completion genuinely
excludes reasoning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 18:42
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b0d20ee-8d02-410e-bc79-7f03183eb8c5

📥 Commits

Reviewing files that changed from the base of the PR and between c184a30 and 86fda8d.

📒 Files selected for processing (11)
  • apps/docs/content/features/data-retention.mdx
  • apps/gateway/src/chat-basic.e2e.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/parse-provider-response.spec.ts
  • apps/gateway/src/chat/tools/parse-provider-response.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.spec.ts
  • apps/gateway/src/chat/tools/transform-response-to-openai.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/lib/logs.spec.ts
  • apps/gateway/src/lib/logs.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/gateway/src/lib/logs.ts
  • apps/gateway/src/lib/logs.spec.ts
  • apps/docs/content/features/data-retention.mdx
  • apps/gateway/src/chat/chat.ts

Included review availability: Your plan includes up to 4 reviews per rolling hour; 3 remain after this review.


Walkthrough

The gateway now applies provider-specific reasoning-token rules to billing, storage costs, response usage normalization, and cached, streaming, and non-streaming chat totals. Documentation and tests cover inclusive and additive provider behavior.

Changes

Reasoning-token accounting

Layer / File(s) Summary
Provider token inclusion contract
apps/gateway/src/lib/costs.ts, apps/gateway/src/lib/costs.spec.ts
Adds provider classification and sumTotalTokens. Output billing uses the shared provider rule. Tests cover provider classification, token totals, and null values.
Provider-aware response usage normalization
apps/gateway/src/chat/tools/parse-provider-response.ts, apps/gateway/src/chat/tools/parse-provider-response.spec.ts, apps/gateway/src/chat/tools/transform-response-to-openai.ts, apps/gateway/src/chat/tools/transform-response-to-openai.spec.ts
Fallback total_tokens calculations use provider-aware summation. Usage construction passes the selected provider. Tests cover Groq, xAI, Alibaba, AWS Bedrock, and upstream total precedence.
Storage cost calculation
apps/gateway/src/lib/logs.ts, apps/gateway/src/lib/logs.spec.ts
calculateDataStorageCost accepts an optional provider and avoids double-counting reasoning tokens when completion tokens include them.
Chat usage integration
apps/gateway/src/chat/chat.ts, apps/gateway/src/chat-basic.e2e.ts, apps/docs/content/features/data-retention.mdx
Cached, streaming, and non-streaming paths use provider-aware totals and pass the provider to storage-cost calculations. Documentation and end-to-end assertions reflect the same rules.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 86fda

The change prevents reasoning tokens from being counted twice for providers whose output count already includes them while preserving additive behavior for providers that report reasoning separately; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Provider as Provider response
  participant Transform as Response transformer
  participant Chat as chat.ts
  participant Costs as costs.ts
  participant Logs as logs.ts
  Provider->>Transform: return usage fields
  Transform->>Costs: sumTotalTokens(provider, prompt, completion, reasoning)
  Costs-->>Transform: normalized total tokens
  Transform->>Chat: return provider-aware usage
  Chat->>Logs: calculateDataStorageCost(..., provider)
  Logs->>Costs: classify completion reasoning inclusion
  Costs-->>Logs: adjusted reasoning token count
Loading

Possibly related PRs

Suggested reviewers: ratchaw

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the gateway storage-cost fix for double-billed reasoning tokens.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch flex-tier-output-token-cost

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.

@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: 1

🧹 Nitpick comments (1)
apps/gateway/src/lib/costs.spec.ts (1)

2218-2229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every reasoning-inclusive provider ID.

The direct test covers four of the eleven inclusive IDs. Add cases for glacier, iceberg, google-vertex, quartz, sakana, meta, and ranoai. A classification regression for one of these providers can restore double billing without failing this test.

Proposed test structure
+	const inclusiveProviders = [
+		"google-ai-studio",
+		"glacier",
+		"iceberg",
+		"google-vertex",
+		"quartz",
+		"openai",
+		"azure",
+		"sakana",
+		"meta",
+		"ranoai",
+		"aws-mantle",
+	];
+
 	it("treats OpenAI-style Responses API providers as reasoning-inclusive", () => {
-		expect(completionTokensIncludeReasoning("openai")).toBe(true);
-		expect(completionTokensIncludeReasoning("azure")).toBe(true);
-		expect(completionTokensIncludeReasoning("aws-mantle")).toBe(true);
-		expect(completionTokensIncludeReasoning("google-ai-studio")).toBe(true);
+		for (const provider of inclusiveProviders) {
+			expect(completionTokensIncludeReasoning(provider)).toBe(true);
+		}
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/lib/costs.spec.ts` around lines 2218 - 2229, Add assertions
in the “reasoning-inclusive” test within completionTokensIncludeReasoning to
cover glacier, iceberg, google-vertex, quartz, sakana, meta, and ranoai,
ensuring each provider ID returns true alongside the existing inclusive cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/docs/content/features/data-retention.mdx`:
- Around line 43-44: Update the documentation text near the reasoning-token
billing explanation to remove the hardcoded provider names and use
provider-agnostic wording based on each provider’s usage contract. Explicitly
state whether the example assumes output tokens already include reasoning
tokens, and preserve the rule that separately reported reasoning tokens are only
added when excluded from the output count.

---

Nitpick comments:
In `@apps/gateway/src/lib/costs.spec.ts`:
- Around line 2218-2229: Add assertions in the “reasoning-inclusive” test within
completionTokensIncludeReasoning to cover glacier, iceberg, google-vertex,
quartz, sakana, meta, and ranoai, ensuring each provider ID returns true
alongside the existing inclusive cases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: afe92682-24bb-4eec-95d7-4ae18105bc73

📥 Commits

Reviewing files that changed from the base of the PR and between 0c2bf77 and 151dc76.

📒 Files selected for processing (6)
  • apps/docs/content/features/data-retention.mdx
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • apps/gateway/src/lib/logs.spec.ts
  • apps/gateway/src/lib/logs.ts

Comment on lines +43 to +44
Most providers (including OpenAI, Azure, and Google) already count reasoning tokens inside the output token count, so each token is billed exactly once. Reasoning tokens are only added separately for providers that report them outside the output count.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the hardcoded provider list and clarify the example.

This documentation hardcodes a provider catalogue, which violates the documentation guideline and can become stale. Use provider-agnostic wording based on each provider’s usage contract. State whether the example assumes that output tokens already include reasoning tokens.

Proposed wording
-Most providers (including OpenAI, Azure, and Google) already count reasoning tokens inside the output token count, so each token is billed exactly once. Reasoning tokens are only added separately for providers that report them outside the output count.
+Storage billing follows the provider's usage contract. When completion tokens include reasoning tokens, each token is billed once. When reasoning tokens are reported separately, they are added to the storage total once. The example below assumes reasoning-inclusive completion tokens.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Most providers (including OpenAI, Azure, and Google) already count reasoning tokens inside the output token count, so each token is billed exactly once. Reasoning tokens are only added separately for providers that report them outside the output count.
Storage billing follows the provider's usage contract. When completion tokens include reasoning tokens, each token is billed once. When reasoning tokens are reported separately, they are added to the storage total once. The example below assumes reasoning-inclusive completion tokens.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/content/features/data-retention.mdx` around lines 43 - 44, Update
the documentation text near the reasoning-token billing explanation to remove
the hardcoded provider names and use provider-agnostic wording based on each
provider’s usage contract. Explicitly state whether the example assumes output
tokens already include reasoning tokens, and preserve the rule that separately
reported reasoning tokens are only added when excluded from the output count.

Source: Coding guidelines

calculateDataStorageCost summed prompt + completion + reasoning
unconditionally, but most providers (OpenAI, Azure, Google, aws-mantle,
Sakana, Meta, RanoAI) already count reasoning inside the completion
count, so retention storage billed those tokens twice whenever a
reasoning count reached a call site.

Thread the used provider into the storage calculation and skip the
reasoning add-on when completionIncludesReasoning() says the completion
count already contains it. Providers that report reasoning outside the
completion count (e.g. xAI) keep the additive behavior.

Stacked on #3486, which provides completionIncludesReasoning() and
fixes the equivalent double-count in usage.total_tokens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@steebchen
steebchen force-pushed the flex-tier-output-token-cost branch from 151dc76 to 93a016e Compare August 12, 2026 20:17
@steebchen steebchen changed the title fix(gateway): don't double-count reasoning tokens fix(gateway): provider-aware data storage cost Aug 12, 2026
…n-cost

# Conflicts:
#	apps/gateway/src/chat/tools/parse-provider-response.ts
#	apps/gateway/src/chat/tools/transform-response-to-openai.ts
#	apps/gateway/src/lib/costs.spec.ts
#	apps/gateway/src/lib/costs.ts
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@steebchen steebchen changed the title fix(gateway): provider-aware data storage cost fix(gateway): storage cost double-billed reasoning Aug 16, 2026
@steebchen steebchen closed this Aug 22, 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.

3 participants