Skip to content

Detect subscription-plus-usage-credits billing and capture its per-turn fields (issue #12) - #16

Merged
smolinari merged 1 commit into
mainfrom
dev
Oct 12, 2026
Merged

smolinari merged 1 commit into
mainfrom
dev

Conversation

@smolinari

Copy link
Copy Markdown
Member

Summary

The detection-and-capture half of #12. A Claude subscription with extra usage enabled spills into per-token "usage credits" once the plan allowance runs out; the logs record no cut-over and no per-turn pool attribution, so this changes what Modelog says, never what it computes.

What changed

  • Third billing state. detectBilling() now also reads oauthAccount.hasExtraUsageEnabled and yields "subscription-extra", with dashboard and MCP copy stating plainly that the estimated/billed split is not knowable from disk. billingType alone was never stable enough to carry this — switching an account's payment method has been observed to flip it. Precedence: prepaid still wins (per-token billing is already real money). The modelog.billingMode setting stays a binary override; only detection can land on the third state.
  • Per-turn capture. Turn gains serviceTier (usage.service_tier — Anthropic's own field, distinct from Codex's OpenAI tier riding in speed) and fallbackCredit (usage.fallback_credit, the slot built for marking credit-billed turns; observed only as null, so a populated value is serialized verbatim for the day its shape is known, displayed nowhere, priced never).
  • Fail-closed pricing. serviceTier joins the modifier check: "standard" prices at 1× from a new service_tier table in pricing.json; an unrecognised value — the card's Batch discount is real but its wire value unconfirmed against a log — yields an unavailable cost rather than a wrong one.
  • Store schema v6 (two columns, additive migration — no row wipe needed), CAPTURE_VERSION 3 so pre-capture rows report the gap honestly through uncapturedFields.

Validation

  • 333 tests green, including new test/billing.test.ts modeled on the real oauthAccount shape of the account that crossed into usage credits.
  • Real-capture end-to-end: all 290 deduplicated turns carry "standard" and still price to the cost-state-exact $61.33, zero unpriced — the new fail-closed modifier cannot regress existing data.

Not in this PR

cost-state reconciliation ingestion (#12 item 3) — needs its own design pass; #12 stays open to track it.

🤖 Generated with Claude Code

…its per-turn fields

A Claude subscription with extra usage enabled spills into per-token
"usage credits" once the plan allowance runs out (issue #12). The logs
record no cut-over and no per-turn pool attribution, so this changes
what Modelog says, never what it computes.

detectBilling() now also reads oauthAccount.hasExtraUsageEnabled and
yields a third mode, "subscription-extra", with copy stating plainly
that the estimated/billed split is not knowable from disk. billingType
alone was never stable enough to carry this: switching an account's
payment method has been observed to flip it.

Turn gains serviceTier (usage.service_tier — Anthropic's own field,
distinct from Codex's OpenAI tier riding in `speed`) and fallbackCredit
(usage.fallback_credit, the slot built for marking credit-billed turns;
observed only as null, so a populated value is serialized verbatim for
the day its shape is known). serviceTier joins the fail-closed modifier
check: "standard" prices at 1x from a new service_tier table in
pricing.json, and an unrecognised value — the card's Batch discount is
real but its wire value unconfirmed — yields an unavailable cost rather
than a wrong one.

Verified against the real captured session: all 290 deduplicated turns
carry "standard" and still price to the cost-state-exact $61.33 with
zero unpriced.

cost-state reconciliation ingestion (issue #12 item 3) is deliberately
not here; it needs its own design pass.

Assisted by Claude Code (Claude Fable 5).
@smolinari
smolinari merged commit a058001 into main Oct 12, 2026
2 checks passed
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