Skip to content

Commit 9047000

Browse files
committed
fix: enforce fallback_to_a_la_carte preference and move block grant after validation
1 parent 020121f commit 9047000

File tree

2 files changed

+82
-14
lines changed

2 files changed

+82
-14
lines changed

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ import type {
1818
LoggerWithContextFn,
1919
} from '@codebuff/common/types/contracts/logger'
2020

21-
import type { BlockGrantResult } from '@codebuff/billing/subscription'
21+
import type {
22+
BlockGrantResult,
23+
} from '@codebuff/billing/subscription'
24+
import {
25+
isWeeklyLimitError,
26+
isBlockExhaustedError,
27+
} from '@codebuff/billing/subscription'
28+
29+
export type GetUserPreferencesFn = (params: {
30+
userId: string
31+
logger: Logger
32+
}) => Promise<{ fallbackToALaCarte: boolean }>
2233
import type { NextRequest } from 'next/server'
2334

2435
import type { ChatCompletionRequestBody } from '@/llm-api/types'
@@ -81,6 +92,7 @@ export async function postChatCompletions(params: {
8192
fetch: typeof globalThis.fetch
8293
insertMessageBigquery: InsertMessageBigqueryFn
8394
ensureSubscriberBlockGrant?: (params: { userId: string; logger: Logger }) => Promise<BlockGrantResult | null>
95+
getUserPreferences?: GetUserPreferencesFn
8496
}) {
8597
const {
8698
req,
@@ -92,6 +104,7 @@ export async function postChatCompletions(params: {
92104
fetch,
93105
insertMessageBigquery,
94106
ensureSubscriberBlockGrant,
107+
getUserPreferences,
95108
} = params
96109
let { logger } = params
97110

@@ -186,19 +199,6 @@ export async function postChatCompletions(params: {
186199
logger,
187200
})
188201

189-
// For subscribers, ensure a block grant exists before checking balance.
190-
// This is done here because block grants should only start when the user begins working.
191-
if (ensureSubscriberBlockGrant) {
192-
try {
193-
await ensureSubscriberBlockGrant({ userId, logger })
194-
} catch (error) {
195-
logger.error(
196-
{ error: getErrorObject(error), userId },
197-
'Error ensuring subscription block grant',
198-
)
199-
}
200-
}
201-
202202
// Check user credits
203203
const {
204204
balance: { totalRemaining },
@@ -281,6 +281,59 @@ export async function postChatCompletions(params: {
281281
)
282282
}
283283

284+
// For subscribers, ensure a block grant exists before processing the request.
285+
// This is done AFTER validation so malformed requests don't start a new 5-hour block.
286+
if (ensureSubscriberBlockGrant) {
287+
try {
288+
const blockGrantResult = await ensureSubscriberBlockGrant({ userId, logger })
289+
290+
// Check if user hit subscription limit and should be rate-limited
291+
if (blockGrantResult && (isWeeklyLimitError(blockGrantResult) || isBlockExhaustedError(blockGrantResult))) {
292+
// Fetch user's preference for falling back to a-la-carte credits
293+
const preferences = getUserPreferences
294+
? await getUserPreferences({ userId, logger })
295+
: { fallbackToALaCarte: true } // Default to allowing a-la-carte if no preference function
296+
297+
if (!preferences.fallbackToALaCarte) {
298+
const resetTime = blockGrantResult.resetsAt
299+
const resetCountdown = formatQuotaResetCountdown(resetTime.toISOString())
300+
const limitType = isWeeklyLimitError(blockGrantResult) ? 'weekly' : '5-hour session'
301+
302+
trackEvent({
303+
event: AnalyticsEvent.CHAT_COMPLETIONS_INSUFFICIENT_CREDITS,
304+
userId,
305+
properties: {
306+
reason: 'subscription_limit_no_fallback',
307+
limitType,
308+
fallbackToALaCarte: false,
309+
},
310+
logger,
311+
})
312+
313+
return NextResponse.json(
314+
{
315+
error: 'rate_limit_exceeded',
316+
message: `Subscription ${limitType} limit reached. Your limit resets ${resetCountdown}. Enable "Continue with credits" in the CLI to use a-la-carte credits.`,
317+
},
318+
{ status: 429 },
319+
)
320+
}
321+
// If fallbackToALaCarte is true, continue to use a-la-carte credits
322+
logger.info(
323+
{ userId, limitType: isWeeklyLimitError(blockGrantResult) ? 'weekly' : 'session' },
324+
'Subscriber hit limit, falling back to a-la-carte credits',
325+
)
326+
}
327+
} catch (error) {
328+
logger.error(
329+
{ error: getErrorObject(error), userId },
330+
'Error ensuring subscription block grant',
331+
)
332+
// Fail open: if we can't check the subscription status, allow the request to proceed
333+
// This is intentional - we prefer to allow requests rather than block legitimate users
334+
}
335+
}
336+
284337
const openrouterApiKey = req.headers.get(BYOK_OPENROUTER_HEADER)
285338

286339
// Handle streaming vs non-streaming

web/src/app/api/v1/chat/completions/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,29 @@ import { insertMessageBigquery } from '@codebuff/bigquery'
22
import { ensureSubscriberBlockGrant } from '@codebuff/billing/subscription'
33
import { getUserUsageData } from '@codebuff/billing/usage-service'
44
import { trackEvent } from '@codebuff/common/analytics'
5+
import db from '@codebuff/internal/db'
6+
import * as schema from '@codebuff/internal/db/schema'
7+
import { eq } from 'drizzle-orm'
58

69
import { postChatCompletions } from './_post'
710

11+
import type { GetUserPreferencesFn } from './_post'
812
import type { NextRequest } from 'next/server'
913

1014
import { getAgentRunFromId } from '@/db/agent-run'
1115
import { getUserInfoFromApiKey } from '@/db/user'
1216
import { logger, loggerWithContext } from '@/util/logger'
1317

18+
const getUserPreferences: GetUserPreferencesFn = async ({ userId }) => {
19+
const userPrefs = await db.query.user.findFirst({
20+
where: eq(schema.user.id, userId),
21+
columns: { fallback_to_a_la_carte: true },
22+
})
23+
return {
24+
fallbackToALaCarte: userPrefs?.fallback_to_a_la_carte ?? false,
25+
}
26+
}
27+
1428
export async function POST(req: NextRequest) {
1529
return postChatCompletions({
1630
req,
@@ -23,5 +37,6 @@ export async function POST(req: NextRequest) {
2337
fetch,
2438
insertMessageBigquery,
2539
ensureSubscriberBlockGrant,
40+
getUserPreferences,
2641
})
2742
}

0 commit comments

Comments
 (0)