fix(billing): build the mid-run usage card from the admitted payer and model the empty new-turn 402 - #8502
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
|
…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.
032e7cc to
f0611c3
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
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.
resolveUsageUpgradePayloadpicks the card from the attribution snapshot when a run is attributed. Otherwise it falls back togetHighestPrioritySubscription(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:readUsageStandinginapp/api/billing/update-cost/route.ts)app/api/copilot/api-keys/validate/route.tsWhether 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_planinstead ofincrease_limit) or the wrong wording ("ask your admin" instead of "billing settings").Fix.
readMidRunAccountUsageVerdictalready loads the payer's billing entity and subscription to read usage. Anexceededverdict now carries them aspayer.resolveUsageUpgradePayloadtakes aUsageUpgradePayer({ billingEntity, payerSubscription }). An attribution snapshot already has that shape, so attributed runs are unchanged.verdict.payer.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]declaredvalidateCopilotApiKeyRefusalSchema, 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
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
origin/stagingsource and pass with the fix (checked by reverting the source files only):update-costroute test: a direct-v1 run whose payer is an organization on a paid plan, while the actor's personal plan differs, getsincrease_limitwith organization wording.validateroute test: a direct-v1 continuation refusal gets its admitted payer's card.validateroute test: a new turn's empty 402 parses against the contract's declared 402 schema.vitestoverapp/api/billing,lib/billing,app/api/copilot,lib/mothership(one unrelated agent-cli test flaked under load and passes alone)bun run lintbun run type-check(apps/sim)bun run check:auditstest:integration: not run, because no DB or Redis behaviour changes