Skip to content

fix: bill for serving fallback model in anthropic - #5387

Merged
akshaydeo merged 1 commit into
devfrom
07-20-fix_bill_for_serving_fallback_model_in_anthropic
Jul 20, 2026
Merged

fix: bill for serving fallback model in anthropic#5387
akshaydeo merged 1 commit into
devfrom
07-20-fix_bill_for_serving_fallback_model_in_anthropic

Conversation

@TejasGhatte

@TejasGhatte TejasGhatte commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

When Anthropic's server-side fallback feature hands off a request mid-call to a different model, the orchestrator never sees the swap — it only knows what the caller originally asked for. This means cost calculations were being applied against the requested model's rates rather than the model that actually produced the tokens, causing incorrect (typically inflated) billing figures. This PR surfaces the serving model through the response's RoutingInfo and ensures the cost engine prices against it.

Changes

  • Added ServerSideFallbackModel to RoutingInfo — a provider-owned field that names the model that actually served the response when a server-side handoff occurred. Nil on all ordinary responses, so existing pricing behaviour is unchanged.
  • Added AnthropicUsageIterationTypeFallbackMessage constant and ServerSideFallbackModel() method on AnthropicUsage, which inspects usage.iterations for a fallback_message-typed entry and returns its model name.
  • Populated RoutingInfo.ServerSideFallbackModel in both the chat and responses non-streaming paths before usage is flattened, since the neutral usage representation drops iterations.
  • For streaming responses, latched the serving model into AnthropicResponsesStreamState when first seen on a usage chunk, then stamped it onto both the message_delta and completed response chunks — whichever one ends up being priced will carry it.
  • Updated PopulateRoutingInfo to preserve any pre-existing ServerSideFallbackModel value when core's routing snapshot overwrites RoutingInfo. Without this, streaming would lose the field entirely since the snapshot predates the final usage chunk.
  • Updated resolvePricing in the cost engine to rank ServerSideFallbackModel first in the candidate lookup chain, ahead of alias model name, alias model ID, and the requested model name. Override keys also follow the serving model so negotiated rates for the model that actually ran are the ones applied.

Type of change

  • Bug fix

Affected areas

  • Core (Go)
  • Providers/Integrations

How to test

go test ./core/providers/anthropic/... ./core/schemas/... ./framework/modelcatalog/datasheet/...

Key scenarios covered by new tests:

  • ServerSideFallbackModel() returns the correct model from fallback_message iterations, nil for ordinary responses, nil for compaction-only iterations, and nil for a nil receiver.
  • Both the chat and responses non-streaming paths stamp the serving model onto ExtraFields.RoutingInfo.ServerSideFallbackModel.
  • PopulateRoutingInfo preserves the provider-set serving model while still applying core's routing snapshot for all other fields.
  • Cost is calculated at Opus 4.8 rates (not Fable 5 rates) when a fallback handoff is recorded.
  • Responses with no recorded handoff continue to price against the requested model.
  • An unknown serving model falls through the candidate chain to the requested model rather than zeroing the cost.
  • The serving model outranks a key alias in the lookup chain.
  • A pricing override keyed on the serving model applies correctly to a fallback-served turn.

Breaking changes

  • No

Security considerations

None. This change only affects cost attribution metadata and pricing lookups; no auth, secrets, or PII are involved.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

CLAassistant commented Jul 20, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad82698f-2518-4e3c-b430-7b4f38fb5d8e

📥 Commits

Reviewing files that changed from the base of the PR and between 5e28f23 and f8b5f2c.

📒 Files selected for processing (11)
  • core/providers/anthropic/anthropic.go
  • core/providers/anthropic/chat.go
  • core/providers/anthropic/responses.go
  • core/providers/anthropic/serversidefallback_test.go
  • core/providers/anthropic/types.go
  • core/providers/anthropic/utils.go
  • core/schemas/bifrost.go
  • core/schemas/chatcompletions.go
  • framework/modelcatalog/datasheet/cost.go
  • framework/modelcatalog/datasheet/cost_test.go
  • tests/e2e/api/collections/provider-harness.json
📝 Walkthrough

Walkthrough

Anthropic usage now identifies server-side fallback models, propagates them through chat and Responses routing metadata, preserves them during routing updates, and prioritizes them for pricing. Fallback request fields, count-token handling, and fast-mode headers are also covered.

Changes

Anthropic fallback routing

