Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export default function RequestLoggingOptInsContent() {
<Card>
<CardHeader>
<CardTitle>Active opt-ins</CardTitle>
<CardDescription>Hardcoded email-domain opt-ins are not listed here.</CardDescription>
<CardDescription>
Requests are logged only when the account or organization is opted in.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
Expand All @@ -164,7 +166,7 @@ export default function RequestLoggingOptInsContent() {
{!isLoading && optIns?.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="text-muted-foreground">
No dynamic request logging opt-ins.
No request logging opt-ins.
</TableCell>
</TableRow>
)}
Expand Down
46 changes: 40 additions & 6 deletions apps/web/src/lib/ai-gateway/rewriteModelResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -1409,19 +1444,18 @@ 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',
providerId: 'custom',
logging: makeLogging(),
});

expect(mockedAfter).toHaveBeenCalledTimes(1);
expect(mockedOptIn).not.toHaveBeenCalled();
expect(mockedAfter).not.toHaveBeenCalled();
expect(mockedOptIn).toHaveBeenCalledWith({ accountId: null, organizationId: null });
});
});

Expand Down
20 changes: 6 additions & 14 deletions apps/web/src/lib/ai-gateway/rewriteModelResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,6 @@ type CapturedResponseBody =
| { text: string; readError?: never }
| { readError: string; text?: string };

async function isLoggingEnabledForUser(
user: User | null,
organizationId: string | null
): Promise<boolean> {
// 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) {
Expand Down Expand Up @@ -115,7 +102,12 @@ async function createRequestLogCapture(
logging: RequestLoggingParams
): Promise<RequestLogCapture | null> {
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;
Expand Down