-
Notifications
You must be signed in to change notification settings - Fork 0
Jorge/usage aggregate #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
create new table schemas new procedures
- update usage-event schema and add total cost
packages/api/src/api-router/usage.ts
Outdated
| const activeSubscription = await getActiveSubscription({ | ||
| ctx, | ||
| userId, | ||
| }); | ||
|
|
||
| if (activeSubscription) { | ||
| // Validate subscription has required fields | ||
| if ( | ||
| !activeSubscription.periodStart || | ||
| !activeSubscription.periodEnd || | ||
| !activeSubscription.plan | ||
| ) { | ||
| throw new TRPCError({ | ||
| code: "INTERNAL_SERVER_ERROR", | ||
| message: "Invalid subscription data", | ||
| }); | ||
| } | ||
|
|
||
| // Pro user: try to find existing usage period for current subscription period | ||
| let usagePeriod = await tx.query.UsagePeriod.findFirst({ | ||
| where: and( | ||
| eq(UsagePeriod.user_id, userId), | ||
| eq(UsagePeriod.subscription_id, activeSubscription.id), | ||
| eq(UsagePeriod.period_start, activeSubscription.periodStart), | ||
| eq(UsagePeriod.period_end, activeSubscription.periodEnd), | ||
| ), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should get both the activeSubscription and the usagePeriod in 1 query; otherwise, we are doing two round-trips to the DB, which is a lot of lag. We'll notice things are slower if we use this everywhere. I think we can get both at the same time with Drizzle relationships - worst case scenario we can write a SQL query lol.
packages/api/src/api-router/usage.ts
Outdated
| // Try to find existing usage period for current month | ||
| let usagePeriod = await tx.query.UsagePeriod.findFirst({ | ||
| where: and( | ||
| eq(UsagePeriod.user_id, userId), | ||
| eq(UsagePeriod.period_start, monthStart), | ||
| eq(UsagePeriod.period_end, monthEnd), | ||
| ), | ||
| }); | ||
|
|
||
| if (!usagePeriod) { | ||
| // Get the free plan for free users | ||
| const freePlan = await getFreePlan(ctx); | ||
|
|
||
| // Create usage period for free user | ||
| const [newUsagePeriod] = await tx | ||
| .insert(UsagePeriod) | ||
| .values({ | ||
| period_end: monthEnd, | ||
| period_start: monthStart, | ||
| plan_id: freePlan?.id, | ||
| subscription_id: null, // Free users don't have a subscription_id | ||
| user_id: userId, | ||
| }) | ||
| .returning(); | ||
|
|
||
| usagePeriod = newUsagePeriod; | ||
| } | ||
|
|
||
| if (!usagePeriod) { | ||
| throw new TRPCError({ | ||
| code: "INTERNAL_SERVER_ERROR", | ||
| message: "Failed to create or retrieve usage period for free user", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like doing a write here since this is supposed to be a query; if we combine reads with writes here, it will be very hard to optimize later. This is meant to be a middleware being used everywhere 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
- When users create accounts, create a usage period too
- When users subscribe, update the period?
- When user subscriptions end, create a new period?
- Then we could reset usage in a cron job for free users 🤔
This way we separate writes/reads.
| recurring: insertData.recurring, | ||
| type: insertData.type, | ||
| unitAmount: insertData.unitAmount, | ||
| updatedAt: new Date(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
| created_at: timestamp().defaultNow(), | ||
| updated_at: timestamp() | ||
| .defaultNow() | ||
| .$onUpdateFn(() => new Date()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should look like this:
| created_at: timestamp().defaultNow(), | |
| updated_at: timestamp() | |
| .defaultNow() | |
| .$onUpdateFn(() => new Date()), | |
| created_at: t | |
| .timestamp({ mode: "string", withTimezone: true }) | |
| .defaultNow() | |
| .notNull(), | |
| updated_at: t | |
| .timestamp({ mode: "string", withTimezone: true }) | |
| .defaultNow() | |
| .notNull() | |
| .$onUpdateFn(() => sql`now()`), |
| created_at: timestamp().defaultNow(), | ||
| updated_at: timestamp() | ||
| .defaultNow() | ||
| .$onUpdateFn(() => new Date()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same date-related changes here.
| usage_period_id: t | ||
| .uuid() | ||
| .notNull() | ||
| .references(() => UsagePeriod.id, { onDelete: "cascade" }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should ever delete usage when periods are deleted for tracking purposes 🤔
We should probably never delete periods either.
| created_at: timestamp().defaultNow(), | ||
| updated_at: timestamp() | ||
| .defaultNow() | ||
| .$onUpdateFn(() => new Date()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same date related changes could be done here.
- free user: created on create account, new periods created via cronjob - paid user: created once user subscribes
Add Usage Tracking & Aggregation System
Overview
Implements comprehensive usage tracking for AI model consumption with cost calculation, quota management, and webhook-based processing.
Changes
Database Schema
usage_event,usage_period,usage_aggregate,model_pricingAPI Endpoints
api.usage.checkUsage- Check user quota against current usageapi.usage.trackModelUsage- Record AI model usage eventsapi.usage.processUsageEvent- Process events and calculate costsapi.modelPricing.*- Manage model pricing dataAI Integration
Key Features
Infrastructure
/api/supabase/usageNext Steps
checkUsagein the correct places to prevent/allow usage if needed