diff --git a/apps/web/src/app/admin/request-logging-opt-ins/RequestLoggingOptInsContent.tsx b/apps/web/src/app/admin/request-logging-opt-ins/RequestLoggingOptInsContent.tsx index f63ad610ed..6c849eb496 100644 --- a/apps/web/src/app/admin/request-logging-opt-ins/RequestLoggingOptInsContent.tsx +++ b/apps/web/src/app/admin/request-logging-opt-ins/RequestLoggingOptInsContent.tsx @@ -139,7 +139,9 @@ export default function RequestLoggingOptInsContent() { Active opt-ins - Hardcoded email-domain opt-ins are not listed here. + + Requests are logged only when the account or organization is opted in. + @@ -164,7 +166,7 @@ export default function RequestLoggingOptInsContent() { {!isLoading && optIns?.length === 0 && ( - No dynamic request logging opt-ins. + No request logging opt-ins. )} diff --git a/apps/web/src/lib/ai-gateway/rewriteModelResponse.test.ts b/apps/web/src/lib/ai-gateway/rewriteModelResponse.test.ts index 88c6929f4e..f0c22bbb0c 100644 --- a/apps/web/src/lib/ai-gateway/rewriteModelResponse.test.ts +++ b/apps/web/src/lib/ai-gateway/rewriteModelResponse.test.ts @@ -1382,9 +1382,10 @@ describe('rewriteModelResponse', () => { }); expect(result).not.toBeNull(); + expect(mockedAfter).toHaveBeenCalledTimes(1); }); - test('does not schedule a log insert for non-custom models without opt-in', async () => { + test('does not schedule a log insert without opt-in', async () => { await rewriteModelResponse({ response: jsonResponse({ model: 'openai/gpt-5' }), model: 'openai/gpt-5', @@ -1398,7 +1399,41 @@ describe('rewriteModelResponse', () => { expect(mockedOptIn).toHaveBeenCalled(); }); - test('always schedules a log insert for custom models', async () => { + test.each(['user@anaconda.com', 'user@kilocode.ai'])( + 'requires an explicit opt-in for %s', + async email => { + await rewriteModelResponse({ + response: jsonResponse({ model: 'openai/gpt-5' }), + model: 'openai/gpt-5', + providerId: 'openrouter', + kind: 'chat_completions', + logging: makeLogging({ + user: { id: 'test-user', google_user_email: email } as RequestLoggingParams['user'], + }), + responseTransforms: null, + }); + + expect(mockedAfter).not.toHaveBeenCalled(); + expect(mockedOptIn).toHaveBeenCalledWith({ accountId: 'test-user', organizationId: null }); + } + ); + + test('does not log custom models without opt-in', async () => { + await rewriteModelResponse({ + response: jsonResponse({ model: 'kilo-internal/my-model' }), + model: 'kilo-internal/my-model', + providerId: 'custom', + kind: 'chat_completions', + logging: makeLogging(), + responseTransforms: null, + }); + + expect(mockedAfter).not.toHaveBeenCalled(); + expect(mockedOptIn).toHaveBeenCalledWith({ accountId: null, organizationId: null }); + }); + + test('logs custom models with opt-in', async () => { + mockedOptIn.mockResolvedValueOnce(true); await rewriteModelResponse({ response: jsonResponse({ model: 'kilo-internal/my-model' }), model: 'kilo-internal/my-model', @@ -1409,10 +1444,9 @@ describe('rewriteModelResponse', () => { }); expect(mockedAfter).toHaveBeenCalledTimes(1); - expect(mockedOptIn).not.toHaveBeenCalled(); }); - test('always logs unrewritten custom model responses', async () => { + test('does not log unrewritten custom model responses without opt-in', async () => { await logUnrewrittenResponse({ response: jsonResponse({ error: 'upstream error' }, 400), model: 'kilo-internal/my-model', @@ -1420,8 +1454,8 @@ describe('rewriteModelResponse', () => { logging: makeLogging(), }); - expect(mockedAfter).toHaveBeenCalledTimes(1); - expect(mockedOptIn).not.toHaveBeenCalled(); + expect(mockedAfter).not.toHaveBeenCalled(); + expect(mockedOptIn).toHaveBeenCalledWith({ accountId: null, organizationId: null }); }); }); diff --git a/apps/web/src/lib/ai-gateway/rewriteModelResponse.ts b/apps/web/src/lib/ai-gateway/rewriteModelResponse.ts index 884f946d5b..85973bc449 100644 --- a/apps/web/src/lib/ai-gateway/rewriteModelResponse.ts +++ b/apps/web/src/lib/ai-gateway/rewriteModelResponse.ts @@ -73,19 +73,6 @@ type CapturedResponseBody = | { text: string; readError?: never } | { readError: string; text?: string }; -async function isLoggingEnabledForUser( - user: User | null, - organizationId: string | null -): Promise { - // Hardcoded opt-ins mainly for local testing - if (user?.google_user_email.endsWith('@anaconda.com')) return true; - if (user?.google_user_email.endsWith('@kilocode.ai')) return true; - return isDynamicallyOptedIntoRequestLogging({ - accountId: user?.id ?? null, - organizationId, - }); -} - export function sanitizeApiRequestLogRequest(request: GatewayRequest): unknown { const gateway = request.body.providerOptions?.gateway; if (!gateway?.byok) { @@ -115,7 +102,12 @@ async function createRequestLogCapture( logging: RequestLoggingParams ): Promise { const { user, organization_id, session_id, vercel_request_id, request } = logging; - if (provider !== 'custom' && !(await isLoggingEnabledForUser(user, organization_id))) { + if ( + !(await isDynamicallyOptedIntoRequestLogging({ + accountId: user?.id ?? null, + organizationId: organization_id, + })) + ) { return null; } const status = response.status;