@@ -6,7 +6,7 @@ import { ChatAnthropic } from "@langchain/anthropic";
6
6
import { ChatPromptTemplate } from "@langchain/core/prompts" ;
7
7
8
8
import * as Sentry from "@sentry/nextjs" ;
9
- import { GraderResultTestExtraData } from "@/utils/supabase/DatabaseTypes" ;
9
+ import { GraderResultTestExtraData , LLMRateLimitConfig } from "@/utils/supabase/DatabaseTypes" ;
10
10
11
11
/**
12
12
* Custom error class for errors that should be displayed to users
@@ -99,6 +99,81 @@ async function getPrompt(input: GraderResultTestExtraData["llm"]) {
99
99
return ChatPromptTemplate . fromMessages ( [ [ "human" , input . prompt ] ] ) ;
100
100
}
101
101
102
+ async function checkRateLimits (
103
+ testResult : any ,
104
+ rateLimit : LLMRateLimitConfig ,
105
+ serviceSupabase : any
106
+ ) : Promise < string | null > {
107
+ const submissionId = testResult . grader_results . submissions . id ;
108
+ const classId = testResult . class_id ;
109
+ const assignmentId = testResult . grader_results . submissions . assignment_id ;
110
+
111
+ // Check cooldown (minutes since last inference on this assignment, excluding current submission)
112
+ if ( rateLimit . cooldown ) {
113
+ const { data : lastUsage } = await serviceSupabase
114
+ . from ( "llm_inference_usage" )
115
+ . select ( `
116
+ created_at,
117
+ submissions!inner (
118
+ assignment_id
119
+ )
120
+ ` )
121
+ . eq ( "submissions.assignment_id" , assignmentId )
122
+ . neq ( "submission_id" , submissionId )
123
+ . order ( "created_at" , { ascending : false } )
124
+ . limit ( 1 )
125
+ . single ( ) ;
126
+
127
+ if ( lastUsage ) {
128
+ const minutesSinceLastUsage = Math . floor (
129
+ ( Date . now ( ) - new Date ( lastUsage . created_at ) . getTime ( ) ) / ( 1000 * 60 )
130
+ ) ;
131
+
132
+ if ( minutesSinceLastUsage < rateLimit . cooldown ) {
133
+ const remainingMinutes = rateLimit . cooldown - minutesSinceLastUsage ;
134
+ return `Rate limit: Please wait ${ remainingMinutes } more minute(s) before requesting Feedbot feedback for this assignment.` ;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Check assignment total limit
140
+ if ( rateLimit . assignment_total ) {
141
+ // First get all submissions for this assignment
142
+ const { data : assignmentSubmissions } = await serviceSupabase
143
+ . from ( "submissions" )
144
+ . select ( "id" )
145
+ . eq ( "assignment_id" , assignmentId ) ;
146
+
147
+ if ( assignmentSubmissions && assignmentSubmissions . length > 0 ) {
148
+ const submissionIds = assignmentSubmissions . map ( ( s : any ) => s . id ) ;
149
+
150
+ // Count usage across all submissions for this assignment
151
+ const { count : assignmentUsageCount } = await serviceSupabase
152
+ . from ( "llm_inference_usage" )
153
+ . select ( "*" , { count : "exact" , head : true } )
154
+ . in ( "submission_id" , submissionIds ) ;
155
+
156
+ if ( assignmentUsageCount && assignmentUsageCount >= rateLimit . assignment_total ) {
157
+ return `Rate limit: Maximum number of Feedbot responses (${ rateLimit . assignment_total } ) for this assignment has been reached.` ;
158
+ }
159
+ }
160
+ }
161
+
162
+ // Check class total limit
163
+ if ( rateLimit . class_total ) {
164
+ const { count : classUsageCount } = await serviceSupabase
165
+ . from ( "llm_inference_usage" )
166
+ . select ( "*" , { count : "exact" , head : true } )
167
+ . eq ( "class_id" , classId ) ;
168
+
169
+ if ( classUsageCount && classUsageCount >= rateLimit . class_total ) {
170
+ return `Rate limit: Maximum number of Feedbot responses (${ rateLimit . class_total } ) for this class has been reached.` ;
171
+ }
172
+ }
173
+
174
+ return null ; // No rate limiting issues
175
+ }
176
+
102
177
export async function POST ( request : NextRequest ) {
103
178
try {
104
179
const { testId } = await request . json ( ) ;
@@ -134,7 +209,8 @@ export async function POST(request: NextRequest) {
134
209
grader_results!inner (
135
210
submissions!inner (
136
211
id,
137
- class_id
212
+ class_id,
213
+ assignment_id
138
214
)
139
215
)
140
216
`
@@ -166,8 +242,24 @@ export async function POST(request: NextRequest) {
166
242
} ) ;
167
243
}
168
244
245
+ // Use service role client for the update since users might not have update permissions
246
+ const serviceSupabase = createServiceClient ( process . env . SUPABASE_URL ! , process . env . SUPABASE_SERVICE_ROLE_KEY ! ) ;
247
+
248
+ // Check rate limiting if configured
249
+ if ( extraData . llm . rate_limit ) {
250
+
251
+ const rateLimitError = await checkRateLimits (
252
+ testResult ,
253
+ extraData . llm . rate_limit ,
254
+ serviceSupabase
255
+ ) ;
256
+ if ( rateLimitError ) {
257
+ throw new UserVisibleError ( rateLimitError , 429 ) ;
258
+ }
259
+ }
260
+
169
261
const modelName = extraData . llm . model || process . env . OPENAI_MODEL || "gpt-4o-mini" ;
170
- const providerName = extraData . llm . provider || "anthropic " ;
262
+ const providerName = extraData . llm . provider || "openai " ;
171
263
const accountName = extraData . llm . account ;
172
264
173
265
const chatModel = await getChatModel ( {
@@ -200,9 +292,7 @@ export async function POST(request: NextRequest) {
200
292
}
201
293
} ;
202
294
203
- // Use service role client for the update since users might not have update permissions
204
- const serviceSupabase = createServiceClient ( process . env . SUPABASE_URL ! , process . env . SUPABASE_SERVICE_ROLE_KEY ! ) ;
205
-
295
+
206
296
const { error : updateError } = await serviceSupabase
207
297
. from ( "grader_result_tests" )
208
298
. update ( { extra_data : updatedExtraData } )
0 commit comments