Skip to content

Commit 5c51810

Browse files
committed
Revert restrictions on using paid codebuff
1 parent 45fe312 commit 5c51810

2 files changed

Lines changed: 14 additions & 86 deletions

File tree

web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,25 @@ import type { BlockGrantResult } from '@codebuff/billing/subscription'
1818
import type { GetUserPreferencesFn } from '../_post'
1919

2020
describe('/api/v1/chat/completions POST endpoint', () => {
21-
// Old enough to clear the account-age gate in _post.ts
22-
const AGED_ACCOUNT_CREATED_AT = new Date('2024-01-01T00:00:00Z')
23-
2421
const mockUserData: Record<
2522
string,
26-
{ id: string; banned: boolean; created_at: Date }
23+
{ id: string; banned: boolean }
2724
> = {
2825
'test-api-key-123': {
2926
id: 'user-123',
3027
banned: false,
31-
created_at: AGED_ACCOUNT_CREATED_AT,
3228
},
3329
'test-api-key-no-credits': {
3430
id: 'user-no-credits',
3531
banned: false,
36-
created_at: AGED_ACCOUNT_CREATED_AT,
3732
},
3833
'test-api-key-blocked': {
3934
id: 'banned-user-id',
4035
banned: true,
41-
created_at: AGED_ACCOUNT_CREATED_AT,
4236
},
4337
'test-api-key-new-free': {
4438
id: 'user-new-free',
4539
banned: false,
46-
created_at: new Date(),
4740
},
4841
}
4942

@@ -57,7 +50,6 @@ describe('/api/v1/chat/completions POST endpoint', () => {
5750
return {
5851
id: userData.id,
5952
banned: userData.banned,
60-
created_at: userData.created_at,
6153
} as Awaited<ReturnType<GetUserInfoFromApiKeyFn>>
6254
}
6355

@@ -82,15 +74,15 @@ describe('/api/v1/chat/completions POST endpoint', () => {
8274
).toISOString()
8375

8476
mockLogger = {
85-
error: mock(() => {}),
86-
warn: mock(() => {}),
87-
info: mock(() => {}),
88-
debug: mock(() => {}),
77+
error: mock(() => { }),
78+
warn: mock(() => { }),
79+
info: mock(() => { }),
80+
debug: mock(() => { }),
8981
}
9082

9183
mockLoggerWithContext = mock(() => mockLogger)
9284

93-
mockTrackEvent = mock(() => {})
85+
mockTrackEvent = mock(() => { })
9486

9587
mockGetUserUsageData = mock(async ({ userId }: { userId: string }) => {
9688
if (userId === 'user-no-credits') {
@@ -101,22 +93,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
10193
totalDebt: 0,
10294
netBalance: 0,
10395
breakdown: {},
104-
// Has purchased credits historically (principals > 0) but 0 remaining
105-
// so the paid-plan gate passes and the credit check is what enforces 402.
106-
principals: { purchase: 100 },
107-
},
108-
nextQuotaReset,
109-
}
110-
}
111-
if (userId === 'user-new-free') {
112-
return {
113-
usageThisCycle: 0,
114-
balance: {
115-
totalRemaining: 100,
116-
totalDebt: 0,
117-
netBalance: 100,
118-
breakdown: {} as Record<string, number>,
119-
principals: {} as Record<string, number>,
96+
principals: {},
12097
},
12198
nextQuotaReset,
12299
}
@@ -128,7 +105,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
128105
totalDebt: 0,
129106
netBalance: 100,
130107
breakdown: {},
131-
principals: { purchase: 100 },
108+
principals: {},
132109
},
133110
nextQuotaReset,
134111
}
@@ -474,7 +451,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
474451
expect(body.message).not.toContain(nextQuotaReset)
475452
})
476453

477-
it('returns 403 for a free-tier user with no paid relationship', async () => {
454+
it('lets a new account with no paid relationship through for non-free mode', async () => {
478455
const req = new NextRequest(
479456
'http://localhost:3000/api/v1/chat/completions',
480457
{
@@ -504,11 +481,10 @@ describe('/api/v1/chat/completions POST endpoint', () => {
504481
checkSessionAdmissible: mockCheckSessionAdmissibleAllow,
505482
})
506483

507-
expect(response.status).toBe(403)
508-
const body = await response.json()
509-
expect(body.error).toBe('requires_paid_plan')
484+
expect(response.status).toBe(200)
510485
})
511486

487+
512488
it('lets a BYOK free-tier new account through the paid-plan gate', async () => {
513489
const req = new NextRequest(
514490
'http://localhost:3000/api/v1/chat/completions',

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

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,6 @@ const FREE_MODE_ALLOWED_COUNTRIES = new Set([
7878
'NO', 'SE', 'NL', 'DK', 'DE', 'FI', 'BE', 'LU', 'CH', 'IE', 'IS',
7979
])
8080

81-
const MIN_ACCOUNT_AGE_DAYS = 3
82-
const MIN_ACCOUNT_AGE_FOR_PAID_MS = MIN_ACCOUNT_AGE_DAYS * 24 * 60 * 60 * 1000
83-
84-
// Emails allowed to bypass the paid+aged-account gate so integration tests
85-
// (e.g. the SDK prompt-caching test) can run against a real server without
86-
// needing to seed a purchase on every fresh test account.
87-
const PAID_GATE_BYPASS_EMAILS = new Set(['team@codebuff.com'])
88-
8981
function extractClientIp(req: NextRequest): string | undefined {
9082
const forwardedFor = req.headers.get('x-forwarded-for')
9183
if (forwardedFor) {
@@ -224,7 +216,7 @@ export async function postChatCompletions(params: {
224216
// Get user info
225217
const userInfo = await getUserInfoFromApiKey({
226218
apiKey,
227-
fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned', 'created_at'],
219+
fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned'],
228220
logger,
229221
})
230222
if (!userInfo) {
@@ -520,50 +512,10 @@ export async function postChatCompletions(params: {
520512

521513
// Fetch user credit data (includes subscription credits when block grant was ensured)
522514
const {
523-
balance: { totalRemaining, principals },
515+
balance: { totalRemaining },
524516
nextQuotaReset,
525517
} = await getUserUsageData({ userId, logger, includeSubscriptionCredits })
526518

527-
// Gate non-free-mode requests behind (a) an established paid relationship
528-
// AND (b) a non-new account. An ongoing abuse campaign uses freshly-signed-up
529-
// self-referral accounts to burn credits via the stream-error billing gap in
530-
// openrouter.ts; restricting to aged + paid accounts cuts off that vector.
531-
// BYOK users bypass — they pay OpenRouter directly, so there's nothing to burn.
532-
const openrouterApiKeyHeader = req.headers.get(BYOK_OPENROUTER_HEADER)
533-
const hasPaidRelationship =
534-
(principals.purchase ?? 0) > 0 || (principals.subscription ?? 0) > 0
535-
const accountAgeMs = userInfo.created_at
536-
? Date.now() - new Date(userInfo.created_at).getTime()
537-
: 0
538-
const accountIsTooNew = accountAgeMs < MIN_ACCOUNT_AGE_FOR_PAID_MS
539-
const isBypassedEmail =
540-
!!userInfo.email && PAID_GATE_BYPASS_EMAILS.has(userInfo.email.toLowerCase())
541-
if (
542-
!isFreeModeRequest &&
543-
!openrouterApiKeyHeader &&
544-
!isBypassedEmail &&
545-
(!hasPaidRelationship || accountIsTooNew)
546-
) {
547-
trackEvent({
548-
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
549-
userId,
550-
properties: {
551-
error: 'blocked_for_free_tier',
552-
model: typedBody.model,
553-
hasPaidRelationship,
554-
accountAgeMs,
555-
},
556-
logger,
557-
})
558-
return NextResponse.json(
559-
{
560-
error: 'requires_paid_plan',
561-
message: `Non-free mode requires a paid subscription or purchased credits on an account at least ${MIN_ACCOUNT_AGE_DAYS} days old. Visit ${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/usage to upgrade, or pass an OpenRouter API key to bring your own credits.`,
562-
},
563-
{ status: 403 },
564-
)
565-
}
566-
567519
// Credit check
568520
if (totalRemaining <= 0 && !isFreeModeRequest) {
569521
trackEvent({
@@ -584,7 +536,7 @@ export async function postChatCompletions(params: {
584536
)
585537
}
586538

587-
const openrouterApiKey = openrouterApiKeyHeader
539+
const openrouterApiKey = req.headers.get(BYOK_OPENROUTER_HEADER)
588540

589541
// Handle streaming vs non-streaming
590542
try {

0 commit comments

Comments
 (0)