Repository navigation
0.55.10 — chat costs are final only when the response proves a settlement - #215
Merged
Merged
Conversation
…ment blockrun_chat on a wallet reported every paid call as cost_is_estimate:false. The figure was the delta of the SDK's spend counter, which adds the signed amount on any paid 2xx. The gateway settles a stream after its headers, and its enterprise local-trust path serves calls whose settlement can then fail: on 2026-11-12 four zai/glm-5.3 calls from a $0.001 wallet were served, none settled, and $0.104 was reported as final. - withSettledCost (chat.ts) and the native path (chat-anthropic.ts) call a cost final only when the response names its settlement transaction (PAYMENT-RESPONSE with a transaction or X-Payment-Receipt, and no X-Payment-Settled: false). Otherwise cost_is_estimate is true and the reply says the amount was signed but not confirmed. The ledger still books the signed amount, so budget caps stay conservative. - A paid failure without a receipt is booked as a maybe, not "the charge stands"; the routing loop still stops. - The gateway's in-band sseError frame (model:"error") is a failure, not the answer.
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.
Problem
On 2026-11-12,
blockrun_chaton a Base wallet holding about $0.001 booked $0.104 across 3-4zai/glm-5.3calls (sessionSpend, andcost_usdwithcost_is_estimate: false), but no payment settled. CDP verify failed, the gateway served the calls on its enterprise local-trust path, and its settlement after the stream then failed ([ALERT][enterprise-unpaid]for each call). One call ended in an upstream error and still came back as a success, withserved_model: "error"and a finalcost_usd: 0.04443.Root cause
src/tools/chat.ts:331(withSettledCost, wallet branch) setcostIsEstimate: d === null. In other words, any delta in the SDK'sgetSpending()counter was treated as settled. The SDK (@blockrun/llm3.26.5,LLMClient.chatCompletionStream) adds the signed amount whenever a paid request comes back 2xx, unless the gateway names a skipped settlement. It never learns whether the settlement landed. On a stream it cannot know: the gateway sends the headers (justSSE_HEADERS, no receipt) before it settles. Line 335 did the same thing for failures and booked any delta ascertainty: "settled"("the charge stands").src/utils/chat-stream.tsChatAccumulator.foldrecognised only an{ error }object. The gateway'ssseErrorframe (model: "error",id: "error-<ts>", content"\n\n[Error: …]") was read as normal content, so a failed call came back as the answer.src/tools/chat-anthropic.ts:503(the nativethinkingpath) had the same problem: on the wallet rail it always setcostIsEstimate = false.The settled-vs-maybe split from 0.55.2/0.55.3 decided on how the call ended (it threw, or the edge returned 5xx). It never checked for a receipt, so a 2xx that was served but never settled always counted as settled.
Change
settlementTxFromHeaders(). It counts as proof only a receipt with a transaction:PAYMENT-RESPONSE/X-PAYMENT-RESPONSEwith a non-emptytransactionand nosuccess: false, orX-Payment-Receipt.X-Payment-Settled: falseoverrides both. This matches the gateway's non-stream route, which writessuccess: truewith an empty transaction plusX-Payment-Settled: falsewhen its settlement fails.ChatOutcomeandAcceptedThenFailedErrornow carrysettlementTx. The non-streaming path reads the SDK'ssettlement.transaction. The Solana frame stream has no headers, so it is always unproven.cost_is_estimate: false,settlement_txset) only with that proof. Without it,cost_is_estimate: trueand the reply says the amount was signed but not confirmed as settled. The ledger still books the signed amount, so global budgets and delegated limits stay conservative."unknown"(a maybe-charge). The routing loop still stops and the text still warns against retrying.AcceptedThenFailedError. Any partial text before it is kept.Paid streams never carry a receipt today, so paid wallet chat will normally report
cost_is_estimate: true. That is what this server can actually observe. The gateway does not change.Tests
chat-wallet-after-payment.test.ts(Solana and Base):cost_is_estimate: true, nosettlement_tx.model: "error"frame:isError, noserved_model: "error", booked as a maybe, routing loop stops.settlement_tx;X-Payment-Settled: falseplus an empty transaction is not proof; a failure that has a receipt still says "the charge stands".chat-anthropic-settled.test.ts: native success with and without a receipt, and a failure after connect with and without a receipt.chat-settled-on-errorand the native partial-text test.src: 10 of them fail there.npm run build,npm run typecheckandnpm testall exit 0 (1491 tests, 1491 pass).Not in this PR
blockrun_walletsessionSpendis still one figure that includes maybe-charges, as the budget design intends. If a confirmed/unconfirmed split is wanted, it should cover every paid tool, not just chat. Other tools that book the SDK counter (image, music, speech, etc.) were not audited for the same rule.🤖 Generated with Claude Code