Skip to content

feat: bill Claude API server fallback per attempt and allow Claude 5.x chains - #4700

Open
aheritier wants to merge 7 commits into
feat/claude5x-fallback-streamfrom
feat/claude5x-fallback-billing
Open

aheritier wants to merge 7 commits into
feat/claude5x-fallback-streamfrom
feat/claude5x-fallback-billing

Conversation

@aheritier

@aheritier aheritier commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • each attempt is priced at its own model's rates;
  • a declined attempt counts only when it's billed;
  • nothing is counted twice.

The temporary rejection of Claude 5.x fallback chains is removed, so fallbacks works with Claude 5.x models (Haiku 5.5: default only).

Commit Change
feat(anthropic): record each server fallback attempt's usage The beta adapter maps usage.iterations to chat.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's trigger) is bio, frontier_llm or reasoning_extraction. If the markers don't line up with the hops, every hop counts as billed
feat(runtime): price server fallback attempts at their own rates attemptsCost sums the billed attempts, each through the existing applyModelCost and computeMessageCost. 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 added
test(runtime): a server fallback turn is billed per attempt The end-to-end lifecycle test prices each model differently
feat(anthropic): allow server fallback with Claude 5.x models Removes validateFallbacks. 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 API
test(providers): a Claude 5.x request with server fallback Sonnet 5.5 with a Sonnet 5 fallback, through the real factory
fix: price the dispatched attempt with its own config, check request shape Review fixes: the requested model's attempt is identified by position (the API can report a resolved dated ID); behind a router it is priced with the dispatched leaf; startup rejects sampling settings or a token thinking budget that a listed fallback refuses; a declined hop with an empty category is billed
fix(anthropic): check the request a fallback would receive From Copilot's review: validate the resolved request (what the builder sends after thinking resolution) rather than the raw config

What 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

  • Claude 5.x chains are accepted: fallbacks with a Claude 5.x model is no longer rejected.
  • New startup error: a configuration is rejected when a listed fallback refuses what the request actually sends: 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

  • Why the attempts aren't on chat.Usage: a slice made chat.Usage non-comparable, and existing code compares it with == (chat.Usage{}). The attempts travel beside it, on the stream response and in streamResult. Only the resulting cost is persisted.
  • Billed categories: one constant, linked to the docs and dated September 2026, because Anthropic says the list may change.
  • Fallback credit: no client logic. The credit already shows up in the fallback attempt's reported cache counts.

Tests

Test Pins Mutation that makes it fail
TestServerFallbackAttempts Attempts for mid-output, before output (cyber not billed, bio billed), two hops, sticky routing, a missing marker (billed); exact token mapping; none without a fallback or with only other iteration types Bill every declined hop
TestAttemptsCost, TestDispatchedConfig Per-model rates; an unbilled hop excluded; an uncatalogued attempt leaves the turn unpriced; the dispatched alias and cost override kept, also under a resolved dated ID; the router leaf's config Price every attempt at the dispatched model
TestServerFallbackLifecycle (extended) Stored costs of the handoff turn and the sticky turn Drop the attempts branch in loop.go
TestValidateFallbackRequestShape Sampling settings and token budgets a listed fallback refuses Drop the sampling check
TestNewClient_Claude5Fallbacks Claude 5.x chains accepted; Haiku 5.5: list rejected, default accepted The 5.x block
TestServerFallbackClaude5Request The wire request of a Claude 5.x chain The 5.x block (construction fails)

Not added, and why

#4584 acceptance criteria

Criterion Where
Fixtures: July default/list, June lists, malformed input, target count and uniqueness, incompatible pairs #4690: TestParseFallbacks, TestValidateFallbackSpec, TestServerSideFallbackRequests, host and Haiku rules
Stream tests: fallback before output, mid-output, multiple handovers, interruption, refusal while a tool call is open, completed server tools #4691: TestServerFallbackStream, TestHandleStream_*, TestServerFallbackProjection
Superseded client tools never run; save, reload and continue gives the same projection #4691: TestServerFallbackLifecycle
Sticky routing detected and persisted; interleaved streams never swap models or costs #4691: sticky cases, TestServerFallbackStatePerStream, TestServedByWhileStreaming
Cost: per-attempt totals with no double counting, billed vs. non-billed refusals, unchanged producer with a display alias, unknown producer unpriced; no-fallback and non-5.x costs unchanged This PR (plus the existing cost tests)
Temporary block removed; docs and examples updated This PR
task build, task test, task lint, go test -race All three PRs

References

Rule Documentation
"The usage.iterations array is the per-attempt record of what you're billed. The top-level usage counts describe only the attempt that produced the returned message"; each attempt "at the rates of the model that ran it" Billing and rate limits
Before any output, billed only for bio, frontier_llm, reasoning_extraction (as of September 2026); mid-stream refusals bill input and the output already streamed How refusals are billed
The credit shows up in the retry's usage as lower cache writes and higher cache reads Fallback credit
Haiku 5.5 has no server-side fallback; the Sonnet 5.5 between_tools conversion under July Server-side fallback

Validation

  • task build passes.

  • task test passes.

  • golangci-lint v2.14.0 reports 0 issues, and go run ./lint . reports no offenses.

  • go test -race -shuffle=on passes 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:

    • the dispatched attempt was matched by exact ID, so a resolved dated ID lost the cost override or left the turn unpriced;
    • behind a router, attempts were priced with the router's config instead of the leaf's;
    • removing the block dropped the only startup check for a request shape a 5.x fallback refuses.

    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.

@aheritier
aheritier requested a review from a team as a code owner October 22, 2026 11:16
Copilot AI balanced review requested due to automatic review settings October 22, 2026 11:16

This comment was marked as resolved.

@aheritier aheritier added area/docs Documentation changes area/providers/anthropic For features/issues/fixes related to the usage of Anthropic models area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only. labels Oct 22, 2026
@aheritier
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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation changes area/providers/anthropic For features/issues/fixes related to the usage of Anthropic models area/runtime Runtime engine, agent loop execution, tool dispatch, loop detection kind/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants