diff --git a/packages/code-map/__tests__/parse.test.ts b/packages/code-map/__tests__/parse.test.ts index 57dd11251d..a15d881c05 100644 --- a/packages/code-map/__tests__/parse.test.ts +++ b/packages/code-map/__tests__/parse.test.ts @@ -132,7 +132,7 @@ describe('parse module', () => { () => multilineCode, ) - expect(result.numLines).toBe(2) // Due to operator precedence: .match(/\n/g)?.length ?? 0 + 1 becomes (2 ?? 1) = 2 + expect(result.numLines).toBe(3) }) it('should deduplicate identifiers and calls', () => { diff --git a/packages/code-map/src/parse.ts b/packages/code-map/src/parse.ts index 2ab2a0fc05..09c1866a2f 100644 --- a/packages/code-map/src/parse.ts +++ b/packages/code-map/src/parse.ts @@ -169,7 +169,7 @@ export function parseTokens( calls: [] as string[], } } - const numLines = sourceCode.match(/\n/g)?.length ?? 0 + 1 + const numLines = (sourceCode.match(/\n/g)?.length ?? 0) + 1 if (!parser || !query) { throw new Error('Parser or query not found') } diff --git a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts index e503f4c7c6..40318501af 100644 --- a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts +++ b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts @@ -18,32 +18,25 @@ import type { BlockGrantResult } from '@codebuff/billing/subscription' import type { GetUserPreferencesFn } from '../_post' describe('/api/v1/chat/completions POST endpoint', () => { - // Old enough to clear the account-age gate in _post.ts - const AGED_ACCOUNT_CREATED_AT = new Date('2024-01-01T00:00:00Z') - const mockUserData: Record< string, - { id: string; banned: boolean; created_at: Date } + { id: string; banned: boolean } > = { 'test-api-key-123': { id: 'user-123', banned: false, - created_at: AGED_ACCOUNT_CREATED_AT, }, 'test-api-key-no-credits': { id: 'user-no-credits', banned: false, - created_at: AGED_ACCOUNT_CREATED_AT, }, 'test-api-key-blocked': { id: 'banned-user-id', banned: true, - created_at: AGED_ACCOUNT_CREATED_AT, }, 'test-api-key-new-free': { id: 'user-new-free', banned: false, - created_at: new Date(), }, } @@ -57,7 +50,6 @@ describe('/api/v1/chat/completions POST endpoint', () => { return { id: userData.id, banned: userData.banned, - created_at: userData.created_at, } as Awaited> } @@ -95,22 +87,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { totalDebt: 0, netBalance: 0, breakdown: {}, - // Has purchased credits historically (principals > 0) but 0 remaining - // so the paid-plan gate passes and the credit check is what enforces 402. - principals: { purchase: 100 }, - }, - nextQuotaReset, - } - } - if (userId === 'user-new-free') { - return { - usageThisCycle: 0, - balance: { - totalRemaining: 100, - totalDebt: 0, - netBalance: 100, - breakdown: {} as Record, - principals: {} as Record, + principals: {}, }, nextQuotaReset, } @@ -122,7 +99,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { totalDebt: 0, netBalance: 100, breakdown: {}, - principals: { purchase: 100 }, + principals: {}, }, nextQuotaReset, } @@ -460,7 +437,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { expect(body.message).not.toContain(nextQuotaReset) }) - it('returns 403 for a free-tier user with no paid relationship', async () => { + it('lets a new account with no paid relationship through for non-free mode', async () => { const req = new NextRequest( 'http://localhost:3000/api/v1/chat/completions', { @@ -489,43 +466,6 @@ describe('/api/v1/chat/completions POST endpoint', () => { loggerWithContext: mockLoggerWithContext, }) - expect(response.status).toBe(403) - const body = await response.json() - expect(body.error).toBe('requires_paid_plan') - }) - - it('lets a BYOK free-tier new account through the paid-plan gate', async () => { - const req = new NextRequest( - 'http://localhost:3000/api/v1/chat/completions', - { - method: 'POST', - headers: { - Authorization: 'Bearer test-api-key-new-free', - 'x-openrouter-api-key': 'sk-or-byok-test', - }, - body: JSON.stringify({ - model: 'test/test-model', - stream: false, - codebuff_metadata: { - run_id: 'run-123', - client_id: 'test-client-id-123', - }, - }), - }, - ) - - const response = await postChatCompletions({ - req, - getUserInfoFromApiKey: mockGetUserInfoFromApiKey, - logger: mockLogger, - trackEvent: mockTrackEvent, - getUserUsageData: mockGetUserUsageData, - getAgentRunFromId: mockGetAgentRunFromId, - fetch: mockFetch, - insertMessageBigquery: mockInsertMessageBigquery, - loggerWithContext: mockLoggerWithContext, - }) - expect(response.status).toBe(200) }) diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts index 93e052e4b6..b243a2c3c1 100644 --- a/web/src/app/api/v1/chat/completions/_post.ts +++ b/web/src/app/api/v1/chat/completions/_post.ts @@ -77,14 +77,6 @@ const FREE_MODE_ALLOWED_COUNTRIES = new Set([ 'NO', 'SE', 'NL', 'DK', 'DE', 'FI', 'BE', 'LU', 'CH', 'IE', 'IS', ]) -const MIN_ACCOUNT_AGE_DAYS = 3 -const MIN_ACCOUNT_AGE_FOR_PAID_MS = MIN_ACCOUNT_AGE_DAYS * 24 * 60 * 60 * 1000 - -// Emails allowed to bypass the paid+aged-account gate so integration tests -// (e.g. the SDK prompt-caching test) can run against a real server without -// needing to seed a purchase on every fresh test account. -const PAID_GATE_BYPASS_EMAILS = new Set(['team@codebuff.com']) - function extractClientIp(req: NextRequest): string | undefined { const forwardedFor = req.headers.get('x-forwarded-for') if (forwardedFor) { @@ -217,7 +209,7 @@ export async function postChatCompletions(params: { // Get user info const userInfo = await getUserInfoFromApiKey({ apiKey, - fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned', 'created_at'], + fields: ['id', 'email', 'discord_id', 'stripe_customer_id', 'banned'], logger, }) if (!userInfo) { @@ -483,50 +475,10 @@ export async function postChatCompletions(params: { // Fetch user credit data (includes subscription credits when block grant was ensured) const { - balance: { totalRemaining, principals }, + balance: { totalRemaining }, nextQuotaReset, } = await getUserUsageData({ userId, logger, includeSubscriptionCredits }) - // Gate non-free-mode requests behind (a) an established paid relationship - // AND (b) a non-new account. An ongoing abuse campaign uses freshly-signed-up - // self-referral accounts to burn credits via the stream-error billing gap in - // openrouter.ts; restricting to aged + paid accounts cuts off that vector. - // BYOK users bypass — they pay OpenRouter directly, so there's nothing to burn. - const openrouterApiKeyHeader = req.headers.get(BYOK_OPENROUTER_HEADER) - const hasPaidRelationship = - (principals.purchase ?? 0) > 0 || (principals.subscription ?? 0) > 0 - const accountAgeMs = userInfo.created_at - ? Date.now() - new Date(userInfo.created_at).getTime() - : 0 - const accountIsTooNew = accountAgeMs < MIN_ACCOUNT_AGE_FOR_PAID_MS - const isBypassedEmail = - !!userInfo.email && PAID_GATE_BYPASS_EMAILS.has(userInfo.email.toLowerCase()) - if ( - !isFreeModeRequest && - !openrouterApiKeyHeader && - !isBypassedEmail && - (!hasPaidRelationship || accountIsTooNew) - ) { - trackEvent({ - event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR, - userId, - properties: { - error: 'blocked_for_free_tier', - model: typedBody.model, - hasPaidRelationship, - accountAgeMs, - }, - logger, - }) - return NextResponse.json( - { - error: 'requires_paid_plan', - 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.`, - }, - { status: 403 }, - ) - } - // Credit check if (totalRemaining <= 0 && !isFreeModeRequest) { trackEvent({ @@ -547,7 +499,7 @@ export async function postChatCompletions(params: { ) } - const openrouterApiKey = openrouterApiKeyHeader + const openrouterApiKey = req.headers.get(BYOK_OPENROUTER_HEADER) // Handle streaming vs non-streaming try {