Skip to content

feat(billing): honor the gateway's free-tier model marks — authed fetches, safe defaults, locked pickers#3488

Merged
trunk-io[bot] merged 4 commits into
mainfrom
posthog-code/usage-billing-model-gating
Jul 16, 2026
Merged

feat(billing): honor the gateway's free-tier model marks — authed fetches, safe defaults, locked pickers#3488
trunk-io[bot] merged 4 commits into
mainfrom
posthog-code/usage-billing-model-gating

Conversation

@adboio

@adboio adboio commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

tl;dr

  • pass auth token with all model list requests, so the gateway marks restricted models
  • UI updates to show restricted models

testing

free tier - model picker locked pr3488-1-locked-picker.png
click a locked model pr3488-1b-locked-click-modal.png

full vibe-description:

Problem

Model pickers can't see the gateway's free-tier gate (PostHog/posthog#70951): every /v1/models fetch is anonymous, so free-tier users see all models as available, and new sessions default to claude-opus-4-8 — which 403s a free-tier org's very first message.

Changes

Third slice of the cutover stack (stacked on #3487):

  • Authed models fetches wherever a token exists — Claude adapter, workspace-server preview/models routes (new gatewayAuthToken() on the auth adapter), cloud agent, and the pi harness — so the gateway can mark restricted models (allowed: false). The models caches key on auth presence so authed/anonymous responses never cross-serve.
  • Safe defaults: defaults are unchanged for everyone — pickAllowedModel only swaps when the gateway explicitly marked the preferred default restricted, which only happens for an unsubscribed org with the gate on (subscribed orgs, anonymous fetches, and old gateways carry no marks). That one case lands on the free model instead of a premium default that errors on message one. The pi harness (no locked rendering) drops restricted models and falls back to the first served model.
  • Locked pickers: restricted models stay listed, dimmed with a lock icon; clicking one opens the add-payment-method modal instead of selecting. Marks ride ACP option _meta (posthog.code/restrictedModel). Deliberately no codex model/list threading — codex's live options stay unmarked (the free tier has zero Codex-compatible models). A gated codex send used to die as a silent refusal — warning badge, no explanation; the adapter now rejects the prompt with the gateway's message so the feat(billing): classify gateway billing denials and rework the usage-limit modal for usage-based billing #3485 modal fires, same as Claude sends.

Why: without this, a free-tier org's first-run experience post-cutover is "default model errors immediately" — this makes the gate visible before the send instead of after it.

How did you test this?

Automated: typecheck green across agent/shared/workspace-server/harness/core/ui; pickAllowedModel unit tests (allowed/absent/restricted→newest-allowed/all-restricted/empty), gateway-models suite 28/28, codex adapter suite incl. a new billing-denial rejection test, UI billing+sessions suites 457/457, harness 651, shared 602 — all local. The agent/workspace-server integration suites can't run in this sandbox (they shell out to git commit, which is blocked here); relying on CI for those. Not covered: a visual pass over the locked picker items.

Automatic notifications

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

Created with PostHog Code

adboio commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 2351040.

@adboio adboio added the Create Release This will trigger a new release label Jul 16, 2026 — with Graphite App
@adboio adboio force-pushed the posthog-code/usage-billing-surfaces branch from d8c58e9 to 3a63683 Compare July 16, 2026 00:42
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from d17b496 to ad81fad Compare July 16, 2026 00:43
@adboio adboio force-pushed the posthog-code/usage-billing-surfaces branch from f65a67e to bc1c466 Compare July 16, 2026 01:18
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from ad81fad to 08c8cb4 Compare July 16, 2026 01:20
@adboio adboio force-pushed the posthog-code/usage-billing-surfaces branch from bc1c466 to 6e88f3e Compare July 16, 2026 01:21
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 08c8cb4 to 26e2d04 Compare July 16, 2026 01:21
@adboio adboio force-pushed the posthog-code/usage-billing-surfaces branch from 6e88f3e to 25c1733 Compare July 16, 2026 01:29
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 26e2d04 to 93db29a Compare July 16, 2026 01:30
@adboio adboio force-pushed the posthog-code/usage-billing-surfaces branch from 25c1733 to c8ff7f0 Compare July 16, 2026 01:44
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 93db29a to b791e1c Compare July 16, 2026 01:44
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch 6 times, most recently from 89b23de to 0a4bc69 Compare July 16, 2026 04:44
@adboio adboio marked this pull request as ready for review July 16, 2026 04:47
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 0a4bc69 to 2843258 Compare July 16, 2026 04:47
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Security Review

Authorization-dependent model restrictions are cached by authentication presence rather than principal identity. A process serving different organization tokens can therefore reuse one organization's entitlement response for another.

Reviews (1): Last reviewed commit: "fix(billing): carry the codex denial mes..." | Re-trigger Greptile

Comment thread packages/agent/src/gateway-models.ts Outdated
Comment thread packages/workspace-server/src/services/agent/auth-adapter.ts
@graphite-app graphite-app Bot changed the base branch from posthog-code/usage-billing-surfaces to graphite-base/3488 July 16, 2026 04:59
@adboio adboio force-pushed the graphite-base/3488 branch from 66ffc18 to 783ed00 Compare July 16, 2026 05:09
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 87f0eb3 to 43beea8 Compare July 16, 2026 05:09
@graphite-app graphite-app Bot changed the base branch from graphite-base/3488 to posthog-code/usage-billing-surfaces July 16, 2026 05:09
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch 2 times, most recently from 63d33d9 to 13f74e1 Compare July 16, 2026 05:10
Base automatically changed from posthog-code/usage-billing-surfaces to main July 16, 2026 05:15
@trunk-io

trunk-io Bot commented Jul 16, 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.

adboio added 4 commits July 16, 2026 05:15
… defaults, locked pickers

Third slice of the cutover stack:

- Gateway /v1/models fetches send the user's token wherever one exists
  (Claude adapter, workspace-server preview/models, cloud agent, pi harness)
  so the gateway can mark plan-restricted models; the models caches key on
  auth presence so authed and anonymous responses never cross-serve.
- pickAllowedModel keeps session defaults off restricted models — a
  free-tier org lands on the free model instead of a premium default that
  403s its first message. Applied on the Claude-family default paths; codex
  free tier has no allowed models, so its sends surface the PR-1 modal.
- Restricted models stay listed and render dimmed with a lock
  (ModelRadioItem); picking one opens the upgrade gate instead of selecting.
  Marks ride option _meta (posthog.code/restrictedModel); no codex
  model/list threading — its live options stay unmarked by design.
- The pi harness drops restricted models (no locked rendering there) and
  falls back to the first served model when the preferred one is gone.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
The codex adapter resolved every non-retried fatal error as a silent
refusal, so a free-tier org's gateway 403/429 died with a warning badge and
no explanation. A fatal error that classifies as a gateway billing denial
now rejects the prompt with the gateway's message, which the host already
classifies into the upgrade modal.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
A plain Error serializes to a bare -32603 "Internal error" at the ACP
boundary, so the gateway text never reached the host's classifier — the
denial read as a fatal session error and triggered a kill/respawn/resend
loop instead of the upgrade modal. Reject with
RequestError.internalError(undefined, message) like the Claude adapter,
which lands client-side as "Internal error: …needs a paid PostHog plan…"
and classifies correctly. The test now asserts the RequestError shape, not
just the in-process rejection.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
Restriction marks are identity-scoped, but the cache keyed on token
presence — an org switch in the same process served the previous org's
marks for up to the 10-minute TTL, mislocking pickers and letting
pickAllowedModel swap session defaults off stale data. Entries are now
keyed on the exact token; a rotation costs one refetch.

Generated-By: PostHog Code
Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
@adboio adboio force-pushed the posthog-code/usage-billing-model-gating branch from 13f74e1 to 2351040 Compare July 16, 2026 05:15

adboio commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

/trunk merge

@trunk-io trunk-io Bot merged commit 4f27d54 into main Jul 16, 2026
32 checks passed
@trunk-io trunk-io Bot deleted the posthog-code/usage-billing-model-gating branch July 16, 2026 05:24
@github-actions

Copy link
Copy Markdown

Could not verify team-posthog-code membership (GitHub API error). Auto-release is limited to Team PostHog Code members, so this will ship with the next scheduled release instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Create Release This will trigger a new release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants