Skip to content

feat(billing): classify gateway billing denials and rework the usage-limit modal for usage-based billing#3485

Merged
trunk-io[bot] merged 9 commits into
mainfrom
posthog-code/usage-billing-limit-errors
Jul 16, 2026
Merged

feat(billing): classify gateway billing denials and rework the usage-limit modal for usage-based billing#3485
trunk-io[bot] merged 9 commits into
mainfrom
posthog-code/usage-billing-limit-errors

Conversation

@adboio

@adboio adboio commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

tl;dr

  • updated error handling based on new error shape from #71343
  • updated usage limit alert modal copy
  • retry "helper" llm calls (titles, commit msgs) once with glm-5.2 after a 403 from the gateway
  • billing CTAs use existing getBillingUrlto nav user to posthog cloud billing settings

testing

scenario screenshot
session model gate pr3485-B-model-gate-modal.png
cloud org-limit pr3485-E-free-usage-modal-v2.png
generic usage limit (429) pr3485-F-generic-usage-limit.png

full vibe-description:

Problem

Usage-based billing is replacing seats, and the app misroutes the gateway's new denials: the free-tier model-gate 403 (PostHog/posthog#70951) reads as a fatal session error ("Connection lost" + recovery loops), the org credit-bucket 429 isn't recognized, and the usage-limit modal still pitches the retired Pro plan.

Changes

