Skip to content

feat(budget): park lost settlements durably and fold them at pre-check - #162

Closed
hasitpbhatt wants to merge 28 commits into
Continuum-AI-Corp:mainfrom
hasitpbhatt:budget/pr3-durable-recovery
Closed

hasitpbhatt wants to merge 28 commits into
Continuum-AI-Corp:mainfrom
hasitpbhatt:budget/pr3-durable-recovery

Conversation

@hasitpbhatt

@hasitpbhatt hasitpbhatt commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Orca-Code-Review — push 18

Severity Count Δ vs previous push
P0 0 0
P1 1 0
P2 0 0
P3 0 0

❌ 1 finding blocks merge

PR 3 of a 4-PR stack replacing #91. Builds on #160 and #161 — review commit ddacb57 only (the diff below is cumulative until those merge).

Merge order: do not merge this before #163. The except AdapterError: branch
below returns without settling, so _finalize sees usage_seen=False and
_settlement_amount charges a budgeted key its entire remaining lifetime budget
for a server-side adapter fault. This is known and owned by #163 (4cdf36a),
which settles an adapter fault like the provider-error branch instead — it is
listed here so it does not read as an unfixed gap in this PR's own scope, and it
needs no separate fix.

Review scope: durable recovery — a lost settlement keeps counting against the cap across restarts and workers.

  • New budget_parks table (one row per settlement, keyed by trace_id, created by the startup migration): park writes are idempotent, so an ack-lost commit retries into the same key instead of double-billing.
  • Give-up path proves non-durability via the trace_id gate before parking; blocking cancellation rolls back before probing.
  • Every pre-check re-files memory-held parks and folds fitting obligations into spent_microcents in one CAS-guarded transaction (charge + row deletes); concurrent folders cannot double-bill, and over-remainder parks stay visible while keeping the key exhausted.
  • Coverage: real engine-restart fold-once test, cross-worker test, write-outage memory-fallback + recovery test, stream-outage 429 test, blocking/streaming ack-loss tests, cancellation/memory unit tests, park-table migration test.

OrcaCode Review: PASSED, no findings. Tests: budget unit + integration green; ruff clean.

Known gap, pre-existing and deliberately out of scope here:
app/routes/chat.py:1062 — the except BaseException: pass that follows
await commit_task on the streaming teardown. A second CancelledError aimed at
that await rather than at the commit leaves the settlement neither charged nor
parked, and the arm swallows it. Blamed to main at 9021c8f; this stack neither
introduces nor touches it.

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

Not attached to a line. GitHub only accepts an inline comment on a line this PR changes. The findings below point somewhere else, so they are listed here instead of being dropped.


app/routes/chat.py (line 1154): 🟠 P1 Settle the AdapterError branch like the sibling provider-error branch so a budgeted key isn't charged its entire remaining allowance for an internal adapter fault

The new budget settlement keys the fail-closed "charge the full remaining allowance" rule on usage_seen being False (see _settlement_amount, lines 935-953). The sibling except Exception branch (lines 1155-1205) explicitly sets agg_usage = _settle_unmeasured_stream(...) and usage_seen = True before the finally runs _finalize(), so a mid-stream provider failure is charged only the delivery estimate — its own comment says the alternative "would charge (and exhaust) the key's entire remaining budget" for every transient failure. The except AdapterError: branch (an internal protocol-adapter bug — "our own fault, not the caller's") does NOT do this: it just closes the stream and returns, leaving usage_seen False. The finally then runs _finalize(), whose _settlement_amount() computes max(recorded_cost≈0, cap - spent_snapshot) = the entire remaining allowance, and charge_budget clamps spent_microcents to the cap. So a single AdapterError on a budgeted key — even one that delivered nothing — permanently exhausts the key and bills the customer's whole remaining budget for our own bug. This contradicts the explicit design intent expressed in the sibling branch and in test_budgeted_stream_midstream_error_charges_actual_only. Fix: before return in the AdapterError branch, add agg_usage = _settle_unmeasured_stream(agg_usage, agg_output_chars, body) and usage_seen = True, mirroring the except Exception branch.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 269 calls · 15M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

Not attached to a line. GitHub only accepts an inline comment on a line this PR changes. The findings below point somewhere else, so they are listed here instead of being dropped.