Layer / File(s) Summary
Fallback model usage contract
core/providers/anthropic/types.go, core/providers/anthropic/serversidefallback_test.go
Adds fallback iteration detection, per-attempt fallback overrides, and coverage for model extraction and native fallback round trips.
Routing metadata preservation
core/schemas/bifrost.go, core/schemas/chatcompletions.go, core/providers/anthropic/serversidefallback_test.go
Adds fallback-serving model fields to routing and usage data, and preserves routing values during updates.
Provider response propagation
core/providers/anthropic/anthropic.go, core/providers/anthropic/chat.go, core/providers/anthropic/responses.go, core/providers/anthropic/serversidefallback_test.go
Propagates fallback-serving models through chat, streaming, and non-streaming responses.
Fallback request compatibility
core/providers/anthropic/anthropic.go, core/providers/anthropic/utils.go, core/providers/anthropic/serversidefallback_test.go, tests/e2e/api/collections/provider-harness.json
Preserves fallback entry fields, strips rejected count-token fields, routes fallback arrays by entry shape, and injects fast-mode headers for supported fallback entries.
Serving-model pricing resolution
framework/modelcatalog/datasheet/cost.go, framework/modelcatalog/datasheet/cost_test.go
Prioritizes serving-model pricing and overrides, then falls back to requested-model pricing when necessary.

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

Suggested reviewers: akshaydeo, pratham-mishra04, sammaji

Sequence Diagram(s)

sequenceDiagram
  participant AnthropicUsage
  participant AnthropicResponseConverter
  participant RoutingInfo
  participant resolvePricing
  participant ModelCatalog

  AnthropicUsage->>AnthropicResponseConverter: expose fallback-serving model
  AnthropicResponseConverter->>RoutingInfo: attach ServerSideFallbackModel
  RoutingInfo->>resolvePricing: provide serving and requested model metadata
  resolvePricing->>ModelCatalog: try serving-model pricing first
  ModelCatalog-->>resolvePricing: return rates or no match
  resolvePricing->>ModelCatalog: fall back to requested-model pricing
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
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.
Title check ✅ Passed The title is concise and accurately describes the main change: billing against the serving fallback model in Anthropic.
Description check ✅ Passed The description covers the required sections and includes summary, changes, testing, affected areas, breaking changes, security, and checklist.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 07-20-fix_bill_for_serving_fallback_model_in_anthropic

Comment @coderabbitai help to get the list of available commands.

TejasGhatte commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@TejasGhatte TejasGhatte mentioned this pull request Jul 20, 2026
18 tasks
@TejasGhatte
TejasGhatte marked this pull request as ready for review July 20, 2026 11:59
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

The cancellation billing gap should be fixed before merging.

  • Completed streams retain the serving model through routing and pricing.
  • Cancellation or timeout before terminal usage can still price partial usage against the requested model.

core/providers/anthropic/anthropic.go

Important Files Changed

Filename Overview
core/providers/anthropic/anthropic.go Propagates the serving model through stream state, but cancellation before terminal usage can still lose it.
core/providers/anthropic/types.go Adds fallback request fields and extracts the serving model from usage iterations.
core/schemas/bifrost.go Adds serving-model routing metadata and preserves provider-populated values.
framework/modelcatalog/datasheet/cost.go Prioritizes the serving model in catalog and override pricing.

Reviews (5): Last reviewed commit: "fix: bill for serving fallback model in ..." | Re-trigger Greptile

Comment thread core/providers/anthropic/chat.go

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/providers/anthropic/responses.go (1)

2271-2298: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Server-side-fallback latch only runs for integration_type == "anthropic" — streaming under other integrations never captures the served model.

The state.ServerSideFallbackModel latch is nested inside the ctx.Value(schemas.BifrostContextKeyIntegrationType) == "anthropic" branch. For every other integration (the default OpenAI-compatible Responses API streaming path), the function returns early at Line 2348 without ever calling chunk.Usage.ServerSideFallbackModel(). Since message_stop's completedResp (Line 2412) only reads back whatever was latched, streaming responses on the default integration will never carry ServerSideFallbackModel, so a fallback-served streamed turn is billed against the requested model instead of the model that actually served it — the exact bug this PR targets, left unfixed for the primary streaming path.

chunk.Usage is the raw Anthropic-format usage regardless of output integration, so this bookkeeping should not depend on integration type.

🐛 Proposed fix — hoist the latch out of the integration-type gate
 		if chunk.Delta.StopDetails != nil {
 			state.StopDetails = stopDetailsToBifrost(chunk.Delta.StopDetails)
 		}
