Skip to content

feat(budget): spend accounting schema, migration and library [1/4] - #160

Open
hasitpbhatt wants to merge 4 commits into
Continuum-AI-Corp:mainfrom
hasitpbhatt:budget/pr1-schema
Open

hasitpbhatt wants to merge 4 commits into
Continuum-AI-Corp:mainfrom
hasitpbhatt:budget/pr1-schema

Conversation

@hasitpbhatt

@hasitpbhatt hasitpbhatt commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Orca-Code-Review — push 3

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

✅ no blocking findings

What this PR is

PR 1 of 4 replacing #91 (same feature, split for reviewability; #91 will be closed once this stack is up).

Scope: the budget schema and the accounting library, and nothing else. No request-path behavior changes — deliberately. This PR does not make budget_limit_cents enforced, and it is not supposed to.

PR Lands
#160 (this) spent_microcents column, startup migration, packages.auth.spend library + unit tests
#161 Wires is_exhausted / charge_budget into execute_chat — the enforcement itself
#162 Durable recovery: parks settlements that outlive their retries
#163 Fair pricing for adapter faults and client hangs

On the open "dead code" P1

The review bot flagged charge_budget / is_exhausted / read_spent as having zero callers in app/ and called that dead code. That is accurate about this diff and is the intended shape of the stack: a landable-but-unused library is how the schema and the atomic-charge semantics get reviewed on their own, before they change anyone's requests. The wiring exists and is up for review right now in #161, which adds the pre-dispatch is_exhausted check and the single-transaction charge_budget(commit=False) alongside the RequestLog insert.

Merging #160 first is safe: it only adds a column, a startup migration and a module nothing imports.

Included

  • spent_microcents counter + BIGINT widening on ApiKey; spend index on requests_log.
  • ensure_budget_columns startup migration (upgrade seeding from request history, concurrent-boot race guard), wired into lifespan.
  • packages.auth.spend: charge_budget (single atomic UPDATE, never exceeds cap, clamps instead), read_spent, is_exhausted.
  • Unit coverage: migration upgrade / idempotency / racing boot; atomic charge including concurrent charges for one key.

Review state

OrcaCode Review posted CHANGES_REQUESTED with 1 P1 on push 1 — the stack-scope item discussed above. It is not currently passing, and this description previously claimed otherwise; corrected.

Tests: targeted unit suite green. Lint: ruff clean.

@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: 174 calls · 5.1M tokens · 92% 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
@hasitpbhatt hasitpbhatt changed the title feat(budget): spent accounting, startup migration, and atomic charge feat(budget): spend accounting schema, migration and library [1/4] Sep 25, 2026
The upgrade seed is a correlated SUM over requests_log, and the index that
would serve it was created further down the same function — so the one boot
that runs the seed was also the one that could not use the index, and later
boots do neither. Build the index first and pin the order with a test.

Also states the two contracts the schema leaves implicit: the seed counts
soft-deleted request rows on purpose, because restoring an accrued lifetime
total can only ever tighten a cap, and cap_microcents is budget_limit_cents
scaled to microcents rather than the column itself.
The seed ran only inside the branch that added the column, and the two
statements do not vouch for each other: on SQLite the ALTER is durable the
instant it executes while the seed is DML in the transaction a kill — or the
`database is locked` this very aggregate provokes on an upgrade that overlaps
the old machine's writes — rolls back. Gating on the column's absence made that
half-applied boot the only one that could ever have seeded, so every key
predating the release kept a full fresh allowance forever, silently, which is
exactly the outcome the seed exists to prevent.

It runs on every boot now, restricted to keys that hold a cap, and the
statement is idempotent because a log row and its charge are one commit — a key
already holding spend has nothing to restore.

Three more from the same review:

- Gate the Postgres BIGINT widen on the reflected type rather than the column's
  name, which was present forever and so took ACCESS EXCLUSIVE on api_keys at
  every start.
- Correct the model comment justifying that widen with a client-supplied budget
  no route accepts, in the wrong unit.
- Make the upgrade tests able to fail: the legacy fixture had one key and one
  log row, so a seed that dropped its correlation predicate and stamped every
  key with the table total passed. It now has three keys with three histories,
  pins the half-applied boot above, and the concurrency test runs over a file
  instead of `:memory:`'s StaticPool, where two "independent" sessions shared
  one connection and the atomic guard never met a concurrent writer.

@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: 153 calls · 5.7M tokens · 92% 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
)
if commit:
await db.commit()
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 P1 Wire the budget gate and charge into the request path, or the cap is never enforced and the seeded counter is permanently stale

The new budget subsystem is entirely inert in production. grep across app/ and packages/ shows the only references to is_exhausted/charge_budget/read_spent are the unit tests; no request path (middleware auth, key_validator, deps, execute_chat, native-protocol adapters) calls them, and nothing advances spent_microcents after boot. The existing equivalents this feature mirrors all run per request: the allowlist gate in execute_chat (chat.py:365) rejects before dispatch, validate_api_key checks is_active/revoked_at on every request, and the RequestLog writeback records each request's cost. The new "hard lifetime limit" does none of these: (1) a key with budget_limit_cents set is dispatched past its cap indefinitely — the spend limit the feature claims to enforce ("enforces ApiKey.budget_limit_cents", "the cap is a hard lifetime limit", "so the cap holds even under concurrent requests") is never applied; (2) the migration's every-boot seed (migrate.py:113, WHERE spent_microcents = 0) sets the counter once from historical logs, and since nothing charges after that, the counter is a frozen boot-time snapshot that diverges from actual spend the moment traffic flows — the module's own invariant "a log row and its charge are one commit, so this SUM is the lifetime counter" has no code that maintains it. An operator who sets budget_limit_cents believing this feature caps the key gets no limit and a counter that under-reports spend (for a capped key with history, spend is under-counted by everything after the last boot; for a key created post-boot with a budget and no history, the counter stays 0 forever). Fix: in execute_chat (and the native-protocol surfaces via log_status/execute_chat), before dispatch call is_exhausted when kc.budget_limit_cents is not None (scale by MICROCENTS_PER_CENT) and reject with 429/403; after the upstream call, call charge_budget(commit=False) in the same transaction that inserts the RequestLog and commit once, including on the streaming finalize path where the row is written (and, since chat.py's blocking path swallows commit errors, treat a failed commit as a lost charge and let the next boot's seed recover only while the counter is 0).

@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: 169 calls · 6.4M tokens · 92% 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

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant