From 0d03fc8e7a9a6fdb1d5e94dbc3bfd89dcedffe53 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:31:36 +0000 Subject: [PATCH 1/8] fix(ai-gateway): apply provider policy before Vercel routing Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../api/openrouter/[...path]/route.test.ts | 25 ++++++ .../src/app/api/openrouter/[...path]/route.ts | 82 +++++++++++++------ .../lib/ai-gateway/providers/get-provider.ts | 26 +++++- .../lib/ai-gateway/providers/vercel/index.ts | 6 +- 4 files changed, 108 insertions(+), 31 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 4535305ddf..9b4e00c5b9 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -395,6 +395,31 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(mockedGetBalanceAndOrgSettings.mock.calls[0]?.[2]).toBe(readDb); }); + it('selects the provider after applying the organization provider allow-list', async () => { + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: 'user-123', + google_user_email: 'test@example.com', + microdollars_used: 0, + } as User, + authFailedResponse: null, + organizationId: 'org-1', + }); + mockedGetBalanceAndOrgSettings.mockResolvedValue({ + balance: 1000, + settings: { provider_allow_list: ['amazon-bedrock'] }, + plan: 'enterprise', + }); + + const { POST } = await import('./route'); + const response = await POST(makeRequest(makeBody('anthropic/claude-sonnet-4.5')) as never); + + expect(response.status).toBe(200); + const getRoutingProviderConfig = mockedGetProvider.mock.calls[0]?.[0].getRoutingProviderConfig; + expect(getRoutingProviderConfig).toBeDefined(); + expect(await getRoutingProviderConfig?.()).toEqual({ only: ['amazon-bedrock'] }); + }); + it('returns 404 when the OpenRouter model id is unknown', async () => { mockedIsValidOpenRouterModelId.mockResolvedValue(false); diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 200cd4c822..3fefd22699 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -602,6 +602,46 @@ export async function POST(request: NextRequest): Promise groupDecision.eligibleProviderRoutes?.has(provider)) + : [...groupDecision.eligibleProviderRoutes]; + effectiveProviderConfig = { ...providerConfig, only }; + } + } + return { + balance, + effectiveProviderConfig, + groupModelAllowed, + modelRestrictionError, + plan, + settings, + }; + } + + const accessChecks = new Map>(); + function getAccessCheck(modelId: string) { + const existing = accessChecks.get(modelId); + if (existing) return existing; + const accessCheck = resolveAccessCheck(modelId); + accessChecks.set(modelId, accessCheck); + return accessCheck; + } + // Resolve the initial provider before abuse enforcement because abuse needs // provider/BYOK context, and quarantine-3 may later rewrite these values. const initialProviderResultForAbuseService = await getProvider({ @@ -612,6 +652,9 @@ export async function POST(request: NextRequest): Promise (await getAccessCheck(effectiveModelIdLowerCased)).effectiveProviderConfig, }); if (initialProviderResultForAbuseService.kind === 'not-found') { // Paused experiment for this public id — return a local model-unavailable @@ -735,6 +778,9 @@ export async function POST(request: NextRequest): Promise (await getAccessCheck(effectiveModelIdLowerCased)).effectiveProviderConfig, }); if (quarantineProviderResult.kind === 'not-found') { if (rulesEngineDecision.delayMs > 0) { @@ -780,7 +826,14 @@ export async function POST(request: NextRequest): Promise groupDecision.eligibleProviderRoutes?.has(provider)) - : [...groupDecision.eligibleProviderRoutes]; - if (only.length === 0) return modelNotAllowedResponse(); - effectiveProviderConfig = { ...providerConfig, only }; - } + if (!groupModelAllowed || effectiveProviderConfig?.only?.length === 0) { + return isAutoEfficientRequest ? efficientPoolBlockedResponse() : modelNotAllowedResponse(); } // Experiment traffic captures prompts to R2 for partner evaluation, which diff --git a/apps/web/src/lib/ai-gateway/providers/get-provider.ts b/apps/web/src/lib/ai-gateway/providers/get-provider.ts index aeb9184d81..339f00a00c 100644 --- a/apps/web/src/lib/ai-gateway/providers/get-provider.ts +++ b/apps/web/src/lib/ai-gateway/providers/get-provider.ts @@ -1,4 +1,7 @@ -import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types'; +import type { + GatewayRequest, + OpenRouterProviderConfig, +} from '@/lib/ai-gateway/providers/openrouter/types'; import { shouldRouteToVercel } from '@/lib/ai-gateway/providers/vercel'; import { findKiloExclusiveModel, isKiloExclusiveModel } from '@/lib/ai-gateway/models'; import { CUSTOM_LLM_PREFIX } from '@/lib/ai-gateway/model-utils'; @@ -195,10 +198,22 @@ export type GetProviderInput = { /** Machine identifier from `x-kilocode-machineid`. Used as the machine- * cohort allocation subject for experiment routing. */ machineId: string | null; + /** Resolves organization/group provider policy only when selecting a managed + * gateway. Direct BYOK and custom LLM routes remain exempt. */ + getRoutingProviderConfig?: () => Promise; }; export async function getProvider(input: GetProviderInput): Promise { - const { requestedModel, request, user, organizationId, taskId, clientIp, machineId } = input; + const { + requestedModel, + request, + user, + organizationId, + taskId, + clientIp, + machineId, + getRoutingProviderConfig, + } = input; const directByokByok = await checkDirectBYOK(user, requestedModel, organizationId); if (directByokByok) { @@ -270,7 +285,12 @@ export async function getProvider(input: GetProviderInput): Promise Date: Thu, 20 Aug 2026 16:45:15 +0000 Subject: [PATCH 2/8] test(ai-gateway): allow additional provider policy fields Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/api/openrouter/[...path]/route.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9b4e00c5b9..7357918f4e 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -417,7 +417,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(response.status).toBe(200); const getRoutingProviderConfig = mockedGetProvider.mock.calls[0]?.[0].getRoutingProviderConfig; expect(getRoutingProviderConfig).toBeDefined(); - expect(await getRoutingProviderConfig?.()).toEqual({ only: ['amazon-bedrock'] }); + expect(await getRoutingProviderConfig?.()).toMatchObject({ only: ['amazon-bedrock'] }); }); it('returns 404 when the OpenRouter model id is unknown', async () => { From 526619313e5bc6ef905a0460965059a727281bec Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:54:02 +0000 Subject: [PATCH 3/8] fix(ai-gateway): defer provider policy routing lookup Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../api/openrouter/[...path]/route.test.ts | 2 +- .../src/app/api/openrouter/[...path]/route.ts | 21 ++++-- .../lib/ai-gateway/providers/get-provider.ts | 4 +- .../ai-gateway/providers/vercel/index.test.ts | 74 +++++++++++++++++++ .../lib/ai-gateway/providers/vercel/index.ts | 4 +- 5 files changed, 94 insertions(+), 11 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 7357918f4e..769ea42997 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -417,7 +417,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect(response.status).toBe(200); const getRoutingProviderConfig = mockedGetProvider.mock.calls[0]?.[0].getRoutingProviderConfig; expect(getRoutingProviderConfig).toBeDefined(); - expect(await getRoutingProviderConfig?.()).toMatchObject({ only: ['amazon-bedrock'] }); + expect((await getRoutingProviderConfig?.())?.only).toEqual(['amazon-bedrock']); }); it('returns 404 when the OpenRouter model id is unknown', async () => { diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 3fefd22699..1e3fe71ac0 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -611,6 +611,7 @@ export async function POST(request: NextRequest): Promise groupDecision.eligibleProviderRoutes?.has(provider)) : [...groupDecision.eligibleProviderRoutes]; + groupProvidersAllowed = only.length > 0; effectiveProviderConfig = { ...providerConfig, only }; } } @@ -627,6 +629,7 @@ export async function POST(request: NextRequest): Promise (await getAccessCheck(modelId)).effectiveProviderConfig; + } + // Resolve the initial provider before abuse enforcement because abuse needs // provider/BYOK context, and quarantine-3 may later rewrite these values. const initialProviderResultForAbuseService = await getProvider({ @@ -652,9 +661,7 @@ export async function POST(request: NextRequest): Promise (await getAccessCheck(effectiveModelIdLowerCased)).effectiveProviderConfig, + getRoutingProviderConfig: getRoutingProviderConfigFor(effectiveModelIdLowerCased), }); if (initialProviderResultForAbuseService.kind === 'not-found') { // Paused experiment for this public id — return a local model-unavailable @@ -778,9 +785,7 @@ export async function POST(request: NextRequest): Promise (await getAccessCheck(effectiveModelIdLowerCased)).effectiveProviderConfig, + getRoutingProviderConfig: getRoutingProviderConfigFor(effectiveModelIdLowerCased), }); if (quarantineProviderResult.kind === 'not-found') { if (rulesEngineDecision.delayMs > 0) { @@ -830,6 +835,7 @@ export async function POST(request: NextRequest): Promise + (await getRoutingProviderConfig?.()) ?? request.body.provider; if ( eligibleForVercelRouting && @@ -289,7 +291,7 @@ export async function getProvider(input: GetProviderInput): Promise { }); }); +describe('shouldRouteToVercel', () => { + function request(provider?: GatewayRequest['body']['provider']): GatewayRequest { + return { + kind: 'chat_completions', + body: { + model: 'anthropic/claude-sonnet-4.5', + messages: [{ role: 'user', content: 'hello' }], + provider, + }, + }; + } + + async function loadShouldRouteToVercel(options?: { optOut?: boolean }) { + jest.resetModules(); + jest.doMock('@/lib/ai-gateway/providers/routing-config', () => ({ + getRuntimeGatewayRoutingConfig: jest.fn().mockResolvedValue({ + vercelPaid: 100, + vercelFree: 100, + vercelOptOutModels: new Set(options?.optOut ? ['anthropic/claude-sonnet-4.5'] : []), + friendli: 0, + perplexity: 0, + }), + })); + jest.doMock('@/lib/ai-gateway/is-free-model', () => ({ + isFreeModel: jest.fn().mockResolvedValue(false), + })); + jest.doMock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({ + getVercelModelsFromRedis: jest + .fn() + .mockResolvedValue(new Set(['anthropic/claude-sonnet-4.5'])), + getCachedVercelInferenceProviderIdsForModel: jest.fn().mockResolvedValue(['anthropic']), + })); + return (await import('@/lib/ai-gateway/providers/vercel')).shouldRouteToVercel; + } + + it('uses resolved provider policy instead of unrestricted request preferences', async () => { + const shouldRouteToVercel = await loadShouldRouteToVercel(); + + await expect( + shouldRouteToVercel('anthropic/claude-sonnet-4.5', request(), 'seed', async () => ({ + only: ['google-vertex'], + })) + ).resolves.toBe(false); + }); + + it('falls back to request preferences when no policy config is resolved', async () => { + const shouldRouteToVercel = await loadShouldRouteToVercel(); + + await expect( + shouldRouteToVercel( + 'anthropic/claude-sonnet-4.5', + request({ only: ['anthropic'] }), + 'seed', + async () => undefined + ) + ).resolves.toBe(true); + }); + + it('does not resolve provider policy for models opted out of Vercel routing', async () => { + const shouldRouteToVercel = await loadShouldRouteToVercel({ optOut: true }); + const getRoutingProviderConfig = jest.fn(async () => ({ only: ['anthropic'] })); + + await expect( + shouldRouteToVercel( + 'anthropic/claude-sonnet-4.5', + request(), + 'seed', + getRoutingProviderConfig + ) + ).resolves.toBe(false); + expect(getRoutingProviderConfig).not.toHaveBeenCalled(); + }); +}); + describe('applyVercelSettings BYOK pinning', () => { function byokRequest(ignore: string[]): GatewayRequest { return { diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts index 3ba60605bd..cb9983d746 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts @@ -64,7 +64,7 @@ export async function shouldRouteToVercel( requestedModel: string, request: GatewayRequest, randomSeed: string, - routingProviderConfig = request.body.provider + getRoutingProviderConfig: () => Promise ) { const routingConfig = await getRuntimeGatewayRoutingConfig(); if (isVercelRoutingOptOut(requestedModel, routingConfig.vercelOptOutModels)) { @@ -90,7 +90,7 @@ export async function shouldRouteToVercel( return false; } - const provider: OpenRouterProviderConfig | undefined = routingProviderConfig; + const provider = await getRoutingProviderConfig(); if (provider && (provider.only || provider.ignore?.length)) { const { only, ignore } = provider; const vercelInferenceProviders = From 1b79795e33753150fe01b1d607b718027ddb31f2 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:59:18 +0000 Subject: [PATCH 4/8] test(ai-gateway): type Vercel routing mocks Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../lib/ai-gateway/providers/vercel/index.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts index 22e02d03f2..3d3fe50c1f 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts @@ -162,22 +162,20 @@ describe('shouldRouteToVercel', () => { async function loadShouldRouteToVercel(options?: { optOut?: boolean }) { jest.resetModules(); jest.doMock('@/lib/ai-gateway/providers/routing-config', () => ({ - getRuntimeGatewayRoutingConfig: jest.fn().mockResolvedValue({ + getRuntimeGatewayRoutingConfig: jest.fn(async () => ({ vercelPaid: 100, vercelFree: 100, vercelOptOutModels: new Set(options?.optOut ? ['anthropic/claude-sonnet-4.5'] : []), friendli: 0, perplexity: 0, - }), + })), })); jest.doMock('@/lib/ai-gateway/is-free-model', () => ({ - isFreeModel: jest.fn().mockResolvedValue(false), + isFreeModel: jest.fn(async () => false), })); jest.doMock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({ - getVercelModelsFromRedis: jest - .fn() - .mockResolvedValue(new Set(['anthropic/claude-sonnet-4.5'])), - getCachedVercelInferenceProviderIdsForModel: jest.fn().mockResolvedValue(['anthropic']), + getVercelModelsFromRedis: jest.fn(async () => new Set(['anthropic/claude-sonnet-4.5'])), + getCachedVercelInferenceProviderIdsForModel: jest.fn(async () => ['anthropic']), })); return (await import('@/lib/ai-gateway/providers/vercel')).shouldRouteToVercel; } From 5fa207fa3b96357b6e2371d3866045b57dadb4e1 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:11:10 +0000 Subject: [PATCH 5/8] perf(ai-gateway): skip group policy for denied models Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/app/api/openrouter/[...path]/route.test.ts | 14 ++++++++++++++ apps/web/src/app/api/openrouter/[...path]/route.ts | 11 +++++++++++ 2 files changed, 25 insertions(+) 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 769ea42997..17b92324bd 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -420,6 +420,20 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => expect((await getRoutingProviderConfig?.())?.only).toEqual(['amazon-bedrock']); }); + it('skips group policy evaluation when organization policy denies the model', async () => { + mockedGetBalanceAndOrgSettings.mockResolvedValue({ + balance: 1000, + settings: { model_deny_list: ['openai/gpt-4o'] }, + plan: 'enterprise', + }); + + const { POST } = await import('./route'); + const response = await POST(makeRequest(makeBody()) as never); + + expect(response.status).toBe(403); + expect(mockedGetEffectiveModelDecision).not.toHaveBeenCalled(); + }); + it('returns 404 when the OpenRouter model id is unknown', async () => { mockedIsValidOpenRouterModelId.mockResolvedValue(false); diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 1e3fe71ac0..a034e8fa84 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -609,6 +609,17 @@ export async function POST(request: NextRequest): Promise Date: Thu, 20 Aug 2026 20:22:09 +0000 Subject: [PATCH 6/8] refactor(ai-gateway): use single access check resolver Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/app/api/openrouter/[...path]/route.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index a034e8fa84..611de0af8b 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -647,20 +647,18 @@ export async function POST(request: NextRequest): Promise>(); - function getAccessCheck(modelId: string) { - const existing = accessChecks.get(modelId); - if (existing) return existing; - const accessCheck = resolveAccessCheck(modelId); - accessChecks.set(modelId, accessCheck); - return accessCheck; + function createAccessCheckResolver(modelId: string) { + let accessCheck: ReturnType | undefined; + const get = () => (accessCheck ??= resolveAccessCheck(modelId)); + return { + get, + getRoutingProviderConfig: isAnonymousContext(user) + ? undefined + : async () => (await get()).effectiveProviderConfig, + }; } - function getRoutingProviderConfigFor(modelId: string) { - return isAnonymousContext(user) - ? undefined - : async () => (await getAccessCheck(modelId)).effectiveProviderConfig; - } + let accessCheckResolver = createAccessCheckResolver(effectiveModelIdLowerCased); // Resolve the initial provider before abuse enforcement because abuse needs // provider/BYOK context, and quarantine-3 may later rewrite these values. @@ -672,7 +670,7 @@ export async function POST(request: NextRequest): Promise 0) { @@ -850,7 +849,7 @@ export async function POST(request: NextRequest): Promise Date: Thu, 20 Aug 2026 20:53:54 +0000 Subject: [PATCH 7/8] test(ai-gateway): expect hidden denied model response Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/api/openrouter/[...path]/route.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 17b92324bd..cd582c11ab 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -316,7 +316,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => const { POST } = await import('./route'); const response = await POST(makeRequest(makeBody()) as never); - expect(response.status).toBe(403); + expect(response.status).toBe(404); expect(await response.json()).toMatchObject({ error_type: 'abuse_blocked', message: 'Request blocked by abuse prevention rules.', From dd588800e7f81557ee3ef107674cde5b58f96639 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:23:11 +0000 Subject: [PATCH 8/8] test(ai-gateway): cover denied organization policy Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/app/api/openrouter/[...path]/route.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 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 cd582c11ab..a7b2cfb8b9 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -316,7 +316,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => const { POST } = await import('./route'); const response = await POST(makeRequest(makeBody()) as never); - expect(response.status).toBe(404); + expect(response.status).toBe(403); expect(await response.json()).toMatchObject({ error_type: 'abuse_blocked', message: 'Request blocked by abuse prevention rules.', @@ -421,6 +421,15 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => }); it('skips group policy evaluation when organization policy denies the model', async () => { + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: 'user-123', + google_user_email: 'test@example.com', + microdollars_used: 0, + } as User, + authFailedResponse: null, + organizationId: 'org-1', + }); mockedGetBalanceAndOrgSettings.mockResolvedValue({ balance: 1000, settings: { model_deny_list: ['openai/gpt-4o'] }, @@ -430,7 +439,7 @@ describe('POST /api/openrouter/v1/chat/completions rules-engine actions', () => const { POST } = await import('./route'); const response = await POST(makeRequest(makeBody()) as never); - expect(response.status).toBe(403); + expect(response.status).toBe(404); expect(mockedGetEffectiveModelDecision).not.toHaveBeenCalled(); });