+		// The handoff is only visible on this chunk's iterations, but the chunk
+		// that ends up being priced may be a later one, so latch it in state,
+		// regardless of which integration format is being emitted downstream.
+		if served := chunk.Usage.ServerSideFallbackModel(); served != nil {
+			state.ServerSideFallbackModel = served
+		}
 		// Check if integration type in ctx is anthropic
 		if ctx.Value(schemas.BifrostContextKeyIntegrationType) == "anthropic" {
 			// Convert usage from Anthropic format to Bifrost
 			bifrostUsage := ConvertAnthropicUsageToBifrostUsage(chunk.Usage)
 
-			// The handoff is only visible on this chunk's iterations, but the chunk
-			// that ends up being priced may be a later one, so latch it in state.
-			if served := chunk.Usage.ServerSideFallbackModel(); served != nil {
-				state.ServerSideFallbackModel = served
-			}
-
 			// Use the already-remapped stop reason so SO overrides are preserved.

Also applies to: 2337-2343, 2407-2413

🤖 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 `@core/providers/anthropic/responses.go` around lines 2271 - 2298, Move the
ServerSideFallbackModel latch using chunk.Usage.ServerSideFallbackModel() out of
the integration_type == "anthropic" branch in the
AnthropicStreamEventTypeMessageDelta handling, so it runs for every streaming
integration before any early return. Keep ConvertAnthropicUsageToBifrostUsage
and other integration-specific processing inside the existing branch, and
preserve the message_stop completedResp path that reads
state.ServerSideFallbackModel.
🤖 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 `@core/providers/anthropic/responses.go`:
- Line 78: Update the AnthropicResponsesStreamState cleanup in flush() to clear
ServerSideFallbackModel before returning the struct to
anthropicResponsesStreamStatePool. Ensure every pooled-state reuse starts with a
nil value, preserving the existing resets for all other fields.

---

Outside diff comments:
In `@core/providers/anthropic/responses.go`:
- Around line 2271-2298: Move the ServerSideFallbackModel latch using
chunk.Usage.ServerSideFallbackModel() out of the integration_type == "anthropic"
branch in the AnthropicStreamEventTypeMessageDelta handling, so it runs for
every streaming integration before any early return. Keep
ConvertAnthropicUsageToBifrostUsage and other integration-specific processing
inside the existing branch, and preserve the message_stop completedResp path
that reads state.ServerSideFallbackModel.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89bfa7f3-fd9d-4716-a084-209e68ef0e31

📥 Commits

Reviewing files that changed from the base of the PR and between b587562 and 9f624d4.

📒 Files selected for processing (7)
  • core/providers/anthropic/chat.go
  • core/providers/anthropic/responses.go
  • core/providers/anthropic/serversidefallback_test.go
  • core/providers/anthropic/types.go
  • core/schemas/bifrost.go
  • framework/modelcatalog/datasheet/cost.go
  • framework/modelcatalog/datasheet/cost_test.go

Comment thread core/providers/anthropic/responses.go Outdated
@TejasGhatte
TejasGhatte force-pushed the 07-20-fix_bill_for_serving_fallback_model_in_anthropic branch 2 times, most recently from 66c2122 to 7834f85 Compare July 20, 2026 12:23
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026
@TejasGhatte
TejasGhatte force-pushed the 07-20-fix_bill_for_serving_fallback_model_in_anthropic branch 2 times, most recently from dd9aa5f to 5e28f23 Compare July 20, 2026 14:12
Comment thread core/providers/anthropic/anthropic.go
akshaydeo
akshaydeo previously approved these changes Jul 20, 2026

akshaydeo commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 20, 5:36 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 20, 5:38 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 20, 5:39 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo changed the base branch from 07-20-fix_anthropic_fallbacks_and_refusal to graphite-base/5387 July 20, 2026 17:36
@akshaydeo
akshaydeo changed the base branch from graphite-base/5387 to dev July 20, 2026 17:37
@akshaydeo
akshaydeo dismissed stale reviews from coderabbitai[bot] and themself July 20, 2026 17:37

The base branch was changed.

@akshaydeo
akshaydeo force-pushed the 07-20-fix_bill_for_serving_fallback_model_in_anthropic branch from 5e28f23 to f8b5f2c Compare July 20, 2026 17:37
@akshaydeo
akshaydeo merged commit f428d8f into dev Jul 20, 2026
13 of 15 checks passed
@akshaydeo
akshaydeo deleted the 07-20-fix_bill_for_serving_fallback_model_in_anthropic branch July 20, 2026 17:39
Comment on lines +990 to +993
if served := usageToProcess.ServerSideFallbackModel(); served != nil {
servedFallbackModel = served
usage.ServerSideFallbackModel = served
}

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.

P1 Capture fallback model earlierServerSideFallbackModel() becomes available only when the terminal message_delta includes usage.iterations. If the stream is cancelled or times out after fallback content starts but before that event is processed, the deferred billing handler snapshots usage while this field is still nil. The partial turn is then priced using the requested model instead of the model that served it. Retain the serving model from metadata available before the terminal usage event so cancellation billing can use it.

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