From a7fc39fdf7952ceef49e4bc6ee78606c83a10e16 Mon Sep 17 00:00:00 2001
From: urjitc <135136842+urjitc@users.noreply.github.com>
Date: Wed, 15 Apr 2026 23:53:34 +0000
Subject: [PATCH 1/4] Enforce Autumn credits on premium chat models
---
src/app/api/chat/route.ts | 61 +++++++++++++++++--
src/components/assistant-ui/ModelPicker.tsx | 35 ++---------
.../assistant-ui/WorkspaceRuntimeProvider.tsx | 24 +++++---
src/components/billing/UpgradeDialog.tsx | 44 +++++++++++++
src/lib/ai/models.ts | 12 ++++
src/lib/stores/ui-store.ts | 5 ++
6 files changed, 140 insertions(+), 41 deletions(-)
create mode 100644 src/components/billing/UpgradeDialog.tsx
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts
index 53e7dc4f..aaa98d7f 100644
--- a/src/app/api/chat/route.ts
+++ b/src/app/api/chat/route.ts
@@ -21,7 +21,11 @@ import {
import { withServerObservability } from "@/lib/with-server-observability";
import { normalizeLegacyToolMessages } from "@/lib/ai/legacy-tool-message-compat";
import type { ReplySelection } from "@/lib/stores/ui-store";
-import { getDefaultChatModelId, resolveGatewayModelId } from "@/lib/ai/models";
+import {
+ getDefaultChatModelId,
+ isPremiumChatModel,
+ resolveGatewayModelId,
+} from "@/lib/ai/models";
import {
buildGatewayProviderOptions,
createGatewayLanguageModel,
@@ -55,6 +59,17 @@ function getSelectedCardsContext(body: any): string {
return body.selectedCardsContext || "";
}
+type AutumnBillingAPI = {
+ check: (args: {
+ headers: Awaited>;
+ body: { featureId: string };
+ }) => Promise<{ allowed: boolean }>;
+ track: (args: {
+ headers: Awaited>;
+ body: { featureId: string; value: number };
+ }) => Promise;
+};
+
/**
* Inject user-selected context (selected cards + reply quotes / workspace passages) into the last user message.
* `custom` is body.metadata.custom from the composer's runConfig (replySelections only).
@@ -123,9 +138,11 @@ async function handlePOST(req: Request) {
try {
// FIX: Parallelize headers() and req.json() to eliminate waterfall
const [headersObj, body] = await Promise.all([headers(), req.json()]);
+ const autumnBilling = auth.api as typeof auth.api & AutumnBillingAPI;
// Get authenticated user ID
const session = await auth.api.getSession({ headers: headersObj });
+ const isAnonymousUser = Boolean(session?.user?.isAnonymous);
userId = session?.user?.id || null;
const { messages = [] }: { messages?: UIMessage[] } = body;
@@ -188,9 +205,8 @@ async function handlePOST(req: Request) {
// Get pre-formatted selected cards context from client (no DB fetch needed)
const selectedCardsContext = getSelectedCardsContext(body);
- const modelId = resolveGatewayModelId(
- body.modelId || getDefaultChatModelId(),
- );
+ const rawModelId = body.modelId || getDefaultChatModelId();
+ const modelId = resolveGatewayModelId(rawModelId);
// Inject selected cards + reply selections into the last user message
injectSelectionContext(
@@ -199,6 +215,32 @@ async function handlePOST(req: Request) {
selectedCardsContext,
);
+ if (userId && !isAnonymousUser && isPremiumChatModel(rawModelId)) {
+ try {
+ const checkResult = await autumnBilling.check({
+ headers: headersObj,
+ body: { featureId: "premium_message" },
+ });
+
+ if (!checkResult.allowed) {
+ return new Response(
+ JSON.stringify({
+ error: "credits_exhausted",
+ message:
+ "You've used all your premium AI messages for this month. Upgrade to Pro for unlimited access, or switch to another model.",
+ code: "CREDITS_EXHAUSTED",
+ }),
+ {
+ status: 403,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+ } catch (err) {
+ console.error("[billing] Autumn check failed, allowing request:", err);
+ }
+ }
+
const posthogClient = getPostHogServerClient();
const baseGatewayModel = createGatewayLanguageModel(modelId);
const tracedModel = posthogClient
@@ -257,6 +299,17 @@ async function handlePOST(req: Request) {
};
logger.info("📊 [CHAT-API] Final Token Usage:", usageInfo);
+
+ if (userId && !isAnonymousUser && isPremiumChatModel(rawModelId)) {
+ autumnBilling
+ .track({
+ headers: headersObj,
+ body: { featureId: "premium_message", value: 1 },
+ })
+ .catch((err: unknown) => {
+ console.error("[billing] Autumn track failed:", err);
+ });
+ }
},
onStepFinish: (result) => {
// stepType exists in runtime but may not be in type definitions
diff --git a/src/components/assistant-ui/ModelPicker.tsx b/src/components/assistant-ui/ModelPicker.tsx
index c63be640..bb0e2517 100644
--- a/src/components/assistant-ui/ModelPicker.tsx
+++ b/src/components/assistant-ui/ModelPicker.tsx
@@ -125,37 +125,12 @@ function ModelPickerRow({
{model.ui.description}
-
-
-
- Speed
-
-
- {model.ui.speed}
-
+
+
+ Speed
-
-
- Cost
-
-
- {Array.from({ length: 3 }, (_, index) => {
- const isActive = index < model.ui.costLevel;
- return (
-
- $
-
- );
- })}
-
+
+ {model.ui.speed}
diff --git a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx
index 8b76b7a8..ab57c1a4 100644
--- a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx
+++ b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx
@@ -13,6 +13,7 @@ import {
import type { UIMessage } from "ai";
import { useMemo, useCallback, useRef } from "react";
import { AssistantAvailableProvider } from "@/contexts/AssistantAvailabilityContext";
+import { UpgradeDialog } from "@/components/billing/UpgradeDialog";
import { useUIStore } from "@/lib/stores/ui-store";
import { toast } from "sonner";
import { useWorkspaceState } from "@/hooks/workspace/use-workspace-state";
@@ -27,6 +28,12 @@ interface WorkspaceRuntimeProviderProps {
children: React.ReactNode;
}
+function UpgradeDialogConnected() {
+ const open = useUIStore((state) => state.showUpgradeDialog);
+ const setOpen = useUIStore((state) => state.setShowUpgradeDialog);
+ return
;
+}
+
function createWorkspaceChatRuntimeHook(
transport: AssistantChatTransport
,
onError: (error: Error) => void,
@@ -79,12 +86,7 @@ export function WorkspaceRuntimeProvider({
activePdfPageByItemId,
viewingItemIds,
);
- }, [
- workspaceState,
- contextCardIds,
- activePdfPageByItemId,
- viewingItemIds,
- ]);
+ }, [workspaceState, contextCardIds, activePdfPageByItemId, viewingItemIds]);
// Per AI SDK, transport `body` is `Resolvable