- Analytics: <Analytics /> in root layout loads umami when NEXT_PUBLIC_UMAMI_* env are set - Subscription: free/pro tiers in Postgres, daily quotas (10msg/5tool free, 1000/500 pro) - Chat route: checkAndConsume(chat_message) gate before streamText, 429 when exhausted - Tool metering: getWeather wrapped with withLimit decorator that deducts tool_call quota - Pricing page at /pricing with upgrade button - /api/billing/checkout posts to EGBE payment gateway, /api/billing/webhook verifies HMAC and upgrades user on checkout.session.completed - UsageBadge in chat header polls /api/usage every 15s - Dropped now-unused entitlements.ts
40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import type { Tool } from "ai";
|
|
import type { LimitKey } from "@/lib/subscription/config";
|
|
import {
|
|
checkAndConsume,
|
|
LimitExceededError,
|
|
} from "@/lib/subscription/service";
|
|
|
|
export function withLimit<T extends Tool>(
|
|
tool: T,
|
|
kind: LimitKey,
|
|
userId: string
|
|
): T {
|
|
const originalExecute = tool.execute;
|
|
if (!originalExecute) {
|
|
return tool;
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: wrapper has to be generic across all tool signatures
|
|
const wrappedExecute = (async (input: any, options: any) => {
|
|
try {
|
|
await checkAndConsume(userId, kind);
|
|
} catch (error) {
|
|
if (error instanceof LimitExceededError) {
|
|
return {
|
|
error: "limit_exceeded",
|
|
kind: error.kind,
|
|
tier: error.tier,
|
|
used: error.used,
|
|
limit: error.limit,
|
|
message: `You've hit your ${kind.replace("_", " ")} limit for today (${error.used}/${error.limit} on ${error.tier} tier). Upgrade to Pro for higher limits.`,
|
|
};
|
|
}
|
|
throw error;
|
|
}
|
|
return originalExecute(input, options);
|
|
// biome-ignore lint/suspicious/noExplicitAny: tool execute signature varies
|
|
}) as any;
|
|
|
|
return { ...tool, execute: wrappedExecute } as T;
|
|
}
|