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. |
The usage-based billing cutover shipped (#3485, #3487, #3488) and the seat model is dead code: nothing rendered useSeat, the seat store was only fed by auth/onboarding sync calls, and the /api/seats/* client methods have no remaining product surface. Removed: shared seat types/plan keys, core SeatService/seatView/ seatErrors + tokens, the UI seat store/client/hook, api-client seat methods and seat error classes, the SUBSCRIPTION_STARTED/CANCELLED analytics events, the orphaned "upgrade_dialog" surface literal, and the gateway invalidate-plan-cache chain (router procedure, service method, endpoint, URL helper) whose only caller was the seat upgrade flow. Auth identity sync keeps the usage-snapshot cache clear (still load-bearing for usage billing); the llmGateway query invalidation it carried was a no-op (no queries live under that key) and is dropped. Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec
First pass of copy rewrites over the usage-based billing surfaces added in the cutover stack (#3485, #3487, #3488). - Announcement modal: shorter description; reframe the models bullet around open-source vs frontier; "$X/mo" and split "Adjust it any time…" sentences. - Usage-limit / model-gate modal: rewrite the model-gate body (now two paragraphs) and trim the free-tier org-limit body. - Plan & usage page: tighten the free and subscribed plan-card blurbs. Renders the model-gate body's paragraph break via whitespace-pre-line. Generated-By: PostHog Code Task-Id: 8dff0a32-c536-4b09-b5a1-087e800b4600
First pass of copy rewrites over the usage-based billing surfaces added in the cutover stack (#3485, #3487, #3488). - Announcement modal: shorter description; reframe the models bullet around open-source vs frontier; "$X/mo" and split "Adjust it any time…" sentences. - Usage-limit / model-gate modal: rewrite the model-gate body (now two paragraphs) and trim the free-tier org-limit body. - Plan & usage page: tighten the free and subscribed plan-card blurbs. Renders the model-gate body's paragraph break via whitespace-pre-line. Generated-By: PostHog Code Task-Id: 8dff0a32-c536-4b09-b5a1-087e800b4600
The usage-based billing cutover shipped (#3485, #3487, #3488) and the seat model is dead code: nothing rendered useSeat, the seat store was only fed by auth/onboarding sync calls, and the /api/seats/* client methods have no remaining product surface. Removed: shared seat types/plan keys, core SeatService/seatView/ seatErrors + tokens, the UI seat store/client/hook, api-client seat methods and seat error classes, the SUBSCRIPTION_STARTED/CANCELLED analytics events, the orphaned "upgrade_dialog" surface literal, and the gateway invalidate-plan-cache chain (router procedure, service method, endpoint, URL helper) whose only caller was the seat upgrade flow. Auth identity sync keeps the usage-snapshot cache clear (still load-bearing for usage billing); the llmGateway query invalidation it carried was a no-op (no queries live under that key) and is dropped. Generated-By: PostHog Code Task-Id: 1039ea23-9930-44e2-9888-b05cf8b129ec

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