app/routes/chat.py (line 1164): 🟠 P1 AdapterError mid-stream leaves usage_seen False and exhausts the key's entire remaining budget

The new fail-closed settlement (_settlement_amount, lines 935-966) charges the full remaining allowance (cap - kc._budget_spent) whenever a stream ends with usage_seen == False. The cancel branch (line 1132) and the provider-error branch (line 1215) both mark the delivery as known — agg_usage = _settle_unmeasured_stream(...) + usage_seen = True — before _finalize() runs. The except AdapterError branch (lines 1146-1164) does neither: it closes the upstream and returns, so _finalize() runs with usage_seen == False and agg_usage == {}. _settlement_amount() then raises row_values["cost_microcents"] to the key's whole remaining allowance and charge_budget moves the counter by that amount, permanently exhausting a budgeted key on our own adapter bug. This is exactly the defect the commit fixed for the provider-error branch (its test comment: "Before the fix, usage_seen stayed False in that branch and every transient provider failure permanently exhausted the key (charged cap - spent)") and for client disconnects; an AdapterError is the same class of mid-stream failure whose error response IS delivered to the client (the adapter emits a native error event), so per the commit's own policy it should be priced from _settle_unmeasured_stream(agg_usage, agg_output_chars, body) and marked known. Fix: in the AdapterError branch, before return, add agg_usage = _settle_unmeasured_stream(agg_usage, agg_output_chars, body); usage_seen = True (mirroring the except Exception branch).

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 276 calls · 15.6M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@hasitpbhatt
hasitpbhatt force-pushed the budget/pr3-durable-recovery branch from ddacb57 to eac6f2a Compare September 25, 2026 16:53

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 2 issues in this PR: 🟠 2 P1.

Not attached to a line. GitHub only accepts an inline comment on a line this PR changes. The findings below point somewhere else, so they are listed here instead of being dropped.


app/routes/chat.py (line 1168): 🟠 P1 Settle the AdapterError branch before returning so a budgeted key is not charged its full remaining allowance