First of a small stack (errors/copy → billing surfaces → model gating):

  • classifyGatewayLimitError in @posthog/shared, matched against the error bodies from PostHog/posthog#71343. Only two causes exist under usage-based billing — model_gate and org_limit; anything else gets generic no-CTA copy. Model-gate 403s no longer classify as fatal session errors.
  • The usage-limit modal always renders usage-based copy (add a payment method / manage the org spend limit) — no flag, no seat-era branch. Safe in any release order relative to the cutover flip: the gate can't fire until the server-side switch turns on, and the seat-era copy was already a dead end in production.
  • Helper prompts (commit messages, titles, PR copy) run on the free-tier model for free-tier orgs, keyed on the new optional code_usage_subscribed usage field (PostHog/posthog#71315), with a single gate-403 retry as the cold-start backstop.

Why: the gateway's model gate can only flip once the app stops treating its 403 as fatal — this is the must-ship slice of the cutover client work. Ships in the same release as PR 2 (billing surfaces), which replaces the seat-era plans page.

How did you test this?

Automated only: typecheck and Biome clean across shared/api-client/core/ui; suites green (shared 602, core 2291, ui 1659) with new tests for the classifier (envelope, message-only, and legacy body shapes), the fatal-403 exclusion, helper routing, and the modal copy matrix. Not covered: a visual pass over the new modal copy.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

…l for usage-based billing

First of the usage-based billing cutover stack (errors/copy -> billing
surfaces -> model gating):

- classifyGatewayLimitError/extractGatedModel in @posthog/shared; the
  free-tier model-gate 403 no longer classifies as a fatal session error.
- sessionService routes gateway billing denials to the usage-limit modal
  with a cause; the modal renders cause-aware usage-based copy behind the
  new posthog-code-usage-billing flag (off = seat-era copy unchanged).
- Helper prompts route to the free-tier model for orgs known unbilled via
  the optional code_usage_billed usage field, with a single gate-403 retry
  as the cold-start backstop.
- Seat handling keys off the API's 410 seat_product_retired: auto-provision
  stops re-attempting and explicit upgrades surface a clear message.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
@trunk-io

trunk-io Bot commented Jul 15, 2026

Copy link
Copy Markdown

😎 Merged directly without going through the merge queue, as the queue was empty and the PR was up to date with the target branch - details.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit d8738b9.

adboio added 4 commits July 15, 2026 17:12
…410 copy

Seats are gone entirely by cutover and the whole seat surface gets deleted
after it, so special handling isn't worth carrying: SeatService goes back to
main unchanged (auto-provision already fails soft on any create error), and
the 410 keeps only a typed error plus readable copy in classifySeatError for
the seat-era UI still visible until the flag flips.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
…ll comments

Usage-based billing has exactly two denial causes — model_gate and
org_limit — so the per-user burst/sustained cause taxonomy and the
bucket-to-cause derivation are gone. Per-user valve messages (a pre-cutover
mechanism) now land on the generic no-CTA fallback instead of an invented
"free daily limit" story, and preflightCloudUsage is back to main's shape
plus a single org_limit cause. Also strips narrative comments across the
diff down to the constraints the code can't show.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
…ng flag

The seat-era copy this branch preserved is already a dead end in production
(Pro upgrades return 410 Gone), so there is nothing to keep behind a flag:
the usage-limit modal renders usage-based copy unconditionally. Pre-flip
this degrades safely — the model gate can't fire until the server-side
cutover switch flips, and valve hits land on the generic fallback — so
merge/release order relative to the flip doesn't matter.

Deletes seatEraLimitContent, the USAGE_BILLING_FLAG, the modal's seat/Pro
plumbing (useSeat, the support mailto), and the store's unused bucket/isPro
state; threshold and preflight callers pass only what the modal reads.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
… the model

The regex existed only to fish the model id back out of our own error prose
so the modal could name it — and the session path only ever sees the
stringified message, so even a structured gateway field would need the same
string-fishing. The user just sent a message on the model they picked, so
"This model isn't included in the free tier" is unambiguous. Deletes the
regex, the store's model field, and the show-args threading.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
adboio added a commit to PostHog/posthog that referenced this pull request Jul 15, 2026
…him (#71343)

## Problem

PostHog Code's usage-based billing cutover (#70951) gates unbilled orgs to the free-tier model list, but the gate's 403 goes out as a bare FastAPI `detail` string. Pre-cutover desktop builds classify errors by substring: they treat that 403 as a fatal session error and tear a healthy session down ("Connection lost" + recovery loops), and no client can tell which limit tripped a 429 without parsing prose.

**Why:** installed desktop builds can't be updated in time for the cutover — the gateway's error bodies are the only lever that reaches them, and this can deploy ahead of the model-gate flip.

## Changes

- The free-tier model-gate 403 now uses the standard error envelope with a machine-readable `code: "model_gate"`, and its message carries a `(rate_limit)` suffix — a compat shim that routes pre-cutover PostHog Code clients to their usage-limit modal instead of session teardown (documented inline; remove once those builds roll forward).
- Throttle 429s repeat the reason in `error.message` (SDK error strings often surface only the message) and carry the throttle scope as `error.code` (`billable_credits`, `user_cost_burst`, `user_cost_sustained`, …), so clients can classify the limit cause.

Companion client work: [PostHog/code#3485](PostHog/code#3485) classifies these bodies into cause-aware billing UX.

## How did you test this code?

Automated only: the enforcement-path gate tests now pin the new envelope (`code`, the `(rate_limit)` suffix), the rate-limiting endpoint test pins the message/reason/code shape, and a TestClient-level test pins the gate 403's wire body end-to-end (top-level error envelope with `code: "model_gate"` and the `(rate_limit)` suffix — never `{"detail": ...}` nesting) through the raise site and the exception-handler unwrap together. All would fail if the shim or codes were dropped. Full llm-gateway suite green locally (1239 passed, 50 skipped, 30 xfailed, 22 xpassed).

## Automatic notifications

- [ ] Publish to changelog?
- [ ] Alert Sales and Marketing teams?

## Docs update

None — internal error-body shape.

## 🤖 Agent context

**Autonomy:** Human-driven (agent-assisted)

PostHog Code session. Part of the usage-based billing cutover client plan: this PR makes gateway billing denials classifiable by both pre- and post-cutover desktop builds.

---
*Created with [PostHog Code](https://posthog.com/code?ref=pr)*
@adboio adboio requested a review from a team July 15, 2026 23:31
… release

The readable-copy branch only served the window where the seat-era plans
page was live without PR 2's replacement. PR 1 and PR 2 ship as one release,
so that window is zero: the typed 410 error, the classify branch, and their
tests go, leaving the PR with no seat-API changes at all (auto-provisioning
already fails soft on any create error).

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
@adboio adboio force-pushed the posthog-code/usage-billing-limit-errors branch from ad97d99 to 2e09cf7 Compare July 15, 2026 23:34

adboio commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

…code_usage_billed

posthog#71315 shipped the bit as code_usage_subscribed; the schema listened
for code_usage_billed, which zod strips, so the client would have read
"unknown" forever. Rename the field and align internal naming to subscribed.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
@adboio adboio marked this pull request as ready for review July 16, 2026 01:03
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(billing): the usage endpoint field i..." | Re-trigger Greptile

Comment thread packages/core/src/sessions/sessionService.ts Outdated
Comment thread packages/core/src/llm-gateway/llm-gateway.ts
Comment thread packages/core/src/llm-gateway/llm-gateway.ts
@veria-ai

veria-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

adboio added 2 commits July 15, 2026 21:18
…e subscription memo to the org

Greptile P1s: an org_limit classification only opened the modal when the
message also carried generic rate-limit wording, and the singleton's learned
code_usage_subscribed bit survived org switches, pinning helpers of a
subscribed org to the free-tier model. The modal now keys off the classified
cause directly, and the memo clears when the auth org changes.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
Local testing surfaced a coherence gap: hitting the free valve during a
session showed the generic "try again shortly" modal, while the cloud
pre-flight showed "Free usage used up / Add payment method" for the same
state. The valves only exist for unsubscribed orgs, so classify their 429
wording as org_limit — both paths now land on the free-tier upsell. The
free-tier copy drops the "billing period" over-claim (a daily valve resets
sooner) and carries the reset hint when the caller has one.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
@trunk-io trunk-io Bot merged commit 092903f into main Jul 16, 2026
32 checks passed
@trunk-io trunk-io Bot deleted the posthog-code/usage-billing-limit-errors branch July 16, 2026 04:59
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.

2 participants