feat(billing): honor the gateway's free-tier model marks — authed fetches, safe defaults, locked pickers#3488
Conversation
be7d563 to
d17b496
Compare
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
d8c58e9 to
3a63683
Compare
d17b496 to
ad81fad
Compare
f65a67e to
bc1c466
Compare
ad81fad to
08c8cb4
Compare
bc1c466 to
6e88f3e
Compare
08c8cb4 to
26e2d04
Compare
6e88f3e to
25c1733
Compare
26e2d04 to
93db29a
Compare
25c1733 to
c8ff7f0
Compare
93db29a to
b791e1c
Compare
89b23de to
0a4bc69
Compare
0a4bc69 to
2843258
Compare
|
66ffc18 to
783ed00
Compare
87f0eb3 to
43beea8
Compare
63d33d9 to
13f74e1
Compare
|
😎 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. |
… 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
13f74e1 to
2351040
Compare
|
/trunk merge |
|
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. |

tl;dr
testing
full vibe-description:
Problem
Model pickers can't see the gateway's free-tier gate (PostHog/posthog#70951): every
/v1/modelsfetch is anonymous, so free-tier users see all models as available, and new sessions default toclaude-opus-4-8— which 403s a free-tier org's very first message.Changes
Third slice of the cutover stack (stacked on #3487):
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.pickAllowedModelonly 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._meta(posthog.code/restrictedModel). Deliberately no codexmodel/listthreading — 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;
pickAllowedModelunit 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 togit commit, which is blocked here); relying on CI for those. Not covered: a visual pass over the locked picker items.Automatic notifications
Created with PostHog Code