Skip to content

Harden billing data integrity and audit capacity - #13

Merged
maiphucgiang merged 7 commits into
mainfrom
fix/usage-billing-integrity
Sep 14, 2026
Merged

maiphucgiang merged 7 commits into
mainfrom
fix/usage-billing-integrity

Conversation

@maiphucgiang

Copy link
Copy Markdown
Owner

What changes

Seven commits making balances and usage honest about incompleteness:

Billing API responses

  • fetch_request_usage now validates the business code and requires the data.data/total structure — an HTTP 200 with a failing code or missing structure is an error, not a silent zero (a missing total also used to stop pagination after page 1)
  • fetch_credits distinguishes a legitimately empty balance (Accounts key present) from a missing structure (error), so a malformed success can no longer overwrite history with zero

Pagination

  • fetch_credits pages until a short page (was: first 100 packages only); fetch_request_usage keeps its 30-page cap but both now return partial: true when capped, the flag is stored in the ledger, surfaced by aggregate_credits, and exposed on billing responses

Sync resilience

  • Usage sync keeps one snapshot per account (usage_daily_accounts): a failed account keeps its last good data instead of vanishing from the aggregate, and the published view carries partial + stale_accounts. Removed/disabled credentials drop out of the aggregate
  • daily_costs are priced per site per day at that site's price instead of one blended average; the balance identity with total_usage still holds

Tool-call retries

  • Discarded generations after malformed tool calls are recorded in the request's attempts with sequence number and total_tokens; the retry budget is now --tool-call-max-retry / CODEBUDDY2API_TOOL_CALL_MAX_RETRY (default 3, 0 disables)

Audit capacity

  • Audit schema v2: ingest dedup rows gain created_at (backfilled on migration) and expire at the same retention cutoff as their details, so the dedup table can no longer grow without bound; aggregates are still never deleted by budget/retention

Docs: new "Billing data integrity" section and updated retry-row in docs/advanced.md / docs/advanced.zh-CN.md.

Verification

Full suite on the branch: 649 passed, 2346 subtests, including new regression tests for failure semantics, pagination caps, per-account snapshot retention, per-site pricing arithmetic, retry-attempt recording, and the v1→v2 audit migration.

An HTTP 200 with a failing business code or a missing result structure
was treated as a valid empty/zero response: fetch_request_usage never
checked the business code (and a missing total silently stopped
pagination after the first page), and fetch_credits could not tell a
missing Accounts structure from a legitimately empty balance. Both now
raise on a non-zero business code or missing data.data/total/Accounts
structure, while a present-but-empty result remains a valid zero.
Verified by new failure-semantics tests; existing paging and HTTP tests
unchanged and passing.
fetch_credits read only the first page of 100 packages and
fetch_request_usage stopped silently at the 6000-row cap, so balances
and usage could be understated while looking complete. Both now page
until a short page (with a hard cap) and return partial=True when the
cap is hit; the flag is stored in the credit ledger, surfaced by
aggregate_credits, and exposed on the billing totals so an incomplete
snapshot is visible instead of pretending to be exact. Verified by new
pagination and page-cap tests (full suite passed).
Usage sync rebuilt the aggregate from scratch each round and published
it whenever at least one account succeeded, so an account whose fetch
failed saw its previously synced usage vanish while fetched_at claimed
a fresh sync. Each account now keeps its own snapshot in
usage_daily_accounts; a successful sync replaces only that account's
data, and a failed one keeps its history while the published view is
marked partial with stale_accounts listed. Snapshots of removed or
disabled credentials are excluded from the aggregate. Verified by a new
sync regression test covering failure, recovery, and credential removal
(test_credits.py passes).
The usage endpoint applied a single weighted-average price to every
day and model, so with mixed domestic/international usage the per-day
and per-model amounts were wrong even when the grand total happened to
be right. daily_costs is now computed from each site group's own by_day
at that site's price and then merged; the balance identity with
total_usage still holds because both derive from the same per-site
totals. The response also surfaces partial and stale_accounts when the
usage data is incomplete. Verified by a two-site different-days
arithmetic test and the existing billing identity tests.
…igurable

Up to three extra generations after malformed tool calls consumed real
credits, but only the final usage was recorded. Each discarded
generation now lands in the request's attempts with its sequence number
and total_tokens, and the budget moves from a hardcoded constant to
--tool-call-max-retry / CODEBUDDY2API_TOOL_CALL_MAX_RETRY (default 3,
0 disables retries). Verified by a new endpoint test covering
retry-with-usage recording and the zero budget (affected test files
pass).
The ingest dedup table had no time dimension and intentionally outlived
all detail eviction, so it grew monotonically with unique requests and
the detail byte budget could not bound the database file. Schema v2
adds created_at to ingest (migrated from v1 with timestamps backfilled
from the matching detail row), expires dedup rows at the same
retention cutoff with bounded batches, and reports them in
pending_cleanup. Within the retention window dedup is unchanged;
beyond it an ancient id may record again instead of being kept forever.
Aggregates are still never deleted by budget or retention. Verified by
a v1-migration regression test and the updated retention contract tests
(test_audit_store.py passes).
Sync the advanced guides: partial/stale_accounts semantics on billing
endpoints, per-site daily pricing, the per-account snapshot behavior on
sync failures, and the new --tool-call-max-retry option with its credit
cost note.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T12:26:49.714016Z 958fa7a PR opened
🔒 Security Review Completed 2026-09-14T12:28:08.826457Z 958fa7a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@maiphucgiang
maiphucgiang merged commit 28647ad into main Sep 14, 2026
7 of 8 checks passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 958fa7a39d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/credits.py
if page > CREDITS_MAX_PAGES:
partial = True
break
rows = _fetch_accounts_page(client, url, headers, page, retry_empty=(page == 1))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry empty responses on every credits page

When an account has more than one page and the upstream’s known transient empty Accounts response occurs on page 2 or later, retry_empty is false, so the loop accepts that empty page as the end and returns an understated balance with partial: false. Retry empty results on subsequent pages as well, or use authoritative pagination metadata to distinguish a real terminal page.

Useful? React with 👍 / 👎.

Comment thread converter.py
Comment on lines +1263 to +1265
if cred_id in stale:
partial = True
stale_out.append(Path(cred_id).name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark failures that have no prior usage snapshot

On an initial sync where one enabled account succeeds and another fails, the failed account is in stale but absent from accounts, so this condition is never evaluated for it. The successful account is then published with partial: false and no stale_accounts, making an incomplete aggregate appear exact; derive missing failures from stale ∩ enabled, not only from snapshots already present.

Useful? React with 👍 / 👎.

Comment thread converter.py
Comment on lines +1884 to +1885
if t.get("partial"):
out["partial"] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate partial balances through the subscription endpoint

Only the usage response forwards this marker. If credit pagination reaches its cap, _billing_totals() has partial=True, but /v1/dashboard/billing/subscription still returns truncated remaining-credit and hard-limit values without any indication that they are incomplete, despite the documented balance-integrity contract. Forward the partial state, and relevant stale-account information, from the subscription response too.

Useful? React with 👍 / 👎.

Comment thread converter.py
Comment on lines +2595 to +2596
observe_attempt("tool_args_retry", attempt=tool_attempt, max_attempts=budget,
total_tokens=discarded.get("total_tokens"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow discarded usage fields through attempt sanitization

In production, observe_attempt immediately passes this metadata through app.audit_store.safe_attempt, whose numeric allowlist excludes both total_tokens and max_attempts; the persisted attempt therefore contains only stage and attempt. The new test mocks observe_attempt, bypassing this sanitizer, so it does not catch that discarded-generation usage still disappears from the audit record.

Useful? React with 👍 / 👎.

Comment thread converter.py
Comment on lines +2589 to 2591
budget = CONFIG.get("tool_call_max_retry", _TOOL_CALL_MAX_RETRY)
if detector.detected or not body.get("tools") or tool_attempt >= budget:
raise UpstreamResponseError(502, b"Invalid upstream tool_calls after retries")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record the generation that exhausts the retry budget

When the budget is zero or every permitted retry also returns malformed tool calls, this branch raises before observe_attempt runs. The last consumed—and potentially billed—generation is therefore absent from the request’s attempts even after the sanitizer is fixed, making failed/exhausted requests systematically omit one generation’s usage.

Useful? React with 👍 / 👎.

@maiphucgiang
maiphucgiang deleted the fix/usage-billing-integrity branch September 16, 2026 22:29
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