useage limits on OpenAI requests. closes #12
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- 'Lift' auth context into server middleware to support authenticated api (rest) endpoints for alternate clients while still supporting fully typed Trpc context.
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ Discord [here](https://discord.gg/3hWPDTA4kD)
|
|||||||
- [x] Max Notes enforced
|
- [x] Max Notes enforced
|
||||||
- [x] Add, Delete notes on Dashboard
|
- [x] Add, Delete notes on Dashboard
|
||||||
- [x] AI Note generation with OpenAI
|
- [x] AI Note generation with OpenAI
|
||||||
|
- [x] Per Account, Per Month Useage Limits on AI Access
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- [x] Manual test scenario for auth and sub workflows passing
|
- [x] Manual test scenario for auth and sub workflows passing
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { ACCOUNT_ACCESS } from '~~/prisma/account-access-enum';
|
|||||||
import prisma_client from '~~/prisma/prisma.client';
|
import prisma_client from '~~/prisma/prisma.client';
|
||||||
import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types';
|
import { accountWithMembers, AccountWithMembers, membershipWithAccount, MembershipWithAccount, membershipWithUser, MembershipWithUser } from './service.types';
|
||||||
import generator from 'generate-password-ts';
|
import generator from 'generate-password-ts';
|
||||||
|
import { UtilService } from './util.service';
|
||||||
|
import { AccountLimitError } from './errors';
|
||||||
|
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
export default class AccountService {
|
export default class AccountService {
|
||||||
async getAccountById(account_id: number): Promise<AccountWithMembers> {
|
async getAccountById(account_id: number): Promise<AccountWithMembers> {
|
||||||
@@ -50,6 +54,7 @@ export default class AccountService {
|
|||||||
data: {
|
data: {
|
||||||
stripe_subscription_id,
|
stripe_subscription_id,
|
||||||
current_period_ends,
|
current_period_ends,
|
||||||
|
ai_gen_count:0,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -64,6 +69,8 @@ export default class AccountService {
|
|||||||
max_notes: paid_plan.max_notes,
|
max_notes: paid_plan.max_notes,
|
||||||
max_members: paid_plan.max_members,
|
max_members: paid_plan.max_members,
|
||||||
plan_name: paid_plan.name,
|
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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import prisma_client from '~~/prisma/prisma.client';
|
import prisma_client from '~~/prisma/prisma.client';
|
||||||
import { openai } from './openai.client';
|
import { openai } from './openai.client';
|
||||||
import { AccountLimitError } from './errors';
|
import { AccountLimitError } from './errors';
|
||||||
|
import AccountService from './account.service';
|
||||||
|
|
||||||
export default class NotesService {
|
export default class NotesService {
|
||||||
async getAllNotes() {
|
async getAllNotes() {
|
||||||
@@ -36,7 +37,10 @@ export default class NotesService {
|
|||||||
return prisma_client.note.delete({ where: { id } });
|
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 = `
|
const prompt = `
|
||||||
Write an interesting short note about ${userPrompt}.
|
Write an interesting short note about ${userPrompt}.
|
||||||
Restrict the note to a single paragraph.
|
Restrict the note to a single paragraph.
|
||||||
@@ -49,6 +53,9 @@ export default class NotesService {
|
|||||||
max_tokens: 1000,
|
max_tokens: 1000,
|
||||||
n: 1,
|
n: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await accountService.incrementAIGenCount(account);
|
||||||
|
|
||||||
return completion.data.choices[0].text;
|
return completion.data.choices[0].text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "supanuxt-saas",
|
"name": "supanuxt-saas",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "supanuxt-saas",
|
"name": "supanuxt-saas",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "supanuxt-saas",
|
"name": "supanuxt-saas",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Michael Dausmann",
|
"name": "Michael Dausmann",
|
||||||
"email": "mdausmann@gmail.com",
|
"email": "mdausmann@gmail.com",
|
||||||
|
|||||||
@@ -60,6 +60,11 @@
|
|||||||
<span>{{ activeMembership?.account.max_notes }}</span>
|
<span>{{ activeMembership?.account.max_notes }}</span>
|
||||||
</div>
|
</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">
|
<div class="flex gap-4 items-center">
|
||||||
<span class="font-bold w-32">Maximum Members:</span>
|
<span class="font-bold w-32">Maximum Members:</span>
|
||||||
<span>{{ activeMembership?.account.max_members }}</span>
|
<span>{{ activeMembership?.account.max_members }}</span>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ model Account {
|
|||||||
stripe_customer_id String?
|
stripe_customer_id String?
|
||||||
max_members Int @default(1)
|
max_members Int @default(1)
|
||||||
join_password String @unique
|
join_password String @unique
|
||||||
|
ai_gen_max_pm Int @default(7)
|
||||||
|
ai_gen_count Int @default(0)
|
||||||
|
|
||||||
@@map("account")
|
@@map("account")
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ model Plan {
|
|||||||
max_notes Int @default(100)
|
max_notes Int @default(100)
|
||||||
stripe_product_id String?
|
stripe_product_id String?
|
||||||
max_members Int @default(1)
|
max_members Int @default(1)
|
||||||
|
ai_gen_max_pm Int @default(7)
|
||||||
|
|
||||||
@@map("plan")
|
@@map("plan")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ async function main() {
|
|||||||
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES'],
|
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES'],
|
||||||
max_notes: 10,
|
max_notes: 10,
|
||||||
max_members: 1,
|
max_members: 1,
|
||||||
|
ai_gen_max_pm: 7,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const individualPlan = await prisma.plan.upsert({
|
const individualPlan = await prisma.plan.upsert({
|
||||||
@@ -19,6 +20,7 @@ async function main() {
|
|||||||
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE'],
|
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE'],
|
||||||
max_notes: 100,
|
max_notes: 100,
|
||||||
max_members: 1,
|
max_members: 1,
|
||||||
|
ai_gen_max_pm: 50,
|
||||||
stripe_product_id: 'prod_NQR7vwUulvIeqW'
|
stripe_product_id: 'prod_NQR7vwUulvIeqW'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -30,6 +32,7 @@ async function main() {
|
|||||||
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE', 'SPECIAL_TEAM_FEATURE'],
|
features: ['ADD_NOTES', 'EDIT_NOTES', 'VIEW_NOTES', 'SPECIAL_FEATURE', 'SPECIAL_TEAM_FEATURE'],
|
||||||
max_notes: 200,
|
max_notes: 200,
|
||||||
max_members: 10,
|
max_members: 10,
|
||||||
|
ai_gen_max_pm: 500,
|
||||||
stripe_product_id: 'prod_NQR8IkkdhqBwu2'
|
stripe_product_id: 'prod_NQR8IkkdhqBwu2'
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const notesRouter = router({
|
|||||||
.input(z.object({ user_prompt: z.string() }))
|
.input(z.object({ user_prompt: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const notesService = new NotesService();
|
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 {
|
return {
|
||||||
noteText
|
noteText
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user