From ec8e8859e7d7d54d0f782e98d2d7f4d01b89c264 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:18:24 +0000 Subject: [PATCH] refactor(web): remove abuse service from AI gateway Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../api/openrouter/[...path]/route.test.ts | 313 +-------- .../src/app/api/openrouter/[...path]/route.ts | 162 +---- apps/web/src/lib/ai-gateway/abuse-service.ts | 663 +----------------- .../src/lib/ai-gateway/processUsage.test.ts | 31 - apps/web/src/lib/ai-gateway/processUsage.ts | 13 +- .../src/lib/ai-gateway/processUsage.types.ts | 6 - .../ai-gateway/providers/upstream-attempt.ts | 7 - apps/web/src/lib/proxy-error-types.ts | 1 - apps/web/src/lib/redis-keys.ts | 3 - 9 files changed, 21 insertions(+), 1178 deletions(-) diff --git a/apps/web/src/app/api/openrouter/[...path]/route.test.ts b/apps/web/src/app/api/openrouter/[...path]/route.test.ts index 3d65029d8b..0e7b54f6a4 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { describe, it, expect, beforeEach } from '@jest/globals'; import type { User } from '@kilocode/db/schema'; import jwt from 'jsonwebtoken'; import { getUserFromAuth } from '@/lib/user/server'; @@ -9,7 +9,6 @@ import { KILO_GATEWAY_AUDIENCE, } from '@kilocode/worker-utils/internal-service-token-audiences'; import { getBalanceAndOrgSettings } from '@/lib/organizations/organization-usage'; -import { classifyAbuse } from '@/lib/ai-gateway/abuse-service'; import { getProvider } from '@/lib/ai-gateway/providers/get-provider'; import { upstreamRequest } from '@/lib/ai-gateway/providers/upstream-request'; import { @@ -18,7 +17,6 @@ import { } from '@/lib/ai-gateway/providers/gateway-models-cache'; import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server'; import { accountForMicrodollarUsage } from '@/lib/ai-gateway/llm-proxy-helpers'; -import { redisClient } from '@/lib/redis'; import { ReasoningDetailsTransform, type Provider } from '@/lib/ai-gateway/providers/types'; import { fetchEfficientAutoDecision } from '@/lib/ai-gateway/auto-routing-decision'; import { collectDeniedAutoRoutingModelIds } from '@/lib/ai-gateway/auto-routing-denied-models'; @@ -68,13 +66,6 @@ jest.mock('@/lib/organizations/effective-model-access.server', () => ({ evaluateEffectiveModelAccessPolicy: jest.fn().mockReturnValue({}), getEffectiveModelDecision: jest.fn().mockResolvedValue({ allowed: true }), })); -jest.mock('@/lib/ai-gateway/abuse-service', () => { - const actual = jest.requireActual('@/lib/ai-gateway/abuse-service'); - return { - ...actual, - classifyAbuse: jest.fn(), - }; -}); jest.mock('@/lib/ai-gateway/providers/get-provider'); jest.mock('@/lib/ai-gateway/providers/partner/routing'); jest.mock('@/lib/ai-gateway/providers/direct-byok', () => ({ @@ -82,9 +73,6 @@ jest.mock('@/lib/ai-gateway/providers/direct-byok', () => ({ })); jest.mock('@/lib/ai-gateway/providers/upstream-request'); jest.mock('@/lib/ai-gateway/providers/gateway-models-cache'); -jest.mock('@/lib/redis', () => ({ - redisClient: { get: jest.fn(), set: jest.fn() }, -})); jest.mock('@/lib/ai-gateway/o11y/api-metrics.server', () => ({ emitApiMetricsForResponse: jest.fn(), getToolsAvailable: jest.fn(() => false), @@ -135,15 +123,12 @@ jest.mock('@/lib/ai-gateway/auto-model/resolution', () => { const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); const mockedGetBalanceAndOrgSettings = jest.mocked(getBalanceAndOrgSettings); -const mockedClassifyAbuse = jest.mocked(classifyAbuse); const mockedGetProvider = jest.mocked(getProvider); const mockedUpstreamRequest = jest.mocked(upstreamRequest); const mockedGetOpenRouterModels = jest.mocked(getOpenRouterModelsFromDatabase); const mockedIsValidOpenRouterModelId = jest.mocked(isValidOpenRouterModelId); const mockedEmitApiMetricsForResponse = jest.mocked(emitApiMetricsForResponse); const mockedAccountForMicrodollarUsage = jest.mocked(accountForMicrodollarUsage); -const mockedRedisGet = jest.mocked(redisClient.get); -const mockedRedisSet = jest.mocked(redisClient.set); const mockedFetchEfficientAutoDecision = jest.mocked(fetchEfficientAutoDecision); const mockedCollectDeniedAutoRoutingModelIds = jest.mocked(collectDeniedAutoRoutingModelIds); const mockedLogMicrodollarUsage = jest.mocked(logMicrodollarUsage); @@ -237,36 +222,6 @@ function setUserAuth() { }); } -function classifyResult( - action: 'block' | 'rate-limit' | 'quarantine-1' | 'quarantine-2' | 'quarantine-3' | 'log' | null -) { - return { - verdict: 'ALLOW' as const, - risk_score: 0, - signals: [], - action_metadata: {}, - context: { - identity_key: 'user:user-123', - current_spend_1h: 0, - is_new_user: false, - requests_per_second: 0, - }, - request_id: 123, - rules_engine: { - matches: action ? [{}] : [], - sus_score: action ? 0.9 : 0, - resolved_action: action, - matched_abuse_rule_ids: action ? ['rule-1'] : [], - }, - }; -} - -function cachedRulesEngineAction( - action: NonNullable['rules_engine']['resolved_action']> -) { - return action; -} - function upstreamJsonResponse(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, @@ -320,9 +275,6 @@ describe('POST /api/openrouter/v1/chat/completions bearer audiences', () => { userByok: null, bypassAccessCheck: false, }); - mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); - mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue(new Set()); mockedIsValidOpenRouterModelId.mockResolvedValue(true); mockedUpstreamRequest.mockResolvedValue({ @@ -374,15 +326,6 @@ describe('POST /api/openrouter/v1/chat/completions bearer audiences', () => { expect(providerInput).not.toHaveProperty('tokenSource'); expect(providerInput).not.toHaveProperty('balance'); expect(providerInput).not.toHaveProperty('userByok'); - expect(mockedClassifyAbuse).toHaveBeenCalledWith( - expect.any(Request), - expect.anything(), - expect.objectContaining({ - kiloUserId: 'anon:127.0.0.1', - organizationId: undefined, - isByok: false, - }) - ); expect(mockedAccountForMicrodollarUsage).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ @@ -437,15 +380,6 @@ describe('POST /api/openrouter/v1/chat/completions bearer audiences', () => { expect(mockedGetProvider).toHaveBeenCalledWith( expect.objectContaining({ user: authenticatedUser, organizationId: 'org-123' }) ); - expect(mockedClassifyAbuse).toHaveBeenCalledWith( - expect.any(Request), - expect.anything(), - expect.objectContaining({ - kiloUserId: 'user-123', - organizationId: 'org-123', - isByok: true, - }) - ); expect(mockedAccountForMicrodollarUsage).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ @@ -485,7 +419,7 @@ describe('POST /api/openrouter/v1/chat/completions bearer audiences', () => { }); }); -describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => { +describe('POST /api/openrouter/v1/chat/completions request handling', () => { beforeEach(() => { jest.clearAllMocks(); setUserAuth(); @@ -495,9 +429,6 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => userByok: null, bypassAccessCheck: false, }); - mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); - mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue(new Set(['poolside/laguna-s-2.1:free'])); mockedIsValidOpenRouterModelId.mockResolvedValue(true); mockedUpstreamRequest.mockResolvedValue({ @@ -508,10 +439,6 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => mockedAccountForMicrodollarUsage.mockReturnValue(undefined); }); - afterEach(() => { - jest.useRealTimers(); - }); - it('rejects providerOptions and directs clients to provider', async () => { const { POST } = await import('./route'); const response = await POST( @@ -528,48 +455,6 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(mockedUpstreamRequest).not.toHaveBeenCalled(); }); - it('blocks request-local rules-engine block actions before upstream', async () => { - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('block')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('block')); - - const { POST } = await import('./route'); - const response = await POST(makeRequest(makeBody()) as never); - - expect(response.status).toBe(403); - expect(await response.json()).toMatchObject({ - error_type: 'abuse_blocked', - message: 'Request blocked by abuse prevention rules.', - }); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - }); - - it('uses cached blocking action when blocking abuse refresh fails', async () => { - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('block')); - mockedClassifyAbuse.mockResolvedValue(null); - - const { POST } = await import('./route'); - const response = await POST(makeRequest(makeBody()) as never); - - expect(response.status).toBe(403); - expect(await response.json()).toMatchObject({ error_type: 'abuse_blocked' }); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - }); - - it('does not block upstream on fresh blocking classifications when cache is nonblocking', async () => { - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('log')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('block')); - - const { POST } = await import('./route'); - const response = await POST(makeRequest(makeBody()) as never); - - expect(response.status).toBe(200); - expect(mockedUpstreamRequest).toHaveBeenCalledTimes(1); - expect(mockedRedisSet).toHaveBeenCalledWith( - expect.stringContaining('ai-gateway.abuse-rules:last-classification:user:user-123'), - 'block' - ); - }); - it('passes the Vercel request ID to request logging', async () => { const { POST } = await import('./route'); @@ -801,21 +686,6 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(mockedUpstreamRequest).not.toHaveBeenCalled(); }); - it('rate limits rules-engine rate-limit actions before upstream', async () => { - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('rate-limit')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('rate-limit')); - - const { POST } = await import('./route'); - const response = await POST(makeRequest(makeBody()) as never); - - expect(response.status).toBe(429); - expect(await response.json()).toMatchObject({ - error_type: 'rate_limit_exceeded', - message: 'Rate limit exceeded. Please try again later.', - }); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - }); - it('applies free-model rate limiting to flagged Kilo-exclusive models', async () => { mockedCheckFreeModelRateLimit.mockResolvedValue({ allowed: false, requestCount: 200 }); @@ -854,162 +724,6 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(mockedLogFreeModelRequest).not.toHaveBeenCalled(); expect(mockedUpstreamRequest).toHaveBeenCalledTimes(1); }); - - it('adds latency and rewrites quarantine-3 non-BYOK requests to a free model', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-3')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-3')); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(5999); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - - await jest.advanceTimersByTimeAsync(1); - const response = await responsePromise; - - expect(response.status).toBe(200); - expect(mockedGetProvider).toHaveBeenCalledTimes(2); - expect(mockedGetProvider.mock.calls[1]?.[0].requestedModel).toBe('poolside/laguna-s-2.1:free'); - expect(mockedUpstreamRequest.mock.calls[0]?.[0].body.model).toBe('poolside/laguna-s-2.1:free'); - expect(mockedAccountForMicrodollarUsage.mock.calls[0]?.[1]).toMatchObject({ - abuse_delay: 6000, - abuse_downgraded_from: 'openai/gpt-4o', - }); - }); - - it('applies quarantine-1 latency without model rewrite', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-1')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-1')); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(1999); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - - await jest.advanceTimersByTimeAsync(1); - const response = await responsePromise; - - expect(response.status).toBe(200); - expect(mockedGetProvider).toHaveBeenCalledTimes(1); - expect(mockedUpstreamRequest.mock.calls[0]?.[0].body.model).toBe('openai/gpt-4o'); - expect(mockedAccountForMicrodollarUsage.mock.calls[0]?.[1]).toMatchObject({ - abuse_delay: 2000, - abuse_downgraded_from: null, - }); - }); - - it('applies quarantine-2 latency without model rewrite', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-2')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-2')); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(5999); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - - await jest.advanceTimersByTimeAsync(1); - const response = await responsePromise; - - expect(response.status).toBe(200); - expect(mockedGetProvider).toHaveBeenCalledTimes(1); - expect(mockedUpstreamRequest.mock.calls[0]?.[0].body.model).toBe('openai/gpt-4o'); - expect(mockedAccountForMicrodollarUsage.mock.calls[0]?.[1]).toMatchObject({ - abuse_delay: 6000, - abuse_downgraded_from: null, - }); - }); - - it('applies delay before returning error when quarantine-3 model-override provider fails', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-3')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-3')); - mockedGetProvider - .mockResolvedValueOnce({ - kind: 'provider', - provider, - userByok: null, - bypassAccessCheck: false, - }) - .mockResolvedValueOnce({ kind: 'not-found' }); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(5999); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - - await jest.advanceTimersByTimeAsync(1); - const response = await responsePromise; - - expect(response.status).toBe(404); - expect(mockedGetProvider).toHaveBeenCalledTimes(2); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - }); - - it('applies delay before returning error when quarantine-3 override API kind is unsupported', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-3')); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-3')); - mockedGetProvider - .mockResolvedValueOnce({ - kind: 'provider', - provider, - userByok: null, - bypassAccessCheck: false, - }) - .mockResolvedValueOnce({ - kind: 'provider', - provider: { ...provider, supportedChatApis: ['responses'] }, - userByok: null, - bypassAccessCheck: false, - }); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(5999); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - - await jest.advanceTimersByTimeAsync(1); - const response = await responsePromise; - - expect(response.status).toBe(400); - expect(mockedGetProvider).toHaveBeenCalledTimes(2); - expect(mockedUpstreamRequest).not.toHaveBeenCalled(); - }); - - it('adds latency without rewriting quarantine-3 BYOK requests', async () => { - jest.useFakeTimers(); - mockedRedisGet.mockResolvedValue(cachedRulesEngineAction('quarantine-3')); - mockedGetProvider.mockResolvedValue({ - kind: 'provider', - provider, - userByok: [ - { - decryptedAPIKey: 'byok-key', - providerId: 'openai', - }, - ], - bypassAccessCheck: false, - }); - mockedClassifyAbuse.mockResolvedValue(classifyResult('quarantine-3')); - - const { POST } = await import('./route'); - const responsePromise = POST(makeRequest(makeBody()) as never); - - await jest.advanceTimersByTimeAsync(6000); - const response = await responsePromise; - - expect(response.status).toBe(200); - expect(mockedGetProvider).toHaveBeenCalledTimes(1); - expect(mockedUpstreamRequest.mock.calls[0]?.[0].body.model).toBe('openai/gpt-4o'); - }); }); describe('kilo-auto/efficient classifier billing', () => { @@ -1024,9 +738,6 @@ describe('kilo-auto/efficient classifier billing', () => { userByok: null, bypassAccessCheck: false, }); - mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); - mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue(new Set()); mockedIsValidOpenRouterModelId.mockResolvedValue(true); mockedUpstreamRequest.mockResolvedValue({ @@ -1274,11 +985,15 @@ describe('kilo-auto/efficient classifier billing', () => { expect(mockedLogMicrodollarUsage).not.toHaveBeenCalled(); }); - it('bills the classifier even when the request is rejected downstream (abuse block)', async () => { + it('bills the classifier even when the provider does not support the request API', async () => { // Exit-safe billing: the classifier already spent on Kilo's credential, so - // the row must persist even though the request is blocked before upstream. - mockedRedisGet.mockResolvedValue('block'); - mockedClassifyAbuse.mockResolvedValue(classifyResult('block')); + // the row must persist even though the request is rejected before upstream. + mockedGetProvider.mockResolvedValue({ + kind: 'provider', + provider: { ...provider, supportedChatApis: ['responses'] }, + userByok: null, + bypassAccessCheck: false, + }); mockedFetchEfficientAutoDecision.mockResolvedValue({ decision: { model: 'anthropic/claude-haiku-4', @@ -1294,7 +1009,7 @@ describe('kilo-auto/efficient classifier billing', () => { const { POST } = await import('./route'); const response = await POST(makeRequest(makeBody('kilo-auto/efficient')) as never); - expect(response.status).toBe(403); + expect(response.status).toBe(400); expect(mockedUpstreamRequest).not.toHaveBeenCalled(); await Promise.resolve(); await Promise.resolve(); @@ -1480,9 +1195,6 @@ describe('auto-routing shadow classifier', () => { userByok: null, bypassAccessCheck: false, }); - mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); - mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue(new Set()); mockedIsValidOpenRouterModelId.mockResolvedValue(true); mockedUpstreamRequest.mockResolvedValue({ @@ -1525,9 +1237,6 @@ describe('percentage-routed partner fallback', () => { bypassAccessCheck: false, }); mockedGetPercentageRoutedPartnerProvider.mockResolvedValue(partnerProvider); - mockedClassifyAbuse.mockResolvedValue(classifyResult(null)); - mockedRedisGet.mockResolvedValue(null); - mockedRedisSet.mockResolvedValue('OK'); mockedGetOpenRouterModels.mockResolvedValue(new Set()); mockedIsValidOpenRouterModelId.mockResolvedValue(true); mockedEmitApiMetricsForResponse.mockReturnValue(undefined); diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 6ed31fb28f..ec9ee0f02b 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -78,17 +78,6 @@ import { checkPromotionLimit, } from '@/lib/free-model-rate-limiter'; import { PROMOTION_MAX_REQUESTS, PROMOTION_WINDOW_HOURS } from '@/lib/constants'; -import { - classifyAbuse, - awaitClassifyAbuse, - cacheRulesEngineAction, - getCachedRulesEngineAction, - getQuarantineFreeModel, - getRulesEngineActionDecision, - isRulesEngineBlockingAction, - resolveAbuseClassificationCacheIdentityKey, - sleepForRulesEngineAction, -} from '@/lib/ai-gateway/abuse-service'; import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server'; import { normalizeModelId } from '@/lib/ai-gateway/model-utils'; import { isUnavailableModel } from '@/lib/ai-gateway/unavailable-models'; @@ -403,7 +392,7 @@ export async function POST(request: NextRequest): Promise MAX_TOKENS_LIMIT) { @@ -745,103 +712,6 @@ export async function POST(request: NextRequest): Promise 0) { - await sleepForRulesEngineAction(rulesEngineDecision.delayMs); - } - return modelDoesNotExistResponse(); - } - if (quarantineProviderResult.kind === 'unavailable') { - if (rulesEngineDecision.delayMs > 0) { - await sleepForRulesEngineAction(rulesEngineDecision.delayMs); - } - return temporarilyUnavailableResponse(); - } - - effectiveProviderContext = quarantineProviderResult; - - console.warn('SECURITY: Abuse quarantine-3 model override applied', { - kilo_user_id: user.id, - identity_key: classifyResult?.context?.identity_key ?? abuseCacheIdentityKey, - abuse_request_id: classifyResult?.request_id ?? null, - rules_engine_action: rulesEngineDecision.action, - rules_engine_matched_abuse_rule_ids: - classifyResult?.rules_engine?.matched_abuse_rule_ids ?? [], - original_model: abuseDowngradedFrom, - overridden_model: effectiveModelIdLowerCased, - original_provider: initialProviderResultForAbuseService.provider.id, - overridden_provider: effectiveProviderContext.provider.id, - user_byok: !!effectiveProviderContext.userByok, - feature, - project_id: projectId, - }); - - if (!effectiveProviderContext.provider.supportedChatApis.includes(requestBodyParsed.kind)) { - if (rulesEngineDecision.delayMs > 0) { - await sleepForRulesEngineAction(rulesEngineDecision.delayMs); - } - return apiKindNotSupportedResponse( - requestBodyParsed.kind, - effectiveProviderContext.provider.supportedChatApis - ); - } - } - // Skip balance/org checks for anonymous users - they can only use free models if (!isAnonymousContext(user) && !effectiveProviderContext.bypassAccessCheck) { const { @@ -952,8 +822,6 @@ export async function POST(request: NextRequest): Promise 0 ? rulesEngineDecision.delayMs : null, - abuse_downgraded_from: abuseDowngradedFrom, clientRequestId, }; @@ -1007,7 +875,6 @@ export async function POST(request: NextRequest): Promise c != null && c.type === 'text') - .map(c => c.text ?? '') - .join('\n'); - } - return ''; -} - -function extractFullPromptsFromChatCompletions(body: OpenRouterChatCompletionRequest): { - systemPrompt: string | null; - userPrompt: string | null; -} { - const messages = Array.isArray(body.messages) ? body.messages : []; - - const systemPrompt = - messages - .filter(m => m.role === 'system' || m.role === 'developer') - .map(extractMessageTextContent) - .join('\n') || null; - - const userPrompt = - messages - .filter(m => m.role === 'user') - .map(extractMessageTextContent) - .at(-1) ?? null; - - return { systemPrompt, userPrompt }; -} - -function extractFullPromptsFromResponses(body: GatewayResponsesRequest): { - systemPrompt: string | null; - userPrompt: string | null; -} { - const systemPrompt = body.instructions ?? null; - - let userPrompt: string | null = null; - if (typeof body.input === 'string') { - userPrompt = body.input || null; - } else if (Array.isArray(body.input)) { - userPrompt = - body.input - .map(extractInputItemTextContent) - .filter((t): t is string => t !== null) - .at(-1) ?? null; - } - - return { systemPrompt, userPrompt }; -} - -function extractFullPromptFromMessages(body: GatewayMessagesRequest) { - const systemContent = body.system; - const systemPrompt = - typeof systemContent === 'string' - ? systemContent - : Array.isArray(systemContent) - ? systemContent.map(b => b.text).join('\n') - : null; - const lastUserMessage = body.messages.filter(m => m.role === 'user').at(-1); - let userPrompt: string | null = null; - if (lastUserMessage) { - const content = lastUserMessage.content; - if (typeof content === 'string') { - userPrompt = content; - } else if (Array.isArray(content)) { - userPrompt = - content - .filter((c): c is Anthropic.TextBlockParam => c != null && c.type === 'text') - .map(c => c.text) - .join('\n') || null; - } - } - return { systemPrompt: systemPrompt || null, userPrompt }; -} - -/** - * Verdict types that indicate the action the gateway should take - */ -export type Verdict = 'ALLOW' | 'CHALLENGE' | 'SOFT_BLOCK' | 'HARD_BLOCK'; - -/** - * Signal types indicating which specific heuristics triggered - */ -export type AbuseSignal = - | 'high_velocity' - | 'free_tier_exhausted' - | 'premium_harvester' - | 'suspicious_fingerprint' - | 'datacenter_ip' - | 'known_abuser'; - -/** - * Challenge types for the CHALLENGE verdict - */ -export type ChallengeType = 'turnstile' | 'payment_verification'; - -/** - * Action metadata containing operational instructions for the gateway - */ -export type ActionMetadata = { - /** If verdict is CHALLENGE, the type of challenge to present */ - challenge_type?: ChallengeType; - /** If verdict is SOFT_BLOCK, silently route to this cheaper model */ - model_override?: string; - /** Suggested retry delay in seconds */ - retry_after_seconds?: number; -}; - -/** - * Context information for debugging and observability - */ -export type ClassificationContext = { - /** The resolved identity key used for tracking */ - identity_key: string; - /** Current spend in USD over the last hour */ - current_spend_1h: number; - /** Whether this identity was first seen within the last hour */ - is_new_user: boolean; - /** Current request rate (requests per second over the last minute) */ - requests_per_second: number; -}; - -/** - * Response returned by the /api/classify endpoint - */ -export type AbuseClassificationResponse = { - /** High-level decision for the gateway */ - verdict: Verdict; - /** Risk score from 0.0 (safe) to 1.0 (definite abuse) */ - risk_score: number; - /** Which specific heuristics triggered */ - signals: AbuseSignal[]; - /** Specific operational instructions for the gateway */ - action_metadata: ActionMetadata; - /** State context for debugging headers */ - context: ClassificationContext; - /** Request ID for correlating with cost updates. 0 indicates an error during classification. */ - request_id: number; - /** Rules-engine result used by cloud for enforcement decisions. */ - rules_engine?: RulesEngineClassificationResult; -}; - -export type AbuseRuleAction = z.infer; - -export type RulesEngineClassificationResult = z.infer; - -export type CachedRulesEngineAction = { - identityKey: string; - action: AbuseRuleAction | null; -}; - -export type RulesEngineActionDecision = { - action: AbuseRuleAction | null; - delayMs: number; - modelOverride: string | null; - response: NextResponse | null; -}; - -export function sleepForRulesEngineAction(ms: number): Promise { - console.warn(`SECURITY: Abuse delay of ${ms} ms applied`); - return new Promise(resolve => setTimeout(resolve, ms)); -} - -function rulesEngineBlockResponse() { - const error = 'Request blocked by abuse prevention rules.'; - return NextResponse.json( - { error, error_type: ProxyErrorType.abuse_blocked, message: error }, - { status: 403 } - ); -} - -function rulesEngineRateLimitResponse() { - const error = 'Rate limit exceeded. Please try again later.'; - return NextResponse.json( - { error, error_type: ProxyErrorType.rate_limit_exceeded, message: error }, - { status: 429 } - ); -} - -export async function awaitClassifyAbuse( - classifyPromise: Promise -): Promise { - let timeoutId: ReturnType | undefined; - return await Promise.race([ - classifyPromise.finally(() => timeoutId && clearTimeout(timeoutId)), - new Promise(resolve => { - timeoutId = setTimeout(() => resolve(null), CLASSIFY_ABUSE_TIMEOUT_MS); - }), - ]); -} - -function isAnonymousUserId(kiloUserId: string | null | undefined): boolean { - return kiloUserId?.startsWith('anon:') === true; -} - -async function sha256(value: string): Promise { - const data = new TextEncoder().encode(value); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - return Array.from(new Uint8Array(hashBuffer)) - .map(byte => byte.toString(16).padStart(2, '0')) - .join(''); -} - -export async function resolveAbuseClassificationCacheIdentityKey(args: { - kiloUserId: string | null | undefined; - fraudHeaders: FraudDetectionHeaders; -}): Promise { - const kiloUserId = args.kiloUserId?.trim(); - if (kiloUserId && !isAnonymousUserId(kiloUserId)) { - return `user:${kiloUserId}`; - } - - const compositeParams = [ - args.fraudHeaders.http_x_forwarded_for || 'unknown_ip', - args.fraudHeaders.http_x_vercel_ja4_digest || 'no_ja4', - args.fraudHeaders.http_user_agent || 'no_ua', - ].join('|'); - - return `fingerprint:${await sha256(compositeParams)}`; -} - -function parseCachedRulesEngineAction(raw: string): AbuseRuleAction | null | undefined { - try { - const action = CachedRulesEngineActionSchema.parse(raw); - return action === 'none' ? null : action; - } catch (error) { - console.warn('Failed to parse cached rules-engine action', { error }); - return undefined; - } -} -export async function getCachedRulesEngineAction( - identityKey: string -): Promise { - try { - const raw = await redisClient.get(abuseRulesClassificationRedisKey(identityKey)); - if (!raw) return null; - const action = parseCachedRulesEngineAction(raw); - return action !== undefined ? { identityKey, action } : null; - } catch (error) { - console.warn('Failed to read cached rules-engine action', { identityKey, error }); - return null; - } -} - -export async function cacheRulesEngineAction(args: { - identityKey: string; - rulesEngine: RulesEngineClassificationResult | undefined; -}): Promise { - if (!args.rulesEngine) return; - try { - await redisClient.set( - abuseRulesClassificationRedisKey(args.identityKey), - args.rulesEngine.resolved_action ?? 'none' - ); - } catch (error) { - console.warn('Failed to write cached rules-engine action', { - identityKey: args.identityKey, - error, - }); - } -} - -/** - * Returns true when a cached action is severe enough that the gateway should - * wait for a fresh abuse classification before contacting the upstream model. - */ -export function isRulesEngineBlockingAction(action: AbuseRuleAction | null | undefined): boolean { - return ( - action === 'block' || - action === 'rate-limit' || - action === 'quarantine-1' || - action === 'quarantine-2' || - action === 'quarantine-3' - ); -} - -export async function getQuarantineFreeModel( - apiKind: GatewayRequest['kind'] -): Promise { - const candidates = await getAutoFreeCandidates(apiKind); - const candidate = candidates[0] ?? null; - if (!candidate) { - console.warn('No quarantine free model candidate available', { apiKind }); - } - return candidate; -} - -export function getRulesEngineActionDecision(args: { - action: AbuseRuleAction | null | undefined; - userByok: boolean; - quarantineFreeModel: string | null; -}): RulesEngineActionDecision { - const action = args.action ?? null; - switch (action) { - case null: - case 'nothing': - case 'log': - return { action, delayMs: 0, modelOverride: null, response: null }; - case 'block': - return { action, delayMs: 0, modelOverride: null, response: rulesEngineBlockResponse() }; - case 'rate-limit': - return { action, delayMs: 0, modelOverride: null, response: rulesEngineRateLimitResponse() }; - case 'quarantine-1': - return { action, delayMs: QUARANTINE_1_LATENCY_MS, modelOverride: null, response: null }; - case 'quarantine-2': - return { action, delayMs: QUARANTINE_2_LATENCY_MS, modelOverride: null, response: null }; - case 'quarantine-3': - return { - action, - delayMs: QUARANTINE_2_LATENCY_MS, - modelOverride: args.userByok ? null : args.quarantineFreeModel, - response: null, - }; - default: - console.warn('Ignoring unknown rules-engine action', { action }); - return { action: null, delayMs: 0, modelOverride: null, response: null }; - } -} - -/** - * Request payload matching the microdollar_usage_view schema - * Sent from the Next.js API to classify a request for potential abuse - */ -export type UsagePayload = { - // Identity fields - id?: string; - kilo_user_id?: string | null; - organization_id?: string | null; - project_id?: string | null; - message_id?: string | null; - - // Cost tracking (in microdollars - divide by 1_000_000 for USD) - cost?: number | null; - cache_discount?: number | null; - - // Token usage - input_tokens?: number | null; - output_tokens?: number | null; - cache_write_tokens?: number | null; - cache_hit_tokens?: number | null; - - // Request metadata - ip_address?: string | null; - geo_city?: string | null; - geo_country?: string | null; - geo_latitude?: number | null; - geo_longitude?: number | null; - ja4_digest?: string | null; - user_agent?: string | null; - - // Model information - provider?: string | null; - model?: string | null; - requested_model?: string | null; - inference_provider?: string | null; - - // Prompt content (full prompts for storage and analysis) - user_prompt?: string | null; - system_prompt?: string | null; - max_tokens?: number | null; - has_middle_out_transform?: boolean | null; - has_tools?: boolean | null; - streamed?: boolean | null; - - // Response metadata - status_code?: number | null; - upstream_id?: string | null; - finish_reason?: string | null; - has_error?: boolean | null; - cancelled?: boolean | null; - - // Timing - created_at?: string | null; - latency?: number | null; - moderation_latency?: number | null; - generation_time?: number | null; - - // User context - is_byok?: boolean | null; - is_user_byok?: boolean | null; - editor_name?: string | null; - feature?: string | null; - - // Existing classification (if any) - abuse_classification?: number | null; -}; - -/** - * Shared fetch helper for all abuse service endpoints. - * Handles URL check, CF Access auth headers, and fail-open error handling. - * Returns the parsed JSON response, or null if the service is unavailable or errored. - */ async function fetchAbuseService( path: string, payload: unknown, @@ -510,81 +43,11 @@ async function fetchAbuseService( } } -/** - * Classify a request for potential abuse. - * This is called before proxying requests to detect fraudulent activity. - * - * Currently logs the response only; does not take action. - * - * @param payload - Request details to classify - * @returns Classification response or null if service unavailable - */ -export async function classifyRequest( - payload: UsagePayload -): Promise { - return fetchAbuseService('/api/classify', payload, 'classify'); -} - -/** - * Request payload for reporting cost to the abuse service after request completion. - * Enables spend-based heuristics like free_tier_exhausted. - */ -type CostUpdatePayload = { - // Identity fields (must match what was sent to /classify) - kilo_user_id?: string | null; - ip_address?: string | null; - ja4_digest?: string | null; - user_agent?: string | null; - - // Request identification (REQUIRED) - request_id: number; // From classify response, for correlation - message_id: string; // From LLM response, for analytics - - // Cost data (REQUIRED, in microdollars) - cost: number; - requested_model?: string | null; - - // Token counts (optional but recommended) - input_tokens?: number | null; - output_tokens?: number | null; - cache_write_tokens?: number | null; - cache_hit_tokens?: number | null; -}; - -/** - * Response from the cost update endpoint - */ -export type CostUpdateResponse = { - success: boolean; - identity_key?: string; - message_id?: string; - do_updated?: boolean; - error?: string; -}; - -/** - * Report cost to the abuse service after a request completes. - * This enables spend-based heuristics like free_tier_exhausted. - * - * This is fire-and-forget - failures are logged but don't affect the user. - * - * @param payload - Cost and identity data to report - * @returns Response or null if service unavailable/failed - */ -export async function reportCost(payload: CostUpdatePayload): Promise { - return fetchAbuseService('/api/usage/cost', payload, 'cost update'); -} - -/** - * Payload for the auth event tracking endpoint. - * Tracks signup/signin patterns for abuse detection. - */ export type AuthEventPayload = { - // --- existing fields (unchanged) --- kilo_user_id: string; event_type: 'signup' | 'signin'; email: string; - account_created_at?: string; // ISO 8601 + account_created_at?: string; ip_address?: string | null; geo_city?: string | null; geo_country?: string | null; @@ -593,7 +56,6 @@ export type AuthEventPayload = { auth_method?: AuthProviderId | null; stytch_session_id?: string | null; - // --- NEW: user.* metadata --- hosted_domain?: string | null; signup_ip?: string | null; signup_ja4_digest?: string | null; @@ -608,7 +70,6 @@ export type AuthEventPayload = { has_discord_verified?: boolean | null; cohorts?: string[] | null; - // --- NEW: auth.* metadata --- has_validation_stytch?: boolean | null; has_validation_novel_card_with_hold?: boolean | null; stytch_verdict_action?: string | null; @@ -617,7 +78,6 @@ export type AuthEventPayload = { stytch_hardware_fingerprint?: string | null; auth_providers?: string[] | null; - // --- NEW: org.* metadata --- org_memberships?: Array<{ organization_id: string; role?: string | null; @@ -627,18 +87,10 @@ export type AuthEventPayload = { }> | null; }; -/** - * Report an auth event (signup or signin) to the abuse service. - * Fire-and-forget: catches all errors, never throws, never blocks auth. - */ export async function reportAuthEvent(payload: AuthEventPayload): Promise { await fetchAbuseService('/api/auth-event', payload, 'auth event'); } -// --------------------------------------------------------------------------- -// Generic event batch endpoint — POST /api/events -// --------------------------------------------------------------------------- - type UserEventData = { kilo_user_id: string; reason?: string | null; @@ -712,119 +164,6 @@ type EventsBatchPayload = { events: CloudEvent[]; }; -/** - * Report one or more cloud events to the abuse service. - * Fire-and-forget: catches all errors, never throws, never blocks the caller. - */ export async function reportEvents(payload: EventsBatchPayload): Promise { await fetchAbuseService('/api/events', payload, 'events'); } - -/** - * Context needed to classify abuse for a request. - * All fields are optional to allow classification early in the request lifecycle. - */ -export type AbuseClassificationContext = { - kiloUserId?: string | null; - organizationId?: string | null; - projectId?: string | null; - provider?: string | null; - isByok?: boolean | null; - feature?: FeatureValue | null; -}; - -/** - * High-level function to classify a request for abuse. - * Extracts all needed info from the request and body automatically. - * - * @param request - The incoming NextRequest - * @param body - The parsed OpenRouter request body - * @param context - Additional context (user, org, provider info) - * @returns Classification response or null if service unavailable - */ -export async function classifyAbuse( - request: NextRequest, - requestBodyParsed: GatewayRequest, - context?: AbuseClassificationContext -): Promise { - const fraudHeaders = getFraudDetectionHeaders(request.headers); - const { systemPrompt, userPrompt } = extractFullPrompts(requestBodyParsed); - - const payload: UsagePayload = { - kilo_user_id: context?.kiloUserId ?? null, - organization_id: context?.organizationId ?? null, - project_id: context?.projectId ?? null, - ip_address: fraudHeaders.http_x_forwarded_for, - geo_city: fraudHeaders.http_x_vercel_ip_city, - geo_country: fraudHeaders.http_x_vercel_ip_country, - geo_latitude: fraudHeaders.http_x_vercel_ip_latitude, - geo_longitude: fraudHeaders.http_x_vercel_ip_longitude, - ja4_digest: fraudHeaders.http_x_vercel_ja4_digest, - user_agent: fraudHeaders.http_user_agent, - provider: context?.provider ?? null, - requested_model: requestBodyParsed.body.model?.toLowerCase() ?? null, - user_prompt: userPrompt, - system_prompt: systemPrompt, - max_tokens: getMaxTokens(requestBodyParsed), - has_middle_out_transform: hasMiddleOutTransform(requestBodyParsed), - has_tools: (requestBodyParsed.body.tools?.length ?? 0) > 0, - streamed: requestBodyParsed.body.stream === true, - is_user_byok: context?.isByok ?? null, - editor_name: request.headers.get('x-kilocode-editorname') ?? null, - feature: context?.feature ?? null, - }; - - return classifyRequest(payload); -} - -/** - * Report cost to the abuse service after a request completes. - * Call this after the LLM response is processed and usage stats are available. - * - * Requires usageContext.abuse_request_id (from classify response) and - * usageStats.messageId (from LLM response). Skips if either is missing - * or if abuse_request_id is 0 (indicates classification error). - * - * Use fire-and-forget pattern since this shouldn't block: - * reportAbuseCost(usageContext, usageStats).catch(console.error) - */ -export async function reportAbuseCost( - usageContext: { - kiloUserId: string; - fraudHeaders: { - http_x_forwarded_for: string | null; - http_x_vercel_ja4_digest: string | null; - http_user_agent: string | null; - }; - requested_model: string; - abuse_request_id?: number; - }, - usageStats: { - messageId: string | null; - cost_mUsd: number; - inputTokens: number; - outputTokens: number; - cacheWriteTokens: number; - cacheHitTokens: number; - } -): Promise { - // Skip if missing required fields or request_id is 0 (classification error) - if (!usageContext.abuse_request_id || !usageStats.messageId) { - return null; - } - - return reportCost({ - kilo_user_id: usageContext.kiloUserId, - ip_address: usageContext.fraudHeaders.http_x_forwarded_for, - ja4_digest: usageContext.fraudHeaders.http_x_vercel_ja4_digest, - user_agent: usageContext.fraudHeaders.http_user_agent, - request_id: usageContext.abuse_request_id, - message_id: usageStats.messageId, - cost: usageStats.cost_mUsd, - requested_model: usageContext.requested_model, - input_tokens: usageStats.inputTokens, - output_tokens: usageStats.outputTokens, - cache_write_tokens: usageStats.cacheWriteTokens, - cache_hit_tokens: usageStats.cacheHitTokens, - }); -} diff --git a/apps/web/src/lib/ai-gateway/processUsage.test.ts b/apps/web/src/lib/ai-gateway/processUsage.test.ts index b6a5d5fa54..420a20333c 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.test.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.test.ts @@ -47,9 +47,6 @@ jest.mock('@sentry/nextjs', () => ({ captureMessage: jest.fn(), })); -// Note: Legacy banned_ja4/whitelist_ja4 tests removed - abuse classification -// is now handled by the external abuse detection service (src/lib/abuse-service.ts) - describe('processOpenRouterUsage', () => { const coreProps = { messageId: 'test-message-id', @@ -756,34 +753,6 @@ describe('logMicrodollarUsage', () => { expect(metadataRecord?.session_id).toBe('task-abc123'); }); - test('stores abuse delay and original model when a request is quarantined', async () => { - const user = await insertTestUser({ - id: 'test-log-user-abuse', - microdollars_used: 0, - google_user_email: 'abuse-test@example.com', - }); - - const usageStats: MicrodollarUsageStats = { - ...BASE_USAGE_STATS, - messageId: 'test-msg-abuse', - model: 'nvidia/nemotron-3-super-120b-a12b:free', - }; - const usageContext: MicrodollarUsageContext = { - ...createBaseUsageContext(user), - requested_model: 'nvidia/nemotron-3-super-120b-a12b:free', - abuse_delay: 6000, - abuse_downgraded_from: 'openai/gpt-4o', - }; - - await logMicrodollarUsage(usageStats, usageContext); - - const metadataRecord = await db.query.microdollar_usage_metadata.findFirst({ - where: eq(microdollar_usage_metadata.message_id, 'test-msg-abuse'), - }); - expect(metadataRecord?.abuse_delay).toBe(6000); - expect(metadataRecord?.abuse_downgraded_from).toBe('openai/gpt-4o'); - }); - test('stores usage data without incrementing user microdollars for zero cost', async () => { const user = await insertTestUser({ id: 'test-log-user-2', diff --git a/apps/web/src/lib/ai-gateway/processUsage.ts b/apps/web/src/lib/ai-gateway/processUsage.ts index f858b156e7..893f425c2b 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.ts @@ -47,7 +47,6 @@ import { } from './usage-post-commit-work'; import { appendKiloPassAuditLog } from '@/lib/kilo-pass/issuance'; import { KiloPassAuditLogAction, KiloPassAuditLogResult } from '@/lib/kilo-pass/enums'; -import { reportAbuseCost } from '@/lib/ai-gateway/abuse-service'; import type { BalanceUpdateResult, ChatCompletionChunk, @@ -159,8 +158,8 @@ export function extractUsageContextInfo(usageContext: MicrodollarUsageContext) { mode: usageContext.mode, auto_model: usageContext.auto_model, ttfb_ms: usageContext.ttfb_ms, - abuse_delay: usageContext.abuse_delay ?? null, - abuse_downgraded_from: usageContext.abuse_downgraded_from ?? null, + abuse_delay: null, + abuse_downgraded_from: null, }; } @@ -243,8 +242,6 @@ export async function toInsertableDbUsageRecord( abuse_downgraded_from: metadataFromContext.abuse_downgraded_from, }; - // Legacy heuristic classification removed - abuse_classification is now handled - // by the external abuse detection service in src/lib/abuse-service.ts if (organization_id) { //never log any sensitive data for orgs metadata.user_prompt_prefix = null; @@ -1310,12 +1307,6 @@ export async function processTokenData( const customCost_mUsd = calculateCustomCost_mUsd(usageContext.requested_model, usageStats); - // Report upstream cost to abuse service BEFORE zeroing for free/BYOK - // (abuse service needs actual spend for heuristics like free_tier_exhausted) - reportAbuseCost(usageContext, usageStats).catch(error => { - console.error('[Abuse] Failed to report cost:', error); - }); - // Preserve the real cost before zeroing for free/BYOK usageStats.market_cost ??= usageStats.cost_mUsd; usageStats.cost_mUsd = customCost_mUsd ?? usageStats.cost_mUsd; diff --git a/apps/web/src/lib/ai-gateway/processUsage.types.ts b/apps/web/src/lib/ai-gateway/processUsage.types.ts index 9f7c9f427c..8e70bfa81c 100644 --- a/apps/web/src/lib/ai-gateway/processUsage.types.ts +++ b/apps/web/src/lib/ai-gateway/processUsage.types.ts @@ -138,8 +138,6 @@ export type MicrodollarUsageContext = { has_tools: boolean; botId?: string; tokenSource?: string; - /** Request ID from abuse service classify response, for cost tracking correlation. 0 means skip. */ - abuse_request_id?: number; /** Which product feature generated this API call. NULL if header not sent. */ feature: FeatureValue | null; /** Client session/task identifier from X-KiloCode-TaskId header. */ @@ -150,10 +148,6 @@ export type MicrodollarUsageContext = { auto_model: string | null; /** Time to first byte from the upstream provider, in milliseconds. Set after the upstream request returns. */ ttfb_ms: number | null; - /** Rules-engine delay applied before forwarding the request, in milliseconds. */ - abuse_delay?: number | null; - /** Original model before a rules-engine quarantine override replaced it. */ - abuse_downgraded_from?: string | null; /** * Client-supplied per-message id from the `x-kilo-request` header. * Joinable to PostHog `Feedback Submitted.parentMessageID`. Optional diff --git a/apps/web/src/lib/ai-gateway/providers/upstream-attempt.ts b/apps/web/src/lib/ai-gateway/providers/upstream-attempt.ts index f1bea7edb3..459d321275 100644 --- a/apps/web/src/lib/ai-gateway/providers/upstream-attempt.ts +++ b/apps/web/src/lib/ai-gateway/providers/upstream-attempt.ts @@ -3,7 +3,6 @@ import type { NextResponse } from 'next/server'; import { buildExperimentPromptCapture } from '@/lib/ai-gateway/experiments/persist'; import { getToolsAvailable, getToolsUsed } from '@/lib/ai-gateway/o11y/api-metrics.server'; import type { ExperimentPromptCapture } from '@/lib/ai-gateway/processUsage.types'; -import { sleepForRulesEngineAction } from '@/lib/ai-gateway/abuse-service'; import { applyProviderSpecificLogic } from '@/lib/ai-gateway/providers/apply-provider-specific-logic'; import type { GetProviderProviderResult } from '@/lib/ai-gateway/providers/get-provider'; import { isValidOpenRouterModelId } from '@/lib/ai-gateway/providers/gateway-models-cache'; @@ -21,7 +20,6 @@ type SendUpstreamAttemptInput = { organizationId: string | null; sessionId: string | null; taskId: string | null; - delayMs: number; search: string; method: string; signal?: AbortSignal; @@ -49,7 +47,6 @@ export async function sendUpstreamAttempt({ organizationId, sessionId, taskId, - delayMs, search, method, signal, @@ -80,10 +77,6 @@ export async function sendUpstreamAttempt({ ? buildExperimentPromptCapture(request) : undefined; - if (delayMs > 0) { - await sleepForRulesEngineAction(delayMs); - } - const result = await upstreamRequest({ chatApi: request.kind, search, diff --git a/apps/web/src/lib/proxy-error-types.ts b/apps/web/src/lib/proxy-error-types.ts index 34d1b00e2d..5fc4c5a9a0 100644 --- a/apps/web/src/lib/proxy-error-types.ts +++ b/apps/web/src/lib/proxy-error-types.ts @@ -26,7 +26,6 @@ export const proxyErrorTypeSchema = z.enum([ 'byok_key_required', 'upstream_error', 'no_free_models_available', - 'abuse_blocked', 'organization_auto_configuration', 'upstream_disconnect', 'client_disconnect', diff --git a/apps/web/src/lib/redis-keys.ts b/apps/web/src/lib/redis-keys.ts index b4ac66d1a9..430b776d49 100644 --- a/apps/web/src/lib/redis-keys.ts +++ b/apps/web/src/lib/redis-keys.ts @@ -34,9 +34,6 @@ export const LEADERBOARD_MODEL_PROVIDER_USAGE_REDIS_KEY = redisKey( export const LEADERBOARD_MODEL_USAGE_REDIS_KEY = redisKey('public-api:leaderboard-model-usage'); export const LEADERBOARD_PROVIDER_RACE_REDIS_KEY = redisKey('public-api:leaderboard-provider-race'); -export const abuseRulesClassificationRedisKey = (identityKey: string) => - redisKey(`ai-gateway.abuse-rules:last-classification:${identityKey}`); - export const botIdentityRedisKey = (platform: string, teamId: string, userId: string) => redisKey(`identity:${platform}:${teamId}:${userId}`);