@@ -12,14 +12,14 @@ import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
1212import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
1313import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
1414import { logger } from "./logger"
15- import { AuthError , CreditsError , MonthlyLimitError , UserLimitError , ModelError } from "./error"
15+ import { AuthError , CreditsError , MonthlyLimitError , UserLimitError , ModelError , RateLimitError } from "./error"
1616import { createBodyConverter , createStreamPartConverter , createResponseConverter } from "./provider/provider"
1717import { anthropicHelper } from "./provider/anthropic"
1818import { openaiHelper } from "./provider/openai"
1919import { oaCompatHelper } from "./provider/openai-compatible"
20+ import { createRateLimiter } from "./rateLimiter"
2021
2122type ZenData = Awaited < ReturnType < typeof ZenData . list > >
22- type Model = ZenData [ "models" ] [ string ]
2323
2424export async function handler (
2525 input : APIEvent ,
@@ -28,23 +28,30 @@ export async function handler(
2828 parseApiKey : ( headers : Headers ) => string | undefined
2929 } ,
3030) {
31+ type AuthInfo = Awaited < ReturnType < typeof authenticate > >
32+ type ModelInfo = Awaited < ReturnType < typeof validateModel > >
33+ type ProviderInfo = Awaited < ReturnType < typeof selectProvider > >
34+
3135 const FREE_WORKSPACES = [
3236 "wrk_01K46JDFR0E75SG2Q8K172KF3Y" , // frank
3337 "wrk_01K6W1A3VE0KMNVSCQT43BG2SX" , // opencode bench
3438 ]
3539
3640 try {
3741 const body = await input . request . json ( )
42+ const ip = input . request . headers . get ( "x-real-ip" ) ?? ""
3843 logger . metric ( {
3944 is_tream : ! ! body . stream ,
4045 session : input . request . headers . get ( "x-opencode-session" ) ,
4146 request : input . request . headers . get ( "x-opencode-request" ) ,
4247 } )
4348 const zenData = ZenData . list ( )
4449 const modelInfo = validateModel ( zenData , body . model )
45- const providerInfo = selectProvider ( zenData , modelInfo , input . request . headers . get ( "x-real-ip" ) ?? "" )
50+ const providerInfo = selectProvider ( zenData , modelInfo , ip )
4651 const authInfo = await authenticate ( modelInfo , providerInfo )
47- validateBilling ( modelInfo , authInfo )
52+ const rateLimiter = createRateLimiter ( modelInfo . id , modelInfo . rateLimit , ip )
53+ await rateLimiter ?. check ( )
54+ validateBilling ( authInfo , modelInfo )
4855 validateModelSettings ( authInfo )
4956 updateProviderKey ( authInfo , providerInfo )
5057 logger . metric ( { provider : providerInfo . id } )
@@ -59,7 +66,7 @@ export async function handler(
5966 } ) ,
6067 )
6168 logger . debug ( "REQUEST URL: " + reqUrl )
62- logger . debug ( "REQUEST: " + reqBody )
69+ logger . debug ( "REQUEST: " + reqBody . substring ( 0 , 300 ) + "..." )
6370 const res = await fetch ( reqUrl , {
6471 method : "POST" ,
6572 headers : ( ( ) => {
@@ -84,9 +91,6 @@ export async function handler(
8491 }
8592 }
8693 logger . debug ( "STATUS: " + res . status + " " + res . statusText )
87- if ( res . status === 400 || res . status === 503 ) {
88- logger . debug ( "RESPONSE: " + ( await res . text ( ) ) )
89- }
9094
9195 // Handle non-streaming response
9296 if ( ! body . stream ) {
@@ -95,6 +99,7 @@ export async function handler(
9599 const body = JSON . stringify ( responseConverter ( json ) )
96100 logger . metric ( { response_length : body . length } )
97101 logger . debug ( "RESPONSE: " + body )
102+ await rateLimiter ?. track ( )
98103 await trackUsage ( authInfo , modelInfo , providerInfo , json . usage )
99104 await reload ( authInfo )
100105 return new Response ( body , {
@@ -123,6 +128,7 @@ export async function handler(
123128 response_length : responseLength ,
124129 "timestamp.last_byte" : Date . now ( ) ,
125130 } )
131+ await rateLimiter ?. track ( )
126132 const usage = usageParser . retrieve ( )
127133 if ( usage ) {
128134 await trackUsage ( authInfo , modelInfo , providerInfo , usage )
@@ -197,6 +203,15 @@ export async function handler(
197203 { status : 401 } ,
198204 )
199205
206+ if ( error instanceof RateLimitError )
207+ return new Response (
208+ JSON . stringify ( {
209+ type : "error" ,
210+ error : { type : error . constructor . name , message : error . message } ,
211+ } ) ,
212+ { status : 429 } ,
213+ )
214+
200215 return new Response (
201216 JSON . stringify ( {
202217 type : "error" ,
@@ -221,8 +236,8 @@ export async function handler(
221236 return { id : modelId , ...modelData }
222237 }
223238
224- function selectProvider ( zenData : ZenData , model : Awaited < ReturnType < typeof validateModel > > , ip : string ) {
225- const providers = model . providers
239+ function selectProvider ( zenData : ZenData , modelInfo : ModelInfo , ip : string ) {
240+ const providers = modelInfo . providers
226241 . filter ( ( provider ) => ! provider . disabled )
227242 . flatMap ( ( provider ) => Array < typeof provider > ( provider . weight ?? 1 ) . fill ( provider ) )
228243
@@ -235,22 +250,22 @@ export async function handler(
235250 throw new ModelError ( `Provider ${ provider . id } not supported` )
236251 }
237252
238- const format = zenData . providers [ provider . id ] . format
239-
240253 return {
241254 ...provider ,
242255 ...zenData . providers [ provider . id ] ,
243- ...( format === "anthropic" ? anthropicHelper : format === "openai" ? openaiHelper : oaCompatHelper ) ,
256+ ...( ( ) => {
257+ const format = zenData . providers [ provider . id ] . format
258+ if ( format === "anthropic" ) return anthropicHelper
259+ if ( format === "openai" ) return openaiHelper
260+ return oaCompatHelper
261+ } ) ( ) ,
244262 }
245263 }
246264
247- async function authenticate (
248- model : Awaited < ReturnType < typeof validateModel > > ,
249- providerInfo : Awaited < ReturnType < typeof selectProvider > > ,
250- ) {
265+ async function authenticate ( modelInfo : ModelInfo , providerInfo : ProviderInfo ) {
251266 const apiKey = opts . parseApiKey ( input . request . headers )
252267 if ( ! apiKey ) {
253- if ( model . allowAnonymous ) return
268+ if ( modelInfo . allowAnonymous ) return
254269 throw new AuthError ( "Missing API key." )
255270 }
256271
@@ -282,7 +297,7 @@ export async function handler(
282297 . innerJoin ( WorkspaceTable , eq ( WorkspaceTable . id , KeyTable . workspaceID ) )
283298 . innerJoin ( BillingTable , eq ( BillingTable . workspaceID , KeyTable . workspaceID ) )
284299 . innerJoin ( UserTable , and ( eq ( UserTable . workspaceID , KeyTable . workspaceID ) , eq ( UserTable . id , KeyTable . userID ) ) )
285- . leftJoin ( ModelTable , and ( eq ( ModelTable . workspaceID , KeyTable . workspaceID ) , eq ( ModelTable . model , model . id ) ) )
300+ . leftJoin ( ModelTable , and ( eq ( ModelTable . workspaceID , KeyTable . workspaceID ) , eq ( ModelTable . model , modelInfo . id ) ) )
286301 . leftJoin (
287302 ProviderTable ,
288303 and ( eq ( ProviderTable . workspaceID , KeyTable . workspaceID ) , eq ( ProviderTable . provider , providerInfo . id ) ) ,
@@ -308,11 +323,11 @@ export async function handler(
308323 }
309324 }
310325
311- function validateBilling ( model : Model , authInfo : Awaited < ReturnType < typeof authenticate > > ) {
326+ function validateBilling ( authInfo : AuthInfo , modelInfo : ModelInfo ) {
312327 if ( ! authInfo ) return
313328 if ( authInfo . provider ?. credentials ) return
314329 if ( authInfo . isFree ) return
315- if ( model . allowAnonymous ) return
330+ if ( modelInfo . allowAnonymous ) return
316331
317332 const billing = authInfo . billing
318333 if ( ! billing . paymentMethodID )
@@ -356,26 +371,18 @@ export async function handler(
356371 }
357372 }
358373
359- function validateModelSettings ( authInfo : Awaited < ReturnType < typeof authenticate > > ) {
374+ function validateModelSettings ( authInfo : AuthInfo ) {
360375 if ( ! authInfo ) return
361376 if ( authInfo . isDisabled ) throw new ModelError ( "Model is disabled" )
362377 }
363378
364- function updateProviderKey (
365- authInfo : Awaited < ReturnType < typeof authenticate > > ,
366- providerInfo : Awaited < ReturnType < typeof selectProvider > > ,
367- ) {
379+ function updateProviderKey ( authInfo : AuthInfo , providerInfo : ProviderInfo ) {
368380 if ( ! authInfo ) return
369381 if ( ! authInfo . provider ?. credentials ) return
370382 providerInfo . apiKey = authInfo . provider . credentials
371383 }
372384
373- async function trackUsage (
374- authInfo : Awaited < ReturnType < typeof authenticate > > ,
375- modelInfo : ReturnType < typeof validateModel > ,
376- providerInfo : Awaited < ReturnType < typeof selectProvider > > ,
377- usage : any ,
378- ) {
385+ async function trackUsage ( authInfo : AuthInfo , modelInfo : ModelInfo , providerInfo : ProviderInfo , usage : any ) {
379386 const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
380387 providerInfo . normalizeUsage ( usage )
381388
@@ -483,7 +490,7 @@ export async function handler(
483490 )
484491 }
485492
486- async function reload ( authInfo : Awaited < ReturnType < typeof authenticate > > ) {
493+ async function reload ( authInfo : AuthInfo ) {
487494 if ( ! authInfo ) return
488495 if ( authInfo . isFree ) return
489496 if ( authInfo . provider ?. credentials ) return
0 commit comments