From 1680230eeaeb18c6d7fa1066dcf6d4743a95b420 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 4 May 2026 20:47:48 +0200 Subject: [PATCH 1/3] Upstream provider restriction for Kilo exclusive models --- .../ai-gateway/providers/anthropic.constants.ts | 1 + .../providers/apply-provider-specific-logic.ts | 14 ++++++++------ apps/web/src/lib/ai-gateway/providers/google.ts | 1 + .../ai-gateway/providers/kilo-exclusive-model.ts | 1 + apps/web/src/lib/ai-gateway/providers/minimax.ts | 1 + apps/web/src/lib/ai-gateway/providers/morph.ts | 1 + apps/web/src/lib/ai-gateway/providers/qwen.ts | 4 ++++ apps/web/src/lib/ai-gateway/providers/seed.ts | 1 + apps/web/src/lib/ai-gateway/providers/stepfun.ts | 1 + apps/web/src/lib/ai-gateway/providers/xai.ts | 1 + 10 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts index 5a591b871e..8272fafb07 100644 --- a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts +++ b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts @@ -20,6 +20,7 @@ export const claude_sonnet_clawsetup_model: KiloExclusiveModel = { flags: ['reasoning', 'vision'], pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; export function isClaudeModel(requestedModel: string) { diff --git a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts index e347a80385..bd9c447e40 100644 --- a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts +++ b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts @@ -7,7 +7,6 @@ import type { import { applyMistralModelSettings, isMistralModel } from '@/lib/ai-gateway/providers/mistral'; import { applyXaiModelSettings, isGrokModel } from '@/lib/ai-gateway/providers/xai'; import { kiloExclusiveModels } from '@/lib/ai-gateway/models'; -import { getInferenceProvider } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import { applyAnthropicModelSettings } from '@/lib/ai-gateway/providers/anthropic'; import { isClaudeModel, isHaikuModel } from '@/lib/ai-gateway/providers/anthropic.constants'; import { OpenRouterInferenceProviderIdSchema } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; @@ -113,12 +112,15 @@ export function applyProviderSpecificLogic( const kiloExclusiveModel = kiloExclusiveModels.find(m => m.public_id === requestedModel); if (kiloExclusiveModel) { requestToMutate.body.model = kiloExclusiveModel.internal_id; - const inferenceProvider = getInferenceProvider(kiloExclusiveModel); - if (inferenceProvider) { - if (requestToMutate.body.provider) { - requestToMutate.body.provider.only = [inferenceProvider]; + const restriction = kiloExclusiveModel.inference_provider_restriction; + if (restriction.length > 0) { + const provider = requestToMutate.body.provider; + if (provider?.only) { + provider.only = [...new Set(provider.only).intersection(new Set(restriction))]; + } else if (provider) { + provider.only = [...restriction]; } else { - requestToMutate.body.provider = { only: [inferenceProvider] }; + requestToMutate.body.provider = { only: [...restriction] }; } } } diff --git a/apps/web/src/lib/ai-gateway/providers/google.ts b/apps/web/src/lib/ai-gateway/providers/google.ts index 753fefda1f..5ce5bd8b22 100644 --- a/apps/web/src/lib/ai-gateway/providers/google.ts +++ b/apps/web/src/lib/ai-gateway/providers/google.ts @@ -25,6 +25,7 @@ export const gemma_4_26b_a4b_it_free_model: KiloExclusiveModel = { internal_id: 'google/gemma-4-26b-a4b-it', pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; export function isGemini3Model(model: string) { diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts index 87f1244f5c..6066bd70f1 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts @@ -35,6 +35,7 @@ export type KiloExclusiveModel = { pricing: Pricing | null; /** Features allowed to use this model. Empty array means no restriction. */ exclusive_to: ReadonlyArray; + inference_provider_restriction: ReadonlyArray; }; export function getInferenceProvider( diff --git a/apps/web/src/lib/ai-gateway/providers/minimax.ts b/apps/web/src/lib/ai-gateway/providers/minimax.ts index 47d541f8c4..645dbf3632 100644 --- a/apps/web/src/lib/ai-gateway/providers/minimax.ts +++ b/apps/web/src/lib/ai-gateway/providers/minimax.ts @@ -13,6 +13,7 @@ export const minimax_m25_free_model: KiloExclusiveModel = { internal_id: 'minimax/minimax-m2.5', pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; export function isMinimaxModel(model: string) { diff --git a/apps/web/src/lib/ai-gateway/providers/morph.ts b/apps/web/src/lib/ai-gateway/providers/morph.ts index 702d5bcf85..95ebb2caee 100644 --- a/apps/web/src/lib/ai-gateway/providers/morph.ts +++ b/apps/web/src/lib/ai-gateway/providers/morph.ts @@ -13,4 +13,5 @@ export const morph_warp_grep_free_model: KiloExclusiveModel = { internal_id: 'morph-warp-grep-v2', pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/qwen.ts b/apps/web/src/lib/ai-gateway/providers/qwen.ts index 795c6bda55..8ef16937fb 100644 --- a/apps/web/src/lib/ai-gateway/providers/qwen.ts +++ b/apps/web/src/lib/ai-gateway/providers/qwen.ts @@ -102,6 +102,7 @@ export const qwen36_plus_model: KiloExclusiveModel = { }, ]), exclusive_to: [], + inference_provider_restriction: [], }; export const qwen36_flash_model: KiloExclusiveModel = { @@ -136,6 +137,7 @@ export const qwen36_flash_model: KiloExclusiveModel = { }, ]), exclusive_to: [], + inference_provider_restriction: [], }; export const qwen36_max_preview_model: KiloExclusiveModel = { @@ -170,6 +172,7 @@ export const qwen36_max_preview_model: KiloExclusiveModel = { }, ]), exclusive_to: [], + inference_provider_restriction: [], }; export const qwen36_27b_model: KiloExclusiveModel = { @@ -190,6 +193,7 @@ export const qwen36_27b_model: KiloExclusiveModel = { input_cache_write_per_million: null, }), exclusive_to: [], + inference_provider_restriction: [], }; export const alibabaDirectModels: ReadonlyArray = [ diff --git a/apps/web/src/lib/ai-gateway/providers/seed.ts b/apps/web/src/lib/ai-gateway/providers/seed.ts index a636d0eeb9..6084f98e62 100644 --- a/apps/web/src/lib/ai-gateway/providers/seed.ts +++ b/apps/web/src/lib/ai-gateway/providers/seed.ts @@ -13,4 +13,5 @@ export const seed_20_code_free_model: KiloExclusiveModel = { internal_id: 'seed-2-0-code-preview-260328', pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/stepfun.ts b/apps/web/src/lib/ai-gateway/providers/stepfun.ts index 7311d95d1b..c35936dcdf 100644 --- a/apps/web/src/lib/ai-gateway/providers/stepfun.ts +++ b/apps/web/src/lib/ai-gateway/providers/stepfun.ts @@ -17,4 +17,5 @@ export const stepfun_35_flash_free_model: KiloExclusiveModel = { internal_id: 'stepfun/step-3.5-flash', pricing: null, exclusive_to: [], + inference_provider_restriction: ['stepfun'], }; diff --git a/apps/web/src/lib/ai-gateway/providers/xai.ts b/apps/web/src/lib/ai-gateway/providers/xai.ts index 64f5fd5ff5..9c966ee41f 100644 --- a/apps/web/src/lib/ai-gateway/providers/xai.ts +++ b/apps/web/src/lib/ai-gateway/providers/xai.ts @@ -14,6 +14,7 @@ export const grok_code_fast_1_optimized_free_model: KiloExclusiveModel = { internal_id: 'x-ai/grok-code-fast-1:optimized', pricing: null, exclusive_to: [], + inference_provider_restriction: [], }; export function isGrokModel(requestedModel: string) { From 98558f60626e4276bd8ad890e88fd572138ccbde Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 19:00:34 +0000 Subject: [PATCH 2/3] refactor(ai-gateway): extract applyKiloExclusiveModelSettings Pull the inner block of applyProviderSpecificLogic that rewrites the model id and narrows the OpenRouter provider restriction into a standalone function, and cover it with unit tests. --- .../apply-provider-specific-logic.ts | 14 +-- .../providers/kilo-exclusive-model.test.ts | 118 ++++++++++++++++++ .../providers/kilo-exclusive-model.ts | 26 ++++ 3 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts diff --git a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts index bd9c447e40..883c38e917 100644 --- a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts +++ b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts @@ -7,6 +7,7 @@ import type { import { applyMistralModelSettings, isMistralModel } from '@/lib/ai-gateway/providers/mistral'; import { applyXaiModelSettings, isGrokModel } from '@/lib/ai-gateway/providers/xai'; import { kiloExclusiveModels } from '@/lib/ai-gateway/models'; +import { applyKiloExclusiveModelSettings } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import { applyAnthropicModelSettings } from '@/lib/ai-gateway/providers/anthropic'; import { isClaudeModel, isHaikuModel } from '@/lib/ai-gateway/providers/anthropic.constants'; import { OpenRouterInferenceProviderIdSchema } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; @@ -111,18 +112,7 @@ export function applyProviderSpecificLogic( ) { const kiloExclusiveModel = kiloExclusiveModels.find(m => m.public_id === requestedModel); if (kiloExclusiveModel) { - requestToMutate.body.model = kiloExclusiveModel.internal_id; - const restriction = kiloExclusiveModel.inference_provider_restriction; - if (restriction.length > 0) { - const provider = requestToMutate.body.provider; - if (provider?.only) { - provider.only = [...new Set(provider.only).intersection(new Set(restriction))]; - } else if (provider) { - provider.only = [...restriction]; - } else { - requestToMutate.body.provider = { only: [...restriction] }; - } - } + applyKiloExclusiveModelSettings(requestToMutate, kiloExclusiveModel); } if (isClaudeModel(requestedModel)) { diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts new file mode 100644 index 0000000000..4e1098aee7 --- /dev/null +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from '@jest/globals'; +import { + applyKiloExclusiveModelSettings, + type KiloExclusiveModel, +} from '@/lib/ai-gateway/providers/kilo-exclusive-model'; +import type { + GatewayRequest, + OpenRouterChatCompletionRequest, + OpenRouterProviderConfig, +} from '@/lib/ai-gateway/providers/openrouter/types'; +import type { OpenRouterInferenceProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; + +function makeModel( + overrides: Partial & Pick +): KiloExclusiveModel { + return { + public_id: 'kilo/test-model', + display_name: 'Test', + description: '', + context_length: 0, + max_completion_tokens: 0, + status: 'public', + flags: [], + gateway: 'openrouter', + pricing: null, + exclusive_to: [], + inference_provider_restriction: [], + ...overrides, + }; +} + +function makeRequest( + provider?: OpenRouterProviderConfig, + model = 'public/id' +): GatewayRequest & { kind: 'chat_completions' } { + const body: OpenRouterChatCompletionRequest = { + model, + messages: [], + ...(provider ? { provider } : {}), + } as OpenRouterChatCompletionRequest; + return { kind: 'chat_completions', body }; +} + +describe('applyKiloExclusiveModelSettings', () => { + it('rewrites the public model id to the internal id', () => { + const req = makeRequest(undefined, 'kilo/test-model'); + applyKiloExclusiveModelSettings(req, makeModel({ internal_id: 'vendor/real-model' })); + expect(req.body.model).toBe('vendor/real-model'); + }); + + it('leaves provider untouched when there is no restriction', () => { + const req = makeRequest({ only: ['anthropic'], zdr: true }); + applyKiloExclusiveModelSettings(req, makeModel({ internal_id: 'vendor/x' })); + expect(req.body.provider).toEqual({ only: ['anthropic'], zdr: true }); + }); + + it('creates provider.only when no provider block is present', () => { + const req = makeRequest(undefined); + applyKiloExclusiveModelSettings( + req, + makeModel({ + internal_id: 'vendor/x', + inference_provider_restriction: [ + 'anthropic', + 'amazon-bedrock', + ] as OpenRouterInferenceProviderId[], + }) + ); + expect(req.body.provider).toEqual({ only: ['anthropic', 'amazon-bedrock'] }); + }); + + it('adds only to an existing provider block that has no only set', () => { + const req = makeRequest({ zdr: true }); + applyKiloExclusiveModelSettings( + req, + makeModel({ + internal_id: 'vendor/x', + inference_provider_restriction: ['anthropic'] as OpenRouterInferenceProviderId[], + }) + ); + expect(req.body.provider).toEqual({ zdr: true, only: ['anthropic'] }); + }); + + it('intersects caller-supplied only with the restriction', () => { + const req = makeRequest({ only: ['anthropic', 'openai', 'amazon-bedrock'] }); + applyKiloExclusiveModelSettings( + req, + makeModel({ + internal_id: 'vendor/x', + inference_provider_restriction: [ + 'anthropic', + 'amazon-bedrock', + ] as OpenRouterInferenceProviderId[], + }) + ); + expect(req.body.provider?.only?.sort()).toEqual(['amazon-bedrock', 'anthropic']); + }); + + it('produces an empty only list when caller only and restriction are disjoint', () => { + const req = makeRequest({ only: ['openai'] }); + applyKiloExclusiveModelSettings( + req, + makeModel({ + internal_id: 'vendor/x', + inference_provider_restriction: ['anthropic'] as OpenRouterInferenceProviderId[], + }) + ); + expect(req.body.provider?.only).toEqual([]); + }); + + it('does not clone shared configuration when there is no restriction', () => { + const sharedProvider: OpenRouterProviderConfig = { only: ['openai'] }; + const req = makeRequest(sharedProvider); + applyKiloExclusiveModelSettings(req, makeModel({ internal_id: 'vendor/x' })); + expect(req.body.provider).toBe(sharedProvider); + expect(sharedProvider.only).toEqual(['openai']); + }); +}); diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts index 6066bd70f1..84b75fc8ca 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts @@ -4,6 +4,7 @@ import { type OpenRouterInferenceProviderId, } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import type { ProviderId } from '@/lib/ai-gateway/providers/types'; +import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types'; export type KiloExclusiveModelFlag = 'reasoning' | 'vision' | 'stealth' | 'vercel-routing'; @@ -38,6 +39,31 @@ export type KiloExclusiveModel = { inference_provider_restriction: ReadonlyArray; }; +/** + * Rewrites a gateway request to target a Kilo-exclusive model: swaps the + * public model id for its internal id, and narrows the OpenRouter provider + * `only` list to the model's `inference_provider_restriction` (intersecting + * with any caller-supplied `only`). + */ +export function applyKiloExclusiveModelSettings( + requestToMutate: GatewayRequest, + kiloExclusiveModel: KiloExclusiveModel +) { + requestToMutate.body.model = kiloExclusiveModel.internal_id; + const restriction = kiloExclusiveModel.inference_provider_restriction; + if (restriction.length === 0) { + return; + } + const provider = requestToMutate.body.provider; + if (provider?.only) { + provider.only = [...new Set(provider.only).intersection(new Set(restriction))]; + } else if (provider) { + provider.only = [...restriction]; + } else { + requestToMutate.body.provider = { only: [...restriction] }; + } +} + export function getInferenceProvider( model: KiloExclusiveModel ): OpenRouterInferenceProviderId | null { From 256f19cf62bfa40629d70304e5a3a15bd6373c70 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 19:12:13 +0000 Subject: [PATCH 3/3] docs(ai-gateway): clarify inference_provider_restriction and simplify comment --- .../lib/ai-gateway/providers/kilo-exclusive-model.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts index 84b75fc8ca..288f65c1ff 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts @@ -36,15 +36,14 @@ export type KiloExclusiveModel = { pricing: Pricing | null; /** Features allowed to use this model. Empty array means no restriction. */ exclusive_to: ReadonlyArray; + /** + * Upstream inference providers this model may be routed to; empty means no + * restriction. Only honored by the OpenRouter and Vercel AI Gateway upstreams. + */ inference_provider_restriction: ReadonlyArray; }; -/** - * Rewrites a gateway request to target a Kilo-exclusive model: swaps the - * public model id for its internal id, and narrows the OpenRouter provider - * `only` list to the model's `inference_provider_restriction` (intersecting - * with any caller-supplied `only`). - */ +/** Rewrites a gateway request to target a Kilo-exclusive model. */ export function applyKiloExclusiveModelSettings( requestToMutate: GatewayRequest, kiloExclusiveModel: KiloExclusiveModel