Skip to content

Commit 2e09cf7

Browse files
committed
refactor(billing): drop the seat 410 stop-gap; PR 2 ships in the same 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
1 parent 1a5691e commit 2e09cf7

12 files changed

Lines changed: 0 additions & 83 deletions

File tree

packages/api-client/src/posthog-client.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,6 @@ export class SeatPaymentFailedError extends Error {
133133
}
134134
}
135135

136-
/** Seat creation/upgrades/reactivation return 410 Gone: seats are retired. */
137-
export class SeatProductRetiredError extends Error {
138-
constructor() {
139-
super("PostHog Code seats have been retired");
140-
this.name = "SeatProductRetiredError";
141-
}
142-
}
143-
144136
export class SandboxCustomImagesDisabledError extends Error {
145137
constructor(message?: string) {
146138
super(message ?? "Custom sandbox images are not enabled");
@@ -4597,9 +4589,6 @@ export class PostHogAPIClient {
45974589
const parsed = this.parseFetcherError(error);
45984590

45994591
if (parsed) {
4600-
if (parsed.status === 410) {
4601-
throw new SeatProductRetiredError();
4602-
}
46034592
if (
46044593
parsed.status === 400 &&
46054594
typeof parsed.body.redirect_url === "string"

packages/core/src/billing/seatErrors.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@ export function classifySeatError(error: unknown): ClassifiedSeatError {
88
return { error: "An unexpected error occurred", redirectUrl: null };
99
}
1010

11-
// Seats are retired (410 Gone); the seat-era UI that can hit this goes
12-
// away at cutover, so readable copy is the only handling.
13-
if (error.name === "SeatProductRetiredError") {
14-
return {
15-
error:
16-
"PostHog Code seat plans have been retired — usage is now billed to your organization.",
17-
redirectUrl: null,
18-
};
19-
}
20-
2111
if (error.name === "SeatSubscriptionRequiredError") {
2212
const redirectUrl =
2313
"redirectUrl" in error && typeof error.redirectUrl === "string"

packages/core/src/billing/seatService.test.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ class SeatPaymentFailedError extends Error {
6262
}
6363
}
6464

65-
class SeatProductRetiredError extends Error {
66-
constructor() {
67-
super("PostHog Code seats have been retired");
68-
this.name = "SeatProductRetiredError";
69-
}
70-
}
71-
7265
beforeEach(() => {
7366
vi.clearAllMocks();
7467
});
@@ -285,27 +278,3 @@ describe("error classification", () => {
285278
expect(result.error).toBe("Card declined");
286279
});
287280
});
288-
289-
// The seat API 410s creation/upgrades/reactivation once Code seats are
290-
// retired in favor of usage-based billing (reads keep working).
291-
describe("seat product retired (410 Gone)", () => {
292-
it("treats a retired auto-provision as seatless instead of erroring", async () => {
293-
const client = makeClient({
294-
createSeat: vi.fn().mockRejectedValue(new SeatProductRetiredError()),
295-
});
296-
const result = await new SeatService(client, logger).fetchSeat({
297-
autoProvision: true,
298-
});
299-
expect(result.seat).toBeNull();
300-
expect(result.error).toBeNull();
301-
});
302-
303-
it("surfaces retirement as a clear error on an explicit upgrade", async () => {
304-
const client = makeClient({
305-
createSeat: vi.fn().mockRejectedValue(new SeatProductRetiredError()),
306-
});
307-
const result = await new SeatService(client, logger).upgradeToPro();
308-
expect(result.error).toContain("retired");
309-
expect(result.redirectUrl).toBeNull();
310-
});
311-
});

packages/core/src/billing/usageLimitContent.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import type { GatewayLimitCause } from "@posthog/shared";
33
export interface UsageLimitContent {
44
title: string;
55
description: string;
6-
/** Primary action label; null renders only the dismiss button. */
76
actionLabel: string | null;
87
dismissLabel: string;
98
}
109

1110
export function usageLimitContent(args: {
1211
cause: GatewayLimitCause | null;
1312
resetLabel: string | null;
14-
/** usage.code_usage_billed — absent means unknown, not free. */
1513
billed: boolean | undefined;
1614
}): UsageLimitContent {
1715
const { cause, resetLabel, billed } = args;

packages/core/src/llm-gateway/llm-gateway.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
// the cheapest model rather than the gateway default.
2727
export const HELPER_GATEWAY_MODEL = "claude-haiku-4-5";
2828

29-
// Unbilled orgs can only use the free tier; helpers degrade to it silently.
3029
export const FREE_TIER_GATEWAY_MODEL = "@cf/zai-org/glm-5.2";
3130

3231
export class LlmGatewayError extends Error {
@@ -58,8 +57,6 @@ export class LlmGatewayService {
5857
private readonly endpoints: LlmGatewayEndpoints;
5958
private readonly log: LlmGatewayLogger;
6059

61-
// Learned from this service's own usage fetches; null until the gateway
62-
// has said either way.
6360
private lastKnownCodeUsageBilled: boolean | null = null;
6461

6562
async prompt(
@@ -80,8 +77,6 @@ export class LlmGatewayService {
8077
} = {},
8178
): Promise<PromptOutput> {
8279
const requested = options.model ?? this.endpoints.defaultModel;
83-
// Helper-only traffic (the agent session rides the SDK, not this
84-
// service): known-unbilled orgs go straight to the free-tier model.
8580
const model =
8681
this.lastKnownCodeUsageBilled === false
8782
? FREE_TIER_GATEWAY_MODEL
@@ -94,8 +89,6 @@ export class LlmGatewayService {
9489
error.statusCode === 403 &&
9590
classifyGatewayLimitError(error.message) === "model_gate";
9691
if (!isModelGate || model === FREE_TIER_GATEWAY_MODEL) throw error;
97-
// Cold start / stale bit: the gate is authoritative that the org is
98-
// unbilled — remember it and degrade this call instead of failing.
9992
this.lastKnownCodeUsageBilled = false;
10093
this.log.warn("Model gated for free tier, retrying on free-tier model", {
10194
model,

packages/core/src/llm-gateway/schemas.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export interface AnthropicErrorResponse {
5555
type: string;
5656
code?: string;
5757
};
58-
// Some gateway 403s carry a bare FastAPI `detail` instead of the envelope.
5958
detail?: unknown;
6059
}
6160

packages/core/src/sessions/sessionService.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,8 +2715,6 @@ export class SessionService {
27152715

27162716
const limitCause = classifyGatewayLimitError(errorMessage, errorDetails);
27172717

2718-
// Billing denials from the gateway leave the session healthy — surface
2719-
// the upgrade gate, not an error state.
27202718
if (
27212719
limitCause === "model_gate" ||
27222720
isRateLimitError(errorMessage, errorDetails)

packages/core/src/usage/schemas.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ export const usageOutput = z.object({
1111
user_id: z.number(),
1212
sustained: usageBucketSchema,
1313
burst: usageBucketSchema,
14-
// Org credit bucket (named ai_credits on the wire): free allocation or
15-
// billing limit exhausted.
1614
ai_credits: z.object({ exhausted: z.boolean() }).optional(),
1715
is_rate_limited: z.boolean(),
1816
is_pro: z.boolean(),
19-
// Absent on older gateways — absence means unknown, never "free".
2017
code_usage_billed: z.boolean().optional(),
2118
billing_period_end: z.string().datetime().nullable().optional(),
2219
});

packages/shared/src/analytics-events.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,6 @@ export type UpgradePromptClickedSurface =
983983
| "plan_page_card"
984984
| "upgrade_dialog";
985985

986-
/** Which gateway denial put the prompt on screen (usage-based billing). */
987986
export type UpgradePromptCause = "model_gate" | "org_limit";
988987

989988
export interface UpgradePromptShownProperties {

packages/shared/src/errors.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,12 @@ const RATE_LIMIT_PATTERNS = [
7474
"[429]",
7575
] as const;
7676

77-
/**
78-
* Billing denials from the PostHog LLM gateway, matched on the prose in
79-
* `error.message` (agent-SDK surfaces often reduce the body to that string,
80-
* so the gateway's structured `code` can't be relied on here). Kept in sync
81-
* with services/llm-gateway in posthog/posthog:
82-
* - "model_gate" (403): org isn't billed for Code usage and requested a
83-
* model outside the free tier.
84-
* - "org_limit" (429): the org's credit bucket is exhausted — free
85-
* allocation used up, or the org's billing limit reached.
86-
*/
8777
export type GatewayLimitCause = "model_gate" | "org_limit";
8878

8979
const MODEL_GATE_PATTERNS = ["needs a paid posthog plan"] as const;
9080

9181
const ORG_LIMIT_PATTERNS = [
9282
"reached its posthog code usage limit",
93-
// Gateway fallback wording for a credit bucket without a mapped message.
9483
"reached its usage limit for this billing period",
9584
] as const;
9685

@@ -168,8 +157,6 @@ export function isFatalSessionError(
168157
): boolean {
169158
if (isRateLimitError(errorMessage, errorDetails)) return false;
170159
if (isTransientUpstreamError(errorMessage, errorDetails)) return false;
171-
// A model-gate 403 arrives wrapped as "Internal error: API Error: 403 …"
172-
// but the session is healthy — it must never trigger teardown.
173160
if (classifyGatewayLimitError(errorMessage, errorDetails) === "model_gate") {
174161
return false;
175162
}

0 commit comments

Comments
 (0)