useage limits on OpenAI requests. closes #12

This commit is contained in:
Michael Dausmann
2023-09-10 22:46:32 +10:00
parent caf65a48c1
commit d65705732e
10 changed files with 95 additions and 5 deletions

View File

@@ -1,5 +1,10 @@
# Changelog
## Version 1.3.0
- Add an example of usage limits (Notes AI Gen).
- Includes non-destructive schema changes
```npx prisma db push```
## Version 1.2.0
- 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context.

View File

@@ -85,6 +85,7 @@ Discord [here](https://discord.gg/3hWPDTA4kD)
- [x] Max Notes enforced
- [x] Add, Delete notes on Dashboard
- [x] AI Note generation with OpenAI
- [x] Per Account, Per Month Useage Limits on AI Access
### Testing
- [x] Manual test scenario for auth and sub workflows passing

View File

@@ -2,6 +2,10 @@ import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
import prisma_client from '~~/prisma/prisma.client';
import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types';
import generator from 'generate-password-ts';
import { UtilService } from './util.service';
import { AccountLimitError } from './errors';
const config = useRuntimeConfig();
export default class AccountService {
async getAccountById(account_id: number): Promise<AccountWithMembers> {
@@ -50,6 +54,7 @@ export default class AccountService {
data: {
stripe_subscription_id,
current_period_ends,
ai_gen_count:0,
}
});
} else {
@@ -64,6 +69,8 @@ export default class AccountService {
max_notes: paid_plan.max_notes,
max_members: paid_plan.max_members,
plan_name: paid_plan.name,
ai_gen_max_pm: paid_plan.ai_gen_max_pm,
ai_gen_count:0, // I did vacillate on this point ultimately easier to just reset, discussion here https://www.reddit.com/r/SaaS/comments/16e9bew/should_i_reset_usage_counts_on_plan_upgrade/
}
});
}
@@ -272,4 +279,63 @@ export default class AccountService {
}
});
}
/*
**** Usage Limit Checking *****
This is trickier than you might think at first. Free plan users don't get a webhook from Stripe
that we can use to tick over their period end date and associated usage counts. I also didn't
want to require an additional background thread to do the rollover processing.
getAccountWithPeriodRollover: retrieves an account record and does the rollover checking returning up to date account info
checkAIGenCount: retrieves the account using getAccountWithPeriodRollover, checks the count and returns the account
incrementAIGenCount: increments the counter using the account. Note that passing in the account avoids another db fetch for the account.
Note.. for each usage limit, you will need another pair of check/increment methods and of course the count and max limit in the account schema
How to use in a service method....
async someServiceMethod(account_id: number, .....etc) {
const accountService = new AccountService();
const account = await accountService.checkAIGenCount(account_id);
... User is under the limit so do work
await accountService.incrementAIGenCount(account);
}
*/
async getAccountWithPeriodRollover (account_id: number){
const account = await prisma_client.account.findFirstOrThrow({
where: { id: account_id }
});
if(account.plan_name === config.initialPlanName && account.current_period_ends < new Date()){
return await prisma_client.account.update({
where: { id: account.id },
data: {
current_period_ends: UtilService.addMonths(account.current_period_ends,1),
// reset anything that is affected by the rollover
ai_gen_count: 0,
},
});
};
return account;
}
async checkAIGenCount(account_id: number){
const account = await this.getAccountWithPeriodRollover(account_id);
if(account.ai_gen_count >= account.ai_gen_max_pm){
throw new AccountLimitError('Monthly AI gen limit reached, no new AI Generations can be made');
}
return account;
}
async incrementAIGenCount (account: any){
return await prisma_client.account.update({
where: { id: account.id },
data: {
ai_gen_count: account.ai_gen_count + 1,
}
});
}
}

View File

@@ -1,6 +1,7 @@
import prisma_client from '~~/prisma/prisma.client';
import { openai } from './openai.client';
import { AccountLimitError } from './errors';
import AccountService from './account.service';
export default class NotesService {
async getAllNotes() {
@@ -36,7 +37,10 @@ export default class NotesService {
return prisma_client.note.delete({ where: { id } });
}
async generateAINoteFromPrompt(userPrompt: string) {
async generateAINoteFromPrompt(userPrompt: string, account_id: number) {
const accountService = new AccountService();
const account = await accountService.checkAIGenCount(account_id);
const prompt = `
Write an interesting short note about ${userPrompt}.
Restrict the note to a single paragraph.
@@ -49,6 +53,9 @@ export default class NotesService {
max_tokens: 1000,
n: 1,
});
await accountService.incrementAIGenCount(account);
return completion.data.choices[0].text;
}
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "supanuxt-saas",
"version": "1.2.0",
"version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "supanuxt-saas",
"version": "1.2.0",
"version": "1.3.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "supanuxt-saas",
"version": "1.2.0",
"version": "1.3.0",
"author": {
"name": "Michael Dausmann",
"email": "mdausmann@gmail.com",

View File

@@ -60,6 +60,11 @@
<span>{{ activeMembership?.account.max_notes }}</span>
</div>
<div class="flex gap-4 items-center">
<span class="font-bold w-32">AI Gens for this Month/Max:</span>
<span>{{ activeMembership?.account.ai_gen_count }} / {{ activeMembership?.account.ai_gen_max_pm }}</span>
</div>
<div class="flex gap-4 items-center">
<span class="font-bold w-32">Maximum Members:</span>
<span>{{ activeMembership?.account.max_members }}</span>

View File

@@ -56,6 +56,8 @@ model Account {
stripe_customer_id String?
max_members Int @default(1)
join_password String @unique
ai_gen_max_pm Int @default(7)
ai_gen_count Int @default(0)
@@map("account")
}
@@ -68,6 +70,7 @@ model Plan {
max_notes Int @default(100)
stripe_product_id String?
max_members Int @default(1)
ai_gen_max_pm Int @default(7)
@@map("plan")
}

View File

@@ -9,6 +9,7 @@ async function main() {
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES'],
max_notes: 10,
max_members: 1,
ai_gen_max_pm: 7,
},
});
const individualPlan = await prisma.plan.upsert({
@@ -19,6 +20,7 @@ async function main() {
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE'],
max_notes: 100,
max_members: 1,
ai_gen_max_pm: 50,
stripe_product_id: 'prod_NQR7vwUulvIeqW'
},
});
@@ -30,6 +32,7 @@ async function main() {
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE', 'SPECIAL_TEAM_FEATURE'],
max_notes: 200,
max_members: 10,
ai_gen_max_pm: 500,
stripe_product_id: 'prod_NQR8IkkdhqBwu2'
},
});

View File

@@ -42,7 +42,7 @@ export const notesRouter = router({
.input(z.object({ user_prompt: z.string() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();
const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt):null;
const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt, ctx.activeAccountId):null;
return {
noteText
}