Skip to content

Commit 1a5691e

Browse files
authored
refactor(billing): drop extractGatedModel; the gate copy doesn't name 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
1 parent f34f84f commit 1a5691e

9 files changed

Lines changed: 6 additions & 77 deletions

File tree

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

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,15 @@ import { describe, expect, it } from "vitest";
22
import { usageLimitContent } from "./usageLimitContent";
33

44
describe("usageLimitContent", () => {
5-
it("names the gated model and offers a payment method", () => {
5+
it("offers a payment method for the model gate", () => {
66
const content = usageLimitContent({
77
cause: "model_gate",
8-
model: "Claude Opus 4.8",
98
resetLabel: null,
109
billed: false,
1110
});
1211
expect(content.title).toBe("Unlock premium models");
13-
expect(content.description).toContain("Claude Opus 4.8 isn't");
14-
expect(content.actionLabel).toBe("Add payment method");
15-
});
16-
17-
it("falls back to generic wording when the gated model is unknown", () => {
18-
const content = usageLimitContent({
19-
cause: "model_gate",
20-
model: null,
21-
resetLabel: null,
22-
billed: undefined,
23-
});
2412
expect(content.description).toContain("This model isn't");
13+
expect(content.actionLabel).toBe("Add payment method");
2514
});
2615

2716
it.each([
@@ -36,7 +25,6 @@ describe("usageLimitContent", () => {
3625
(billed, title, actionLabel) => {
3726
const content = usageLimitContent({
3827
cause: "org_limit",
39-
model: null,
4028
resetLabel: null,
4129
billed,
4230
});
@@ -48,7 +36,6 @@ describe("usageLimitContent", () => {
4836
it("renders generic copy without a billing CTA when the cause is unknown", () => {
4937
const content = usageLimitContent({
5038
cause: null,
51-
model: null,
5239
resetLabel: "Resets in 2h",
5340
billed: true,
5441
});

packages/core/src/billing/usageLimitContent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ export interface UsageLimitContent {
1010

1111
export function usageLimitContent(args: {
1212
cause: GatewayLimitCause | null;
13-
model: string | null;
1413
resetLabel: string | null;
1514
/** usage.code_usage_billed — absent means unknown, not free. */
1615
billed: boolean | undefined;
1716
}): UsageLimitContent {
18-
const { cause, model, resetLabel, billed } = args;
17+
const { cause, resetLabel, billed } = args;
1918

2019
if (cause === "model_gate") {
2120
return {
2221
title: "Unlock premium models",
23-
description: `${model ? `${model} isn't` : "This model isn't"} included in the free tier. Add a payment method to your organization to unlock all models — you only pay for what you use. You can keep working now by switching to an included model.`,
22+
description:
23+
"This model isn't included in the free tier. Add a payment method to your organization to unlock all models — you only pay for what you use. You can keep working now by switching to an included model.",
2424
actionLabel: "Add payment method",
2525
dismissLabel: "Not now",
2626
};

packages/core/src/sessions/sessionService.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
type CloudRegion,
1717
classifyGatewayLimitError,
1818
type ExecutionMode,
19-
extractGatedModel,
2019
flattenSelectOptions,
2120
getBackoffDelay,
2221
getCloudUrlFromRegion,
@@ -2730,17 +2729,7 @@ export class SessionService {
27302729
isPromptPending: false,
27312730
promptStartedAt: null,
27322731
});
2733-
this.d.usageLimit.show(
2734-
limitCause === "model_gate"
2735-
? {
2736-
cause: limitCause,
2737-
model:
2738-
extractGatedModel(errorMessage, errorDetails) ?? undefined,
2739-
}
2740-
: limitCause
2741-
? { cause: limitCause }
2742-
: undefined,
2743-
);
2732+
this.d.usageLimit.show(limitCause ? { cause: limitCause } : undefined);
27442733
return { stopReason: "rate_limited" };
27452734
}
27462735

packages/shared/src/errors.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import {
33
classifyGatewayLimitError,
4-
extractGatedModel,
54
getErrorMessage,
65
isAuthError,
76
isFatalSessionError,
@@ -137,26 +136,6 @@ describe("classifyGatewayLimitError", () => {
137136
});
138137
});
139138

140-
describe("extractGatedModel", () => {
141-
it("pulls the model id out of the gate message", () => {
142-
expect(
143-
extractGatedModel(
144-
"Model 'claude-opus-4-8' needs a paid PostHog plan. Models available on the free tier: @cf/zai-org/glm-5.2.",
145-
),
146-
).toBe("claude-opus-4-8");
147-
});
148-
149-
it("falls back to the details and returns null when absent", () => {
150-
expect(
151-
extractGatedModel(
152-
"Internal error",
153-
"Model 'gpt-5.5' needs a paid PostHog plan.",
154-
),
155-
).toBe("gpt-5.5");
156-
expect(extractGatedModel("no model here")).toBeNull();
157-
});
158-
});
159-
160139
describe("isFatalSessionError", () => {
161140
it.each([
162141
"internal error",

packages/shared/src/errors.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -141,20 +141,6 @@ export function isRateLimitError(
141141
);
142142
}
143143

144-
const GATED_MODEL_REGEX = /Model '([^']+)' needs a paid PostHog plan/i;
145-
146-
/** The model id a free-tier model-gate 403 names, when present. */
147-
export function extractGatedModel(
148-
errorMessage: string,
149-
errorDetails?: string,
150-
): string | null {
151-
return (
152-
errorMessage.match(GATED_MODEL_REGEX)?.[1] ??
153-
errorDetails?.match(GATED_MODEL_REGEX)?.[1] ??
154-
null
155-
);
156-
}
157-
158144
export function classifyGatewayLimitError(
159145
errorMessage: string,
160146
errorDetails?: string,

packages/shared/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ export type { SignalReportPriority, Task } from "./domain-types";
7373
export * from "./enrichment";
7474
export {
7575
classifyGatewayLimitError,
76-
extractGatedModel,
7776
type GatewayLimitCause,
7877
getErrorMessage,
7978
isAuthError,

packages/ui/src/features/billing/UsageLimitModal.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export function UsageLimitModal() {
1515
const isOpen = useUsageLimitStore((s) => s.isOpen);
1616
const resetAt = useUsageLimitStore((s) => s.resetAt);
1717
const cause = useUsageLimitStore((s) => s.cause);
18-
const model = useUsageLimitStore((s) => s.model);
1918
const hide = useUsageLimitStore((s) => s.hide);
2019
const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
2120
// code_usage_billed picks the org_limit copy variant.
@@ -32,7 +31,6 @@ export function UsageLimitModal() {
3231

3332
const content = usageLimitContent({
3433
cause,
35-
model,
3634
resetLabel: resetAt ? formatResetTime(resetAt) : null,
3735
billed: usage?.code_usage_billed,
3836
});

packages/ui/src/features/billing/usageLimitStore.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ describe("usageLimitStore", () => {
77
isOpen: false,
88
resetAt: null,
99
cause: null,
10-
model: null,
1110
});
1211
});
1312

@@ -22,20 +21,17 @@ describe("usageLimitStore", () => {
2221
expect(state.isOpen).toBe(true);
2322
expect(state.resetAt).toBeNull();
2423
expect(state.cause).toBeNull();
25-
expect(state.model).toBeNull();
2624
});
2725

2826
it("show stores the denial context when provided", () => {
2927
useUsageLimitStore.getState().show({
3028
resetAt: "2026-01-02T03:04:05Z",
3129
cause: "model_gate",
32-
model: "claude-opus-4-8",
3330
});
3431
const state = useUsageLimitStore.getState();
3532
expect(state.isOpen).toBe(true);
3633
expect(state.resetAt).toBe("2026-01-02T03:04:05Z");
3734
expect(state.cause).toBe("model_gate");
38-
expect(state.model).toBe("claude-opus-4-8");
3935
});
4036

4137
it("hide closes the modal", () => {

packages/ui/src/features/billing/usageLimitStore.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@ export interface UsageLimitShowArgs {
55
resetAt?: string;
66
/** Which gateway denial tripped; unset renders the generic copy. */
77
cause?: GatewayLimitCause;
8-
/** The model the gate blocked, when known. */
9-
model?: string;
108
}
119

1210
interface UsageLimitState {
1311
isOpen: boolean;
1412
resetAt: string | null;
1513
cause: GatewayLimitCause | null;
16-
model: string | null;
1714
}
1815

1916
interface UsageLimitActions {
@@ -27,14 +24,12 @@ export const useUsageLimitStore = create<UsageLimitStore>()((set) => ({
2724
isOpen: false,
2825
resetAt: null,
2926
cause: null,
30-
model: null,
3127

3228
show: (args) =>
3329
set({
3430
isOpen: true,
3531
resetAt: args?.resetAt ?? null,
3632
cause: args?.cause ?? null,
37-
model: args?.model ?? null,
3833
}),
3934
hide: () => set({ isOpen: false }),
4035
}));

0 commit comments

Comments
 (0)