Skip to content

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

Merged
xizhuomengcontin merged 7 commits into
Continuum-AI-Corp:mainfrom
hasitpbhatt:budget/pr1-schema
Oct 6, 2026
Merged

xizhuomengcontin merged 7 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 5

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

@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

@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

@xizhuomengcontin

Copy link
Copy Markdown
Contributor

I verified this end-to-end against current main (31281db), and I think this finding describes the PR's stated scope rather than a defect in it.

The PR body says "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." Commit d36b960 put the same statement in the module docstring: "part 1 of the 4-part budget subsystem; request-path enforcement is wired in #161".

So I tested whether that claim is actually true, rather than taking it on trust:

check main this PR
fresh DB boots, columns created no spent_microcents column spent_microcents + budget_limit_cents present, seeded to 0
set budget_limit_cents, restart → migration seeds from history n/a spent_microcents = 15 vs SUM(cost_microcents) = 15 — matches
restart again n/a still 15 — idempotent, no double-count
budget_limit_cents=1 with spent_microcents=999999999 200 200 — the cap is genuinely not enforced

Plus ruff clean, 803 passed (×3), and my full regression battery across the merged chat/cache/auth behaviour is 15/15, identical to main — so "no request-path behavior changes" holds up.

The one thing I'd have wanted from an inert-schema PR is exactly what the last commit added: the CAST(... AS BIGINT) on the seed, so the startup migration doesn't fail on Postgres where SUM() over BIGINT returns numeric.

On the substance of the finding — it's factually right that nothing calls this yet, and it would be a real P1 if this PR claimed to enforce the cap. It doesn't. As written, the finding would block the first slice of any stacked change, since slice 1 is inert by construction.

Worth noting the stack has a separate constraint that does matter: #161 and #162 both say "do not merge this before #163", because their except AdapterError: branch returns without settling and charges a budgeted key its entire remaining lifetime budget for a server-side fault. #163 owns that fix. And #163 currently does not contain #162 (#162 pushed 6f8a2a4 on 09-27, after #163's last commit on 09-26), so the stack needs re-sequencing before anything past #160 lands.

@hasitpbhatt — if you agree this one is scope-by-design, could you resolve it? That unblocks #160, which is the only one of the four that merges cleanly onto current main today.

@hasitpbhatt

Copy link
Copy Markdown
Contributor Author

Agreed — scope-by-design, and your verification is exactly the evidence this needed. Thank you for running it end-to-end rather than taking the PR body on trust.

Acted on:

@xizhuomengcontin — one ask: since this repo requires a human approving review and your 09-30 write-up is the most thorough verification it's had, could you submit it as an APPROVE review if you're comfortable? (An issue comment can't clear the required-review gate.) If you lack permission to approve, flagging any maintainer to dismiss/resolve the remaining review state would work too.

Remaining on this PR: CI on push 4, and the OrcaCode re-review of the new head.

@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: 210 calls · 9.6M tokens · 94% 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

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.

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.
A `WHERE spent_microcents = 0` backfill can never see traffic logged after
the boot that seeded: until Continuum-AI-Corp#161 wires the per-request charge the counter
only moves at boot, so every later request under-reports lifetime spend
permanently. Repair to max(counter, log total) on every boot instead,
clamped to budget_limit_cents scaled to microcents and never lowered —
the clamp keeps charge_budget's deliberate post-cap under-count a fixed
point instead of an inflation target. Also tolerate Postgres surfacing a
lost CREATE INDEX race as a pg_class verror, which the boot previously
died on.
…ntity map

Session-default synchronize_session evaluates the SET in Python against a
loaded copy of the row and marks it dirty, so a caller that loaded the key
before charging (the documented Continuum-AI-Corp#161 flow: validate_api_key loads it, the
same session commits the charge with the log row) flushes that stale value
over the DB's atomic UPDATE on commit — silently dropping a concurrent
charge and under-counting lifetime spend. Disable sync on both UPDATEs and
pin it with a two-session regression test that fails without the fix.
The boot repair clamped spent_microcents against `budget_limit_cents *
10_000` with a literal, while `packages.auth.spend` defines the same
conversion as MICROCENTS_PER_CENT. Two unlinked copies of one unit
factor across packages: re-typing either silently rescales every capped
key's budget by 10,000x, which spend.py's own docstring warns about for
callers and nothing warned the migration author about.

Define the factor once in `packages/db.units`, below both layers, and
import it from `spend` and `migrate` alike so the dependency direction
stays auth -> db. spend re-exports it, keeping the name where the budget
primitives are documented to live.

Pinned by a test that perturbs the shared constant and reads the scale
back out of the statement the boot emits, so a re-typed literal fails
rather than drifting silently.
@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

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

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

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 199 calls · 9.2M tokens · 94% 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
xizhuomengcontin merged commit ec70055 into Continuum-AI-Corp:main Oct 6, 2026
3 checks passed
hasitpbhatt added a commit to hasitpbhatt/OrcaRouter-Lite that referenced this pull request Oct 8, 2026
…um-AI-Corp#160)

* feat(budget): add spent accounting, startup migration, and atomic charge

* fix(budget): let the boot seed use the index it aggregates through

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.

* fix(budget): make the lifetime-spend seed a repair, not a one-shot

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.

* fix(budget): cast migration seed to BIGINT and clarify schema library scope

* fix(budget): make the boot seed a cap-clamped monotonic repair

A `WHERE spent_microcents = 0` backfill can never see traffic logged after
the boot that seeded: until Continuum-AI-Corp#161 wires the per-request charge the counter
only moves at boot, so every later request under-reports lifetime spend
permanently. Repair to max(counter, log total) on every boot instead,
clamped to budget_limit_cents scaled to microcents and never lowered —
the clamp keeps charge_budget's deliberate post-cap under-count a fixed
point instead of an inflation target. Also tolerate Postgres surfacing a
lost CREATE INDEX race as a pg_class verror, which the boot previously
died on.

* fix(budget): keep the charge UPDATE from writing back through the identity map

Session-default synchronize_session evaluates the SET in Python against a
loaded copy of the row and marks it dirty, so a caller that loaded the key
before charging (the documented Continuum-AI-Corp#161 flow: validate_api_key loads it, the
same session commits the charge with the log row) flushes that stale value
over the DB's atomic UPDATE on commit — silently dropping a concurrent
charge and under-counting lifetime spend. Disable sync on both UPDATEs and
pin it with a two-session regression test that fails without the fix.

* fix(budget): scale the boot cap from the one microcent constant

The boot repair clamped spent_microcents against `budget_limit_cents *
10_000` with a literal, while `packages.auth.spend` defines the same
conversion as MICROCENTS_PER_CENT. Two unlinked copies of one unit
factor across packages: re-typing either silently rescales every capped
key's budget by 10,000x, which spend.py's own docstring warns about for
callers and nothing warned the migration author about.

Define the factor once in `packages/db.units`, below both layers, and
import it from `spend` and `migrate` alike so the dependency direction
stays auth -> db. spend re-exports it, keeping the name where the budget
primitives are documented to live.

Pinned by a test that perturbs the shared constant and reads the scale
back out of the statement the boot emits, so a re-typed literal fails
rather than drifting silently.
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