Skip to content

fix(billing): build the mid-run usage card from the admitted payer and model the empty new-turn 402 - #8502

Merged
waleedlatif1 merged 3 commits into
stagingfrom
fix/midrun-usage-card-payer-and-402-contract
Oct 1, 2026
Merged

waleedlatif1 merged 3 commits into
stagingfrom
fix/midrun-usage-card-payer-and-402-contract

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Addresses two minor findings from the release review of the mid-run usage gate.

1. Mid-run upgrade card named the actor's plan, not the admitted payer's (release review threads on usage-upgrade.ts / update-cost / validate)

Root cause. resolveUsageUpgradePayload picks the card from the attribution snapshot when a run is attributed. Otherwise it falls back to getHighestPrioritySubscription(userId), the actor's current subscription. Two mid-run callers reach that fallback for direct-v1 runs even though they already hold the payer the verdict was read for:

  • the cost callback (readUsageStanding in app/api/billing/update-cost/route.ts)
  • the continuation re-check in app/api/copilot/api-keys/validate/route.ts

Whether the run pauses was always correct, because the verdict reads the payer's own usage. But if the actor's subscription or organization membership changed mid-run, the card could show the wrong action (upgrade_plan instead of increase_limit) or the wrong wording ("ask your admin" instead of "billing settings").

Fix.

  • readMidRunAccountUsageVerdict already loads the payer's billing entity and subscription to read usage. An exceeded verdict now carries them as payer.
  • resolveUsageUpgradePayload takes a UsageUpgradePayer ({ billingEntity, payerSubscription }). An attribution snapshot already has that shape, so attributed runs are unchanged.
  • Both mid-run callers pass the attribution or, for direct-v1 runs, verdict.payer.
  • This removes the second subscription read. The card is now decided without a query on both mid-run paths, so the 1 s card-read deadline in the cost callback can no longer fire. It is removed, along with its test. The verdict-read deadline is unchanged.

Behaviour change. A direct-v1 run that goes over its limit mid-run gets the card for the payer it was admitted under. Admission and new-turn paths are unchanged.

2. The contract's 402 schema required a body the new-turn refusal never sends

Root cause. validateCopilotApiKeyContract.response.statusSchemas[402] declared validateCopilotApiKeyRefusalSchema, but a new turn's usage refusal is an empty 402. This has been unchanged since before the release, and the schema's own TSDoc already says so. Continuations do send a body.

Fix. Make the 402 schema .optional() so the contract describes both shapes. No runtime behaviour changes: no consumer validates this response against the contract.

Findings judged not worth changing here

  • Settle window assumes a run deadline (USAGE_SETTLE_MS). This is pre-existing, already documented in its TSDoc, and affects only the cached usage analytics. It matters only if the worker's run deadline is disabled; invoices, threshold billing and the gate read live sums. It is left for the change that removes the deadline.

Test plan

  • New tests fail on origin/staging source and pass with the fix (checked by reverting the source files only):
    • update-cost route test: a direct-v1 run whose payer is an organization on a paid plan, while the actor's personal plan differs, gets increase_limit with organization wording.
    • validate route test: a direct-v1 continuation refusal gets its admitted payer's card.
    • validate route test: a new turn's empty 402 parses against the contract's declared 402 schema.
  • vitest over app/api/billing, lib/billing, app/api/copilot, lib/mothership (one unrelated agent-cli test flaked under load and passes alone)
  • bun run lint
  • bun run type-check (apps/sim)
  • bun run check:audits
  • test:integration: not run, because no DB or Redis behaviour changes

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 1, 2026 1:44am UTC

Request Review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical risk] Changes how billing verdicts determine which plan to show users.

The PR appears safe to merge; no outstanding findings remain.

Summary

The PR builds mid-run usage cards from the admitted payer’s billing entity and subscription, rather than falling back to the actor’s current subscription. It also makes the 402 response schema accept the empty body sent for a new-turn refusal. The change since the previous review adds the missing assertion that the new-turn body is empty.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Mid-run usage read] --> B{Limit exceeded?}
  B -- Yes --> C[Admitted payer and subscription]
  C --> D[Usage upgrade card]
  E[New-turn usage refusal] --> F[Empty 402 body]
  F --> G[Optional 402 response schema]
Loading

Reviews (2) · Last reviewed commit: "test(copilot): pin the new-turn usage re..."

Comment thread apps/sim/app/api/copilot/api-keys/validate/route.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

…d model the empty new-turn 402

A direct-v1 run's mid-run verdict reads the payer saved in its account
decision, but the upgrade card was resolved from the actor's current
subscription, so a payer/actor mismatch picked the wrong action and copy.
The exceeded account verdict now carries the payer and subscription it
already read, and update-cost and the validate continuation pass it to
resolveUsageUpgradePayload instead of a second lookup.

The validate contract declared every 402 as a JSON refusal, while a new
turn's 402 has no body; the 402 schema now allows the empty body. The
wire is unchanged.
…upgrade card

Every exceeded verdict that reaches update-cost now carries its payer, so the
deadline-bounded actor subscription lookup could no longer run. Remove the
parameter, its call-site argument, the stale TSDoc, and the test that passed
without exercising it.
@waleedlatif1
waleedlatif1 force-pushed the fix/midrun-usage-card-payer-and-402-contract branch from 032e7cc to f0611c3 Compare October 1, 2026 01:44
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 2091953 into staging Oct 1, 2026
23 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/midrun-usage-card-payer-and-402-contract branch October 1, 2026 01:47

This branch was previously deployed

1 inactive deployment
Preview — f0611c32 Deployed Oct 1, 2026 by vercel[bot]
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