Skip to content

Commit 7f24658

Browse files
committed
Revert restrictions on using paid codebuff
1 parent a75349a commit 7f24658

2 files changed

Lines changed: 7 additions & 115 deletions

File tree

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

Lines changed: 4 additions & 64 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

@@ -95,22 +87,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
9587
totalDebt: 0,
9688
netBalance: 0,
9789
breakdown: {},
98-
// Has purchased credits historically (principals > 0) but 0 remaining
99-
// so the paid-plan gate passes and the credit check is what enforces 402.
100-
principals: { purchase: 100 },
101-
},
102-
nextQuotaReset,
103-
}
104-
}
105-
if (userId === 'user-new-free') {
106-
return {
107-
usageThisCycle: 0,
108-
balance: {
109-
totalRemaining: 100,
110-
totalDebt: 0,
111-
netBalance: 100,
112-
breakdown: {} as Record<string, number>,
113-
principals: {} as Record<string, number>,
90+
principals: {},
11491
},
11592
nextQuotaReset,
11693
}
@@ -122,7 +99,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
12299
totalDebt: 0,
123100
netBalance: 100,
124101
breakdown: {},
125-
principals: { purchase: 100 },
102+
principals: {},
126103
},
127104
nextQuotaReset,
128105
}
@@ -460,7 +437,7 @@ describe('/api/v1/chat/completions POST endpoint', () => {
460437
expect(body.message).not.toContain(nextQuotaReset)
461438
})
462439

463-
it('returns 403 for a free-tier user with no paid relationship', async () => {
440+
it('lets a new account with no paid relationship through for non-free mode', async () => {
464441
const req = new NextRequest(
465442
'http://localhost:3000/api/v1/chat/completions',
466443
{
@@ -489,43 +466,6 @@ describe('/api/v1/chat/completions POST endpoint', () => {
489466
loggerWithContext: mockLoggerWithContext,
490467
})
491468

492-
expect(response.status).toBe(403)
493-
const body = await response.json()
494-
expect(body.error).toBe('requires_paid_plan')
495-
})
496-
497-
it('lets a BYOK free-tier new account through the paid-plan gate', async () => {
498-
const req = new NextRequest(
499-
'http://localhost:3000/api/v1/chat/completions',
500-
{
501-
method: 'POST',
502-
headers: {
503-
Authorization: 'Bearer test-api-key-new-free',
504-
'x-openrouter-api-key': 'sk-or-byok-test',
505-
},
506-
body: JSON.stringify({
507-
model: 'test/test-model',
508-
stream: false,
509-
codebuff_metadata: {
510-
run_id: 'run-123',
511-
client_id: 'test-client-id-123',
512-
},
513-
}),
514-
},
515-
)
516-
517-
const response = await postChatCompletions({
518-
req,
519-
getUserInfoFromApiKey: mockGetUserInfoFromApiKey,
520-
logger: mockLogger,
521-
trackEvent: mockTrackEvent,
522-
getUserUsageData: mockGetUserUsageData,
523-
getAgentRunFromId: mockGetAgentRunFromId,
524-
fetch: mockFetch,
525-
insertMessageBigquery: mockInsertMessageBigquery,
526-
loggerWithContext: mockLoggerWithContext,
527-
})
528-
529469
expect(response.status).toBe(200)
530470
})
531471

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

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

80-
const MIN_ACCOUNT_AGE_DAYS = 3
81-
const MIN_ACCOUNT_AGE_FOR_PAID_MS = MIN_ACCOUNT_AGE_DAYS * 24 * 60 * 60 * 1000
82-
83-
// Emails allowed to bypass the paid+aged-account gate so integration tests
84-
// (e.g. the SDK prompt-caching test) can run against a real server without
85-
// needing to seed a purchase on every fresh test account.
86-
const PAID_GATE_BYPASS_EMAILS = new Set(['team@codebuff.com'])
87-
8880
function extractClientIp(req: NextRequest): string | undefined {
8981
const forwardedFor = req.headers.get('x-forwarded-for')
9082
if (forwardedFor) {
@@ -217,7 +209,7 @@ export async function postChatCompletions(params: {
217209
// Get user info
218210
const userInfo = await getUserInfoFromApiKey({
219211
apiKey,
220-
fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned', 'created_at'],
212+
fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned'],
221213
logger,
222214
})
223215
if (!userInfo) {
@@ -483,50 +475,10 @@ export async function postChatCompletions(params: {
483475

484476
// Fetch user credit data (includes subscription credits when block grant was ensured)
485477
const {
486-
balance: { totalRemaining, principals },
478+
balance: { totalRemaining },
487479
nextQuotaReset,
488480
} = await getUserUsageData({ userId, logger, includeSubscriptionCredits })
489481

490-
// Gate non-free-mode requests behind (a) an established paid relationship
491-
// AND (b) a non-new account. An ongoing abuse campaign uses freshly-signed-up
492-
// self-referral accounts to burn credits via the stream-error billing gap in
493-
// openrouter.ts; restricting to aged + paid accounts cuts off that vector.
494-
// BYOK users bypass — they pay OpenRouter directly, so there's nothing to burn.
495-
const openrouterApiKeyHeader = req.headers.get(BYOK_OPENROUTER_HEADER)
496-
const hasPaidRelationship =
497-
(principals.purchase ?? 0) > 0 || (principals.subscription ?? 0) > 0
498-
const accountAgeMs = userInfo.created_at
499-
? Date.now() - new Date(userInfo.created_at).getTime()
500-
: 0
501-
const accountIsTooNew = accountAgeMs < MIN_ACCOUNT_AGE_FOR_PAID_MS
502-
const isBypassedEmail =
503-
!!userInfo.email && PAID_GATE_BYPASS_EMAILS.has(userInfo.email.toLowerCase())
504-
if (
505-
!isFreeModeRequest &&
506-
!openrouterApiKeyHeader &&
507-
!isBypassedEmail &&
508-
(!hasPaidRelationship || accountIsTooNew)
509-
) {
510-
trackEvent({
511-
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
512-
userId,
513-
properties: {
514-
error: 'blocked_for_free_tier',
515-
model: typedBody.model,
516-
hasPaidRelationship,
517-
accountAgeMs,
518-
},
519-
logger,
520-
})
521-
return NextResponse.json(
522-
{
523-
error: 'requires_paid_plan',
524-
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.`,
525-
},
526-
{ status: 403 },
527-
)
528-
}
529-
530482
// Credit check
531483
if (totalRemaining <= 0 && !isFreeModeRequest) {
532484
trackEvent({
@@ -547,7 +499,7 @@ export async function postChatCompletions(params: {
547499
)
548500
}
549501

550-
const openrouterApiKey = openrouterApiKeyHeader
502+
const openrouterApiKey = req.headers.get(BYOK_OPENROUTER_HEADER)
551503

552504
// Handle streaming vs non-streaming
553505
try {

0 commit comments

Comments
 (0)