feat(aws-marketplace): bill a listing's LLM usage to AWS through a per-period metering tally
Merged
WA
3d ago+34462+34462+01234567890123456789012345678901234567890123456789-12-12-01234567890123456789
Summary
- Plans carry their usage pricing:
platform-with-usagebills the organization's billedmodelusage (BYOK ismodel_unbilledand never billed) asllm_creditsof $0.01;contractbills nothing beyond the contract. Repricing or changing what counts is a plan edit - Metering charges what changed in the books, not hours of rows:
usage_logcost is topped up in place (recordCumulativeUsage), so each run re-sums every billing period with a row that can still change (a request only raises its row while it runs, so a row settles after the longest execution timeout plus a day for late callbacks) and charges the whole units above that period's tallyaws_marketplace_usage_tally(organization, dimension, period): cost counted, units charged, and where the count starts. Fractions wait in the tally; units only move forwardaws_marketplace_usage_report(license, dimension, hour), the key AWS deduplicates on: a row gathers units while its hour is open and is sent once it closes, or at once for an ended license
- The charged license is the earliest-linked one in force (status active and not past
expiresAt). A license ends at the earlier of its passed expiry and the check that first finds it inactive, so overlapping licenses never both bill and a renewal handoff needs no partial-hour logic. With none in force, a license that ended within the hour AWS allows takes the usage at the hour it ended, and that record is sent at once (AWS's hour runs from the unsubscribe, which a check only finds later, so later tail usage is deliberately not billed) aws_marketplace_license.ended_at, stamped by the read that first finds a license inactive and cleared if access returns, so a cancellation AWS reports with no entitlement (and no expiry) still gets its final flushaws_marketplace_license.organization_linked_at, stamped wherever a license is linked; usage counts from it. A linked license without one (linked before it existed) is stamped by the first run that sees it- Sending:
BatchMeterUsagebyLicenseArn+CustomerAWSAccountId(noProductCode), 25 per call, grouped per license. Success / DuplicateRecord → reported; CustomerNotSubscribed or a license-level refusal → rejected and logged; unprocessed or transient → pending. Rows AWS won't take (24h old, or last month's after 06:00 UTC on the 1st) are rejected and logged before sending; aTimestampOutOfBoundsExceptionstill raised for a batch is narrowed by sending each row alone - A record's quantity is fixed once it is first sent (
sent_at, stamped under the organization's usage lock with the quantity re-read), since AWS keeps the first record it accepts: a retry resends the same quantity, and later usage waits for a record that can still take it - Cron
/api/cron/aws-marketplace-meterevery 15 minutes (scheduled in infra; SaaS fulfillment only runs on the hosted deployment, likeaws-marketplace-reconcile) - Report rows keep their license on organization deletion (
organization_idset null), so usage already charged is still sent - Migration 0456 is additive only
Type of Change
- New feature
Testing
metering.integration.ts(17, real Postgres; only the AWS metering client is mocked) +lifecycle.integration.tslink-time andended_atcoverage; marketplace integration suites 89/89, unit 10/10- Each guard was removed in turn and its test went red: whole-period re-sum (in-place top-ups), send only after the hour closes, send an ended license at once, stop charging once its end-hour record is sent, skip a license past expiry, stamp unmarked links, period settle window, category filter, count from link,
ended_atstamp / keep / clear, callers relinking only on change, the charge-side and send-sidesent_atfreeze, out-of-range isolation, month-end cutoff and its grace, the long-run row settle window, expiry as end, expired rows sent at once, report rows surviving org deletion bun run lint,tsc,check:migrations,check:audits(the only local failure is the gitignored docs.contentregistry CI generates), drizzle-kit generate reports no drift
Checklist
- Code follows project style guidelines
- Self-reviewed my changes
- Tests added/updated and passing (new tests pass the
test-auditauthoring gate) - No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)