The except AdapterError: branch (a mid-stream failure of the proxy's own SSE adapter, after content has already been forwarded) returns without calling _settle_unmeasured_stream and without setting usage_seen = True, so _finalize → _settlement_amount() runs with usage_seen still False and charges max(actual, cap - spent) — the key's entire remaining lifetime allowance — and records that amount on the row. The two sibling failure branches in this same commit were explicitly refined to avoid exactly this: the except Exception provider-error branch (line 1197) and the cancel branch (line 1129) both settle with _settle_unmeasured_stream and mark usage_seen = True first, and the test test_budgeted_stream_midstream_error_charges_actual_only documents that leaving usage_seen False "permanently exhausted the key (charged cap - spent)" and calls that a bug. An adapter fault (our own bug, e.g. the malformed-chunk case in tests/integration/test_adapter_failure_attribution.py) hits the identical pattern: a budgeted key's whole remaining allowance is consumed for a few delivered tokens the caller cannot steer — the exact over-bill _settle_unmeasured_stream's docstring says must not happen. The adapter-fault integration tests use uncapped keys, so this path is untested. Fix: before return, mirror the generic branch — agg_usage = _settle_unmeasured_stream(agg_usage, agg_output_chars, body) and usage_seen = True — so _finalize prices the delivered content instead of the full remaining cap.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 272 calls · 15.2M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread packages/auth/spend.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

Not attached to a line. GitHub only accepts an inline comment on a line this PR changes. The findings below point somewhere else, so they are listed here instead of being dropped.


app/routes/chat.py (line 1164): 🟠 P1 Mark the settlement known in the AdapterError branch, or a budgeted key is charged its full remaining allowance on an adapter fault

The streaming generator's failure branches all settle the delivery before _finalize runs: the cancel branch and the provider-error branch both call _settle_unmeasured_stream(...) and set usage_seen = True (lines 1132, 1198), precisely so _finalize's _settlement_amount() (line 952: if not usage_seen: actual = max(actual, cap - _budget_spent)) charges only the delivered estimate instead of the whole remaining allowance. The except AdapterError: branch (an adapter bug mid-stream — the deployment's own fault, not the client's and not the provider's) sets neither: it just sets status/error_type, closes the stream and returns. The finally (line 1210) then runs _finalize() → _commit_row() → _settlement_amount() with usage_seen still False, so the row records cap - kc._budget_spent (the key's FULL remaining allowance, e.g. 100_000 microcents for a fresh 10-cent key) and charge_budget moves the counter to the cap — the key is maxed out by a single adapter fault that delivered nothing or a few chunks, where the sibling provider-error branch would charge ~0 (empty delivery) or the character estimate. Fix: mirror the sibling branches before the return — agg_usage = _settle_unmeasured_stream(agg_usage, agg_output_chars, body) then usage_seen = True.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 237 calls · 13.3M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 289 calls · 18M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 2 issues in this PR: 🟠 2 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 237 calls · 13.8M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py
Comment thread packages/auth/spend.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 2 issues in this PR: 🟠 2 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 268 calls · 16M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py Outdated
Comment thread packages/auth/spend.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 249 calls · 14.8M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread packages/db/migrate.py Outdated

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 283 calls · 16.5M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread packages/db/migrate.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 252 calls · 14.1M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py

@orcacode-review orcacode-review Bot 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.

⚠️ Outdated review — the PR head changed from c387d54 to 6f8a2a4 during the review; these findings describe a diff the PR no longer has.
These findings describe c387d54654d6. Re-run the review to check the PR as it stands now.
The gate evaluated to blocked, but it is not enforced here: branch protection will not see it.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 265 calls · 15.8M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread packages/db/migrate.py Outdated

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

✅ No findings — nothing to flag in this PR. Great work!

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 285 calls · 17.3M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@xizhuomengcontin

Copy link
Copy Markdown
Contributor

@orcacode-review

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

✅ No findings — nothing to flag in this PR. Great work!

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 287 calls · 17.7M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@xizhuomengcontin

Copy link
Copy Markdown
Contributor

I spent a while trying to settle the last open finding on this one end-to-end, and I want to report what I found — including that I could not confirm it.

Three things are blocking this PR, and they are not the same kind of thing

1. It conflicts with main (hard blocker). mergeable: CONFLICTING, in app/routes/chat.py. Six PRs landed since this branched — main is now 31281db. This needs a rebase regardless of everything below.

2. Ten review threads are still unresolved (hard blocker). The repo ruleset has required_review_thread_resolution: true, so the merge button stays off until they are closed. Nine of them are anchored to commits you have since superseded (eac6f2a, 9b82eb2, d26d14f, f0eb0ce, 8fa4850, 12cc7fc, 99b4326) — those look like "fixed but never marked resolved", the same bookkeeping gap #151/#152 had. Could you walk them and resolve the ones your later pushes covered?

3. One finding I could not settle either way — Clamp the boot-time spend seed at the scaled budget (packages/db/migrate.py:161, posted 09-27T02:17 against c387d546).

On that last one: I could not reproduce it

The premise is that two concurrent requests both precheck at spent = 0, both end unmeasured/unpriceable, and each writes cap - spent into requests_log.cost_microcents — so SUM(cost_microcents) reaches 2 × cap, and the boot seed (which is monotonic since 99b4326, but not clamped) then pushes spent_microcents above the cap.

That requires the fail-closed arm in _settlement_amount to fire. I instrumented it and tried four client-reachable ways to get there on 6f8a2a4:

attempt result
ordinary stream usage_seen=True
stream with stream_options: {include_usage: false} usage_seen=True — LiteLLM still produces a cost
client disconnect mid-stream (slow upstream, lands as 499) usage_seen=True, settled by delivery estimate, row cost 0
upstream returns a model absent from the catalog _has_known_price still true via fallback_model, row cost 5

In every case the row recorded the real cost, SUM stayed far below the cap, and the seed after restart matched the log exactly. I never got a row carrying cap - spent.

For contrast, the same harness does trigger it on #161: an injected adapter fault there produces cost_microcents = 999995 on a cap = 1000000 key — the full remaining allowance, reproducible 3/3. So the arm was reachable before 9b82eb2, and your later commits appear to have closed the paths I can construct.

So: I can't call this finding real, and I can't call it dead either. I could not construct the state it describes; that is not the same as proving it impossible. The concurrency window it needs is exactly the kind my end-to-end harness can't force. You know this code far better than my probes do — could you say whether the fail-closed arm is still reachable at all on the current head, and if so whether clamping the seed at budget_limit_cents * MICROCENTS_PER_CENT is worth doing defensively?

Why the bot says "0 findings" — and why that is not evidence here

This is worth flagging, because the green checkmark is misleading on this PR:

09-27T02:17  COMMENTED  c387d546   ← reports the clamp P1
09-27T02:01  6f8a2a47              ← the only commit in between
09-27T02:27  COMMENTED  6f8a2a47   ← "No findings"
09-30T10:50  COMMENTED  6f8a2a47   ← "No findings"  (I triggered this one)

6f8a2a47 is style(test): order imports in the blocking settlement helper so ruff I001 passes — one line, in tests/integration/test_budget_enforcement.py. It does not touch migrate.py, and nothing else changed between the two verdicts.

So the flip from "1 P1" to "0 findings" cannot be explained by a fix. I re-triggered the review today to check, and it came back clean again — but that is the same non-deterministic judge disagreeing with its own earlier pass, not independent confirmation. Two clean runs out of three, on identical code, is weak evidence.

Worth knowing generally: re-running the bot does not resolve existing threads either. The count stayed at 10 unresolved through today's re-review. Only a human closing them lifts the ruleset gate.


To summarise what I think unblocks this: rebase onto 31281db, walk the nine superseded threads and resolve them, and give a verdict on the clamp one. I'll re-verify end-to-end and merge once it's mergeable. For reference, the full stack (160→161→162→163) merged locally passes ruff, 854 tests, my whole regression battery 15/15, and behaves correctly on the budget paths I can drive: cap enforced with a 429, adapter fault settled at 0.00% of the allowance instead of wiping it, and the migration seed matching the log exactly across restarts.

@hasitpbhatt

Copy link
Copy Markdown
Contributor Author

Merge-order carve-out (see #165): this PR's "do not merge before #163" banner applies to #161/#162, not to #160.

#160 (schema + spend library + startup migration, inert) must land first and independently of #163: any build of this PR requires the spent_microcents column, the boot seed/repair, and the spend index to be deployed, or it 503s on upgrade and charges against unseeded totals. Committed stack order: #160 → #163 → #162 → #161.

Also: #163 currently lacks this stack's newer commits pushed after its last update (e.g. #162's 6f8a2a4, #160 push 4 7ca80a5 — cap-clamped monotonic boot-seed repair). The stack needs re-sequencing on top of current #160 before re-review; tracked in #165.

@orcacode-review

Copy link
Copy Markdown

🐳 OrcaCode Review

This pull request was not reviewed.

A maintainer of this repository can comment @orcacode review to run it again. If it keeps failing, the usual causes are the account running out of credit or the app losing access to this repository — both are visible in the OrcaRouter settings. If neither applies, contact OrcaRouter support.

No comments were posted for this commit.

Details
Code ORCA-RELAY
Stage review
Error Error
HTTP 500
Attempt 3 of 3

the model provider could not be reached

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.

@hasitpbhatt
hasitpbhatt force-pushed the budget/pr3-durable-recovery branch from ead5e8f to 6aebfc5 Compare October 3, 2026 04:36
@orcacode-review

Copy link
Copy Markdown

🐳 OrcaCode Review

This pull request was not reviewed.

A maintainer of this repository can comment @orcacode review to run it again. If it keeps failing, the usual causes are the account running out of credit or the app losing access to this repository — both are visible in the OrcaRouter settings. If neither applies, contact OrcaRouter support.

No comments were posted for this commit.

Details
Code ORCA-RELAY
Stage review
Error Error
HTTP 500
Attempt 3 of 3

the model provider could not be reached

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.

@xizhuomengcontin

Copy link
Copy Markdown
Contributor

@orcacode-review

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 2 issues in this PR: 🟠 2 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 298 calls · 19.8M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py
Comment thread app/routes/chat.py
@xizhuomengcontin

Copy link
Copy Markdown
Contributor

Status after #160 landed (ec70055):

Conflicts now. Four files:

packages/auth/spend.py
packages/db/migrate.py
tests/unit/test_budget_migration.py
tests/unit/test_budget_spend.py

#160's push 4 rewrote spend.py and migrate.py (monotonic, cap-clamped seed) — this branch still carries its own older copies. Needs a rebase onto ec70055.

The bot is no longer clean on the current head. On 6aebfc5c (10-06T03:09) it reports 2 P1s:

  • app/routes/chat.py:1422 — gate the no-usage fail-closed arm on delivered tokens, like the unpriced arm
  • app/routes/chat.py:999 — settle fail-closed charges from the raw usage dict, not only prompt_tokens/completion_tokens

Worth noting because the "0 findings" state this PR had on 09-27/09-30 is what I checked last time, and it has changed since.

12 of 12 threads are unresolved, which keeps the ruleset gate shut regardless of the above.


One correction to carry over from my 09-30 note: I could not reproduce the clamp finding on this branch. I instrumented _settlement_amount and tried four client-reachable routes into the fail-closed arm — ordinary stream, include_usage: false, mid-stream client disconnect against a slowed upstream, and an upstream returning a model absent from the catalog. Every one came back with usage_seen=True and the row carrying the real cost; I never got a row at cap - spent.

That is not proof the arm is dead — the two new findings above are about exactly that arm, so it is clearly still on the reviewer's mind. It does mean I can't settle it from the outside, and the two new P1s look like the more productive place to start.

Rebase and work those two and I'll re-verify end-to-end before anything merges. Stack-wide status is in #165.

…normalize Anthropic usage keys

Two holes in the enforcement layer Continuum-AI-Corp#161 added:

- The blocking fail-closed arm fired whenever response had no truthy
  usage, so an EMPTY 200 (usage {}, or usage missing with empty content)
  permanently exhausted a budgeted key — inconsistent with the streaming
  twin that treats a {0,0} usage as known-zero, and with a client simply
  not waiting. Gate on delivered content: fail closed only when real
  content was returned and the cost is unaccountable.
- _build_log_row read only prompt_tokens/completion_tokens, so a usage
  frame keyed input_tokens/output_tokens (what /v1/messages forwards)
  normalized to zero tokens and defeated every token-keyed gate. Read
  both shapes.
The three findings from the review of the previous push shared one root
cause: the charge was decided by four independent signals, none of which
owned the decision. usage_seen was set by whichever handler ran, presence
of a usage dict was treated as measurement, and nothing anywhere looked
at whether content had actually been delivered.

Handlers now record facts and nothing else. The provider-error, adapter
and client-disconnect arms set stream_ending; the loop sets
stream_completed the moment the upstream stream ends, before our own
[DONE] framing, so where a client disconnects stops being able to change
the bill. One function (_unmeasured_charge) turns those facts plus the
delivery characters into the charge, and the blocking path calls the same
function, so the two can no longer drift apart.

Consequences, all of them previously reachable holes:

- An empty unmeasured stream settles at zero instead of consuming the
  whole remaining allowance, matching the blocking gate.
- A client that hangs up at the trailing [DONE] frame is charged what a
  client that stayed connected is charged. Reading the full answer and
  leaving was the cheapest way to use a capped key.
- A usage frame with no countable token key ({"total_tokens": 123}) is
  unmeasured, on both paths. It used to normalize to zero tokens and bill
  a delivered completion nothing.
- A blocking completion with content and no usage is priced from what it
  returned, not charged the lifetime allowance. The old rule billed a
  customer their entire budget for one request whose upstream omitted a
  field.
- include_usage is no longer forced on a client that explicitly declined
  it. Declining is priced from the delivery; a provider that ignores the
  usage frame we requested still fails closed.
- The 1-microcent floor: a short delivery estimates below the smallest
  representable amount, and a delivery that costs nothing is exactly what
  the cap must not allow.

Rows now record the estimated tokens behind an estimated charge, so a
non-zero cost never sits on a zero-token row.

Tests: the empty unmeasured stream, the disconnect at [DONE], tokenless
usage on both paths, and the re-split of the fail-closed cases (requested
and dropped usage vs a client that declined it).
A debug print guarded by an env var was left inside _unmeasured_charge
while working through the settlement rework: it reads an environment
variable on every unmeasured settlement and writes to stdout inside a
request handler.
The streaming retry loop detaches each commit so the in-flight stream
cannot abort it, then waits for the task on the cancellation path. When
the task is cancelled itself, that await raises and the arm passed over it
with `pass`. Before enforcement that cost one log row; now it costs a
budget charge, because the row and the settlement share a transaction:
nothing is written, spent_microcents never moves, and nothing is logged.
The cap reopens for exactly the key whose settlement failed, with no
trace.

The arm now re-runs the write inside anyio.CancelScope(shield=True) —
the same primitive the rest of the teardown uses, and the only way an
await survives a cancellation already unwinding us. `_commit_row` is
idempotent on trace_id, so a commit that landed with a lost ack is
recognised rather than charged twice. If even the shielded retry cannot
land, the loss is logged with the amount instead of passing over it.

This belongs here rather than in the park PR: enforcement is what makes
the arm cost a charge, and this PR is where enforcement becomes live. A
review finding it on this diff is right.
… live helpers

_settle_unmeasured_stream died when settlement became one decision
function: no runtime path calls it anymore, and its logic lives in
_estimate_usage plus _unmeasured_charge. Carrying it (and a test file for
it) would ship dead code with a passing suite. The unit tests are
re-pointed at the live helpers so the character math and the countable-usage
gate stay pinned, and _blocking_delivery_has_content says which rung
consumes it.
…on it

A park larger than the remaining allowance moved nowhere and stopped the scan,
so the counter could sit below the cap on a key refused by a row nothing would
ever shrink. Apply the allowance oldest-debt-first and rewrite the oversized row
to its remainder, which keeps the invariant the fold exists to hold: either the
queue is empty or the counter is exactly on the cap. The over-claim stays a row,
so it still blocks and folds for free if the cap is raised.

created_at alone is not a total order (second resolution on SQLite, ties on
Postgres), and two workers computing the same fold have to agree on which row is
the partial one, so the scan breaks ties on trace_id.
`checkfirst` asks the catalog and then creates, and the ask cannot see another
worker's uncommitted CREATE, so two boots racing an upgrade both issue it and one
is rejected. That was the only startup DDL outside `_apply_ddl`, and on Postgres
the error aborts the transaction the rest of startup runs in: the worker never
comes up, and never reaches the fold this table exists to feed.

Generalise `_apply_ddl` to a callable so dialect-generated DDL goes through the
same savepoint, and teach `_already_applied` the shape this collision actually
takes in Postgres — a unique violation on the catalog, not an "already exists".
The retry handler rolls the failed attempt back before deciding, and that is an
await — so it is where a cancellation aimed at the request can land. An exception
raised inside a handler is not caught by the same try's other arms, so the
cancellation escaped past every give-up below it: the row was already lost, the
park never happened, and the key's cap reopened for the cost it had just been
served.

Treat it like the other arm's rollback failure and carry on to the give-up. The
next attempt then still finds the session poisoned, so what lands is one park;
what matters is that the settlement is accounted for rather than dropped.
`_insert_park` answered "not durable" to every failure except a unique
violation, including a COMMIT that applied and lost its ack. The caller then
stored the amount in `_unsettled` beside the row it had just written, another
worker folded and deleted the row, and the stale memory copy re-filed as a new
park under a `trace_id` that no longer collided — billing one delivery twice and
leaving the key pinned on its cap with no debt left to fold. Ask the database
what landed instead of guessing, and drop the memory hold for any row a fold
actually billed.

An unreadable park ledger folded into the total as zero, which dispatched a key
whose cap was held shut only by parks this worker could not see. The two reads
are not the same connection either: the counter rides the request's, the ledger
opens a new one, so a checkout timeout hid every park while the request worked
fine. `pending_parked_spend` now answers `None` for unknown and the pre-check
maps unknown onto the cap.

Oldest-debt-first was decided by `trace_id`, because `created_at` came from
`CURRENT_TIMESTAMP` — one second wide on SQLite, and a recovered outage re-files
a whole batch inside a single pre-check. The row stamps itself Python-side at
sub-second resolution so the tiebreak is the rare path it is documented as.
A commit whose acknowledgement was lost, combined with a durability probe that
also failed, parks an obligation whose charge is already in spent_microcents.
Folding it re-charges the same delivery, and because the fold deletes the park
row there is nothing left to correct it.

The request-log row and the charge share one transaction, so the log row proves
the charge landed: clear those parks without billing them, and drop the matching
in-memory hold so a later pre-check cannot re-file them.
`test_parked_queue_drains_oldest_first_as_the_cap_opens` failed on
Windows: the column default stamps `created_at` from the wall clock, and
two back-to-back `datetime.now()` calls return the SAME value there
(the clock ticks every ~15.6ms), so two parks written moments apart tied
and fell through to the `trace_id` tiebreak. "t-new" sorted before
"t-old", the younger debt was billed first, and the fold trimmed the
older one instead. On ubuntu CI the stamps differ, the test passes, and
the failure mode stays invisible.

Pin the stamps in both tests so the assertions are about fold ordering
rather than the host, and verified the FIFO test still bites by reversing
the offsets: it then fails with exactly the original `{'t-old': 1500}`.

The ordering guarantee is real but weaker than the docstring claimed.
`trace_id` is a uuid4, so parks sharing a stamp break the tie arbitrarily
— no schema change here because nothing in the accounting depends on it:
the parked total is conserved either way and the counter still reaches
min(cap, spent + debt). Only which row is trimmed differs, so the
docstring now says so and a new test pins the invariant that holds either
way (total conserved, remainder stays visible, queue drains to empty).
…ssing-usage fail-closed on delivered content

The streaming commit retry loop detached each commit task and only
awaited it on the give-up path; when the task itself was cancelled
(loop teardown, direct cancel), the inner `except BaseException: pass`
swallowed it, so the delivered stream recorded no row, charged nothing,
and filed no park -- the cap reopened for exactly the key whose
settlement failed. Run the same `_give_up_settlement` probe-and-park as
every sibling arm before propagating.

The blocking fail-closed arm's fail-closed raise fired whenever `usage`
was absent -- including an empty 200 -- while the same delivery with
`usage: {0, 0}` correctly settled at 0. Gate the missing-usage
disjunct on delivered content so an empty response can never exhaust
the key.
Continuum-AI-Corp#161 simplified the pre-check to a plain counter read, which is correct
there because nothing parks yet. This rung reintroduces budget_precheck
precisely so a parked obligation is folded before the 429 decision, and
routing the gate through read_spent would silently skip that: a key whose
settlement failed would keep dispatching as if it had never spent
anything, which is the entire state durable recovery exists to close.

Also reconciles the blocking settlement tests with the unified decision
function now in the tree: a usage-less blocking completion is priced from
what it returned (floored at one microcent), never charged the remaining
allowance, and the parked obligation a write outage leaves behind is that
same amount.
Ported from Continuum-AI-Corp#161, where the arm that swallowed a cancelled detached commit
task cost only a log row; enforcement is what makes it cost a charge, so
the fix belongs on the rung where enforcement lands.

This rung already had the stronger form: _give_up_settlement probes
durability and parks the obligation, so the cost keeps counting even when
the write can no longer land at all — which is why #161s shielded re-run
of the write is not reproduced here, and why #161s regression test (which
asserts the retry lands a row) is not either. test_streaming_commit_task_
cancellation_gives_up pins this path on this rung.
Rebasing onto Continuum-AI-Corp#161 brought its shielded-retry test along. It asserts a
retried write lands a row, which is what Continuum-AI-Corp#161 does; here the cancelled
commit task is probed and parked instead, which
test_streaming_commit_task_cancellation_gives_up already pins. Carrying
both would assert two different mechanisms for one arm.
@hasitpbhatt
hasitpbhatt force-pushed the budget/pr3-durable-recovery branch from db9b0b7 to e5cadb3 Compare October 8, 2026 10:06

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

✅ No findings — nothing to flag in this PR. Great work!

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 306 calls · 21M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@hasitpbhatt

Copy link
Copy Markdown
Contributor Author

/orcacode-review

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 273 calls · 16.5M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py

@orcacode-review orcacode-review Bot 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.

⚠️ Outdated review — the PR head changed from becff5f to 3cec25f during the review; these findings describe a diff the PR no longer has.
These findings describe becff5f40b08. Re-run the review to check the PR as it stands now.
The gate evaluated to blocked, but it is not enforced here: branch protection will not see it.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 276 calls · 17.3M tokens · 95% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py

@orcacode-review orcacode-review Bot 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.

🐳 OrcaCode Review

Found 1 issue in this PR: 🟠 1 P1.

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 298 calls · 19.8M tokens · 96% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

Comment thread app/routes/chat.py Outdated
@hasitpbhatt
hasitpbhatt force-pushed the budget/pr3-durable-recovery branch from 3cec25f to e5cadb3 Compare October 8, 2026 21:54
@hasitpbhatt hasitpbhatt closed this Oct 9, 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.

2 participants