Repository navigation
Conversation
aheritier
added this pull request to stack #4704
October 22, 2026 11:49
usage.iterations is the per-attempt record of what is billed, each at its own model's rates; the top-level usage covers only the last attempt. Report the declined hops and the serving attempt beside the usage. A declined hop with no output is billed only in the categories Anthropic bills (bio, frontier_llm, reasoning_extraction as of September 2026); if the markers don't line up with the hops, every hop counts as billed. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
When a response reports per-attempt usage, its cost is the sum of the billed attempts, each priced at the model that ran it: the dispatched model with its alias and cost override, the others from the catalogue. The top-level usage, which repeats the last attempt, is not added. Any attempt that cannot be priced leaves the turn unpriced. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
The lifecycle test now prices each model differently: the handoff turn costs the declined Opus attempt plus the Sonnet one, and the sticky turn the Sonnet attempt only. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
The stream, continuation and per-attempt billing now handle a fallback that changes the serving model, so remove the temporary rejection of Claude 5.x fallback chains. Claude Haiku 5.5 keeps its own rule: it has no server-side fallback, so a list is rejected and default only warns. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Sonnet 5.5 with a Sonnet 5 fallback goes through the beta API with the July header, the fallback list and the drop_block binding, and no client-side thinking rewrite: the server adapts each attempt. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
…shape - Mark the requested model's attempt by position: the API may report a resolved, dated ID that differs from the configured one. - Behind a router, price that attempt with the leaf the router dispatched to, not the router's config. - Reject at startup sampling settings or a token thinking budget that a listed fallback refuses: the API rejects such a request up front. - Bill a declined hop whose refusal category is empty. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
Validate what the request builder sends rather than the raw config: temperature and top_p only go out with thinking off, and an effort level on a model without adaptive thinking goes out as a token budget. The first rule rejected compatible chains, the second missed incompatible ones. Signed-off-by: Arnaud Héritier <aheritier@apache.org>
aheritier
force-pushed
the
feat/claude5x-fallback-billing
branch
from
October 22, 2026 11:52
b23673b to
ee3e84f
Compare
This was referenced Oct 22, 2026
trungutt
approved these changes
Oct 23, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4584 (PR 3 of 3, stacked on #4691; #4690 is merged). Part of epic #4580.
What
A server fallback turn is now billed the way the API bills it:
The temporary rejection of Claude 5.x fallback chains is removed, so
fallbacksworks with Claude 5.x models (Haiku 5.5:defaultonly).feat(anthropic): record each server fallback attempt's usageusage.iterationstochat.MessageStreamResponse.UsageAttempts: each declined hop (message) and the serving attempt (fallback_message), with its model and tokens. A declined hop with no output is not billed unless its refusal category (from the matching marker'strigger) isbio,frontier_llmorreasoning_extraction. If the markers don't line up with the hops, every hop counts as billedfeat(runtime): price server fallback attempts at their own ratesattemptsCostsums the billed attempts, each through the existingapplyModelCostandcomputeMessageCost. The dispatched model keeps its display alias and cost override; other models are priced from the catalogue. Any unpriceable attempt makes the turn unpriced. The top-level usage, which repeats the last attempt, is not addedtest(runtime): a server fallback turn is billed per attemptfeat(anthropic): allow server fallback with Claude 5.x modelsvalidateFallbacks. The Haiku 5.5 rules from #4690 take effect, and the docs, schema and example drop the warning. The docs now describe the handoff, the billing, and the Sonnet 5.5 → Sonnet 5 conversion done by the APItest(providers): a Claude 5.x request with server fallbackfix: price the dispatched attempt with its own config, check request shapefix(anthropic): check the request a fallback would receiveWhat it fixes in practice
In the end-to-end test, the handoff turn costs $0.000585: the declined Opus 4.8 attempt plus the Sonnet 4.6 one. Without per-attempt pricing it recorded $0.000105, the final attempt's top-level usage only. Every declined attempt that produced output was missing from the bill.
Behavior changes
fallbackswith a Claude 5.x model is no longer rejected.temperature/top_p(sent only with thinking off),top_k, or token-budget thinking, including an effort level that an older model without adaptive thinking turns into tokens. The API rejects such a request up front, because it must be valid for every model named. Before, the temporary block rejected those chains anyway.Design notes
chat.Usage: a slice madechat.Usagenon-comparable, and existing code compares it with== (chat.Usage{}). The attempts travel beside it, on the stream response and instreamResult. Only the resulting cost is persisted.Tests
TestServerFallbackAttemptscybernot billed,biobilled), two hops, sticky routing, a missing marker (billed); exact token mapping; none without a fallback or with only other iteration typesTestAttemptsCost,TestDispatchedConfigTestServerFallbackLifecycle(extended)loop.goTestValidateFallbackRequestShapeTestNewClient_Claude5FallbacksdefaultacceptedTestServerFallbackClaude5RequestNot added, and why
nilin-session and records as $0 with the existing warning. fix(runtime): keep unpriced turns distinguishable from free ones after reload #4683 tracks persisting it.#4584 acceptance criteria
default/list, June lists, malformed input, target count and uniqueness, incompatible pairsTestParseFallbacks,TestValidateFallbackSpec,TestServerSideFallbackRequests, host and Haiku rulesTestServerFallbackStream,TestHandleStream_*,TestServerFallbackProjectionTestServerFallbackLifecycleTestServerFallbackStatePerStream,TestServedByWhileStreamingtask build,task test,task lint,go test -raceReferences
usage.iterationsarray is the per-attempt record of what you're billed. The top-levelusagecounts describe only the attempt that produced the returned message"; each attempt "at the rates of the model that ran it"bio,frontier_llm,reasoning_extraction(as of September 2026); mid-stream refusals bill input and the output already streamedusageas lower cache writes and higher cache readsbetween_toolsconversion under JulyValidation
task buildpasses.task testpasses.golangci-lintv2.14.0 reports 0 issues, andgo run ./lint .reports no offenses.go test -race -shuffle=onpasses on runtime, providers and chat.The wasm tests (
GOOS=js) pass.An independent review found no critical issues and three important ones, all fixed here:
Also fixed: billing an empty refusal category, exact cache-token assertions, a case where the markers don't line up with the declined hops (it caught an index panic before commit), and a docs link.