From 10ce4ced0dc4475e13d528c96f30e46853e192c0 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:06:13 +0000 Subject: [PATCH 1/5] refactor: remove unused exclusive model restriction Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../benchmark-config/route.test.ts | 1 - .../src/app/api/openrouter/[...path]/route.ts | 14 +----- .../app/api/openrouter/models/route.test.ts | 50 ++----------------- .../src/app/api/openrouter/models/route.ts | 14 ++---- .../api/openrouter/models/validate/route.ts | 10 ++-- .../organizations/[id]/models/route.test.ts | 34 ------------- .../api/organizations/[id]/models/route.ts | 6 +-- .../[id]/models/validate/route.ts | 8 +-- .../src/lib/ai-gateway/llm-proxy-helpers.ts | 11 +--- .../lib/ai-gateway/model-api-kinds.test.ts | 1 - apps/web/src/lib/ai-gateway/models.ts | 22 -------- .../providers/anthropic.constants.ts | 5 -- .../src/lib/ai-gateway/providers/deepseek.ts | 2 - .../src/lib/ai-gateway/providers/google.ts | 1 - .../providers/kilo-exclusive-model.test.ts | 1 - .../providers/kilo-exclusive-model.ts | 3 -- apps/web/src/lib/ai-gateway/providers/meta.ts | 1 - .../ai-gateway/providers/openai-exclusive.ts | 1 - apps/web/src/lib/ai-gateway/providers/qwen.ts | 1 - apps/web/src/lib/ai-gateway/providers/seed.ts | 1 - .../src/lib/ai-gateway/providers/stepfun.ts | 1 - .../lib/ai-gateway/providers/streamlake.ts | 1 - apps/web/src/lib/proxy-error-types.ts | 1 - 23 files changed, 15 insertions(+), 175 deletions(-) diff --git a/apps/web/src/app/admin/api/auto-routing/benchmark-config/route.test.ts b/apps/web/src/app/admin/api/auto-routing/benchmark-config/route.test.ts index 072b813ccf..bca6a107c7 100644 --- a/apps/web/src/app/admin/api/auto-routing/benchmark-config/route.test.ts +++ b/apps/web/src/app/admin/api/auto-routing/benchmark-config/route.test.ts @@ -37,7 +37,6 @@ jest.mock('@/lib/ai-gateway/models', () => { gateway: 'alibaba', internal_id: 'stub-internal', pricing: null, - exclusive_to: [], inference_provider_restriction: [], }; return { diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 879500a751..d3a38b9c3a 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -25,11 +25,7 @@ import { debugSaveProxyRequest } from '@/lib/debugUtils'; import { setTag, startInactiveSpan } from '@sentry/nextjs'; import { getUserFromAuth } from '@/lib/user/server'; import { sentryRootSpan } from '@/lib/getRootSpan'; -import { - isDeadFreeModel, - isExcludedForFeature, - isKiloExclusiveFreeModel, -} from '@/lib/ai-gateway/models'; +import { isDeadFreeModel, isKiloExclusiveFreeModel } from '@/lib/ai-gateway/models'; import { hasBestEffortGuessDataCollectionRequirement, isFreeModel, @@ -40,7 +36,6 @@ import { checkOrganizationModelRestrictions, dataCollectionRequiredResponse, extractFraudAndProjectHeaders, - featureExclusiveModelResponse, invalidPathResponse, invalidRequestResponse, malformedJsonResponse, @@ -344,13 +339,6 @@ export async function POST(request: NextRequest): Promise ({ addUserByokAvailability: jest.fn(), getUserByokProviderIds: jest.fn(), })); -jest.mock('@/lib/ai-gateway/models', () => ({ - ...jest.requireActual('@/lib/ai-gateway/models'), - filterByFeature: jest.fn(), -})); jest.mock('@/lib/organizations/organization-models', () => ({ getAvailableModelsForOrganization: jest.fn(), })); @@ -46,7 +42,6 @@ const { getAvailableModelsForOrganization } = jest.requireMock( ); const { getCachedRoutingTable } = jest.requireMock('@/lib/ai-gateway/auto-routing-table-cache'); const { getAutoFreeCandidates } = jest.requireMock('@/lib/ai-gateway/auto-model/resolution'); -const { filterByFeature } = jest.requireMock('@/lib/ai-gateway/models'); const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); const mockedGetEnhancedOpenRouterModels = jest.mocked(getEnhancedOpenRouterModels); @@ -57,7 +52,6 @@ const mockedGetUserByokProviderIds = jest.mocked(getUserByokProviderIds); const mockedGetAvailableModelsForOrganization = jest.mocked(getAvailableModelsForOrganization); const mockedGetCachedRoutingTable = jest.mocked(getCachedRoutingTable); const mockedGetAutoFreeCandidates = jest.mocked(getAutoFreeCandidates); -const mockedFilterByFeature = jest.mocked(filterByFeature); function makeModel(id: string): OpenRouterModel { return { @@ -83,10 +77,6 @@ function request(headers?: Record) { describe('GET /api/openrouter/models', () => { beforeEach(() => { jest.resetAllMocks(); - mockedFilterByFeature.mockImplementation( - (models: Array<{ id: string }>, feature: string | null) => - feature === 'code-review' ? models.filter(model => model.id !== 'feature/excluded') : models - ); mockedGetUserFromAuth.mockResolvedValue({ user: null, organizationId: null, @@ -188,57 +178,25 @@ describe('GET /api/openrouter/models', () => { }); }); - test('builds auto-routing choices from feature-filtered models', async () => { - const efficientModel = makeModel('kilo-auto/efficient'); - const allowedModel = makeModel('openai/gpt-5.4-mini'); - const excludedModel = makeModel('feature/excluded'); - mockedGetEnhancedOpenRouterModels.mockResolvedValue({ - data: [efficientModel, allowedModel, excludedModel], - }); - mockedGetCachedRoutingTable.mockResolvedValue({ - routes: { - 'implementation/code_generation': [{ model: allowedModel.id }, { model: excludedModel.id }], - }, - } as never); - - const response = await GET(request({ 'x-kilocode-feature': 'code-review' })); - - expect(response.status).toBe(200); - await expect(response.json()).resolves.toEqual({ - data: [ - { - ...efficientModel, - autoRouting: { - models: [allowedModel.id], - }, - }, - allowedModel, - ], - }); - }); - - test('adds auto-routing to organization models after feature filtering', async () => { + test('adds auto-routing to organization models', async () => { const efficientModel = makeModel('kilo-auto/efficient'); const allowedModel = makeModel('openai/gpt-5.4-mini'); - const excludedModel = makeModel('feature/excluded'); mockedGetUserFromAuth.mockResolvedValue({ user: { id: 'user-id' }, organizationId: 'org-1', authFailedResponse: null, } as never); mockedGetAvailableModelsForOrganization.mockResolvedValue({ - data: [efficientModel, allowedModel, excludedModel], + data: [efficientModel, allowedModel], } as never); mockedGetCachedRoutingTable.mockResolvedValue({ routes: { - 'implementation/code_generation': [{ model: allowedModel.id }, { model: excludedModel.id }], + 'implementation/code_generation': [{ model: allowedModel.id }], }, } as never); - const response = await GET(request({ 'x-kilocode-feature': 'code-review' })); + const response = await GET(request()); - // The feature-excluded model must appear in neither the top-level list nor the - // Auto Efficient choices — enrichment runs after filterByFeature. expect(response.status).toBe(200); await expect(response.json()).resolves.toEqual({ data: [ diff --git a/apps/web/src/app/api/openrouter/models/route.ts b/apps/web/src/app/api/openrouter/models/route.ts index c9fd71e439..a47b0330b3 100644 --- a/apps/web/src/app/api/openrouter/models/route.ts +++ b/apps/web/src/app/api/openrouter/models/route.ts @@ -6,8 +6,6 @@ import { getEnhancedOpenRouterModels } from '@/lib/ai-gateway/providers/openrout import { getUserFromAuth } from '@/lib/user/server'; import { getDirectByokModelsForUser } from '@/lib/ai-gateway/providers/direct-byok'; import { getAvailableModelsForOrganization } from '@/lib/organizations/organization-models'; -import { FEATURE_HEADER, validateFeatureHeader } from '@/lib/feature-detection'; -import { filterByFeature } from '@/lib/ai-gateway/models'; import { listAvailableExperimentModels } from '@/lib/ai-gateway/experiments/list-available-experiment-models'; import { addUserByokAvailability, getUserByokProviderIds } from '@/lib/ai-gateway/byok'; import { readDb } from '@/lib/drizzle'; @@ -29,7 +27,6 @@ async function tryGetUserFromAuth() { export async function GET( request: NextRequest ): Promise> { - const feature = validateFeatureHeader(request.headers.get(FEATURE_HEADER)); const auth = await tryGetUserFromAuth(); try { const result = auth?.organizationId @@ -38,7 +35,7 @@ export async function GET( if (result) { return NextResponse.json({ ...result, - data: await addAutoRoutingModels(filterByFeature(result.data, feature)), + data: await addAutoRoutingModels(result.data), }); } @@ -46,11 +43,11 @@ export async function GET( if (!Array.isArray(data.data)) { return NextResponse.json(data); } - const models = await addAutoRoutingModels(filterByFeature(data.data, feature)); + const models = await addAutoRoutingModels(data.data); if (!auth?.user) { const experimentModels = await listAvailableExperimentModels(); return NextResponse.json({ - data: models.concat(filterByFeature(experimentModels, feature)), + data: models.concat(experimentModels), }); } @@ -64,10 +61,7 @@ export async function GET( enabledByokProviderIds ); return NextResponse.json({ - data: modelsWithByokAvailability.concat( - filterByFeature(byokModels, feature), - filterByFeature(experimentModels, feature) - ), + data: modelsWithByokAvailability.concat(byokModels, experimentModels), }); } catch (error) { captureException(error, { diff --git a/apps/web/src/app/api/openrouter/models/validate/route.ts b/apps/web/src/app/api/openrouter/models/validate/route.ts index d8d3d5c808..6b23ec0276 100644 --- a/apps/web/src/app/api/openrouter/models/validate/route.ts +++ b/apps/web/src/app/api/openrouter/models/validate/route.ts @@ -5,9 +5,7 @@ import * as z from 'zod'; import { getEnhancedOpenRouterModels } from '@/lib/ai-gateway/providers/openrouter'; import { getUserFromAuth } from '@/lib/user/server'; import { getDirectByokModelsForUser } from '@/lib/ai-gateway/providers/direct-byok'; -import { FEATURE_HEADER, validateFeatureHeader } from '@/lib/feature-detection'; import { ORGANIZATION_ID_HEADER } from '@/lib/constants'; -import { filterByFeature } from '@/lib/ai-gateway/models'; import { listAvailableExperimentModels } from '@/lib/ai-gateway/experiments/list-available-experiment-models'; const BodySchema = z.object({ modelId: z.string().trim().min(1) }); @@ -44,7 +42,6 @@ export async function POST(request: NextRequest) { ); } - const feature = validateFeatureHeader(request.headers.get(FEATURE_HEADER)); const auth = await tryGetUserFromAuth(); try { const models = await getEnhancedOpenRouterModels(); @@ -55,10 +52,9 @@ export async function POST(request: NextRequest) { auth?.user ? getDirectByokModelsForUser(auth.user.id) : [], listAvailableExperimentModels(), ]); - const available = filterByFeature( - models.data.concat(byokModels, experimentModels), - feature - ).some(model => model.id === bodyResult.data.modelId); + const available = models.data + .concat(byokModels, experimentModels) + .some(model => model.id === bodyResult.data.modelId); return NextResponse.json(available ? { valid: true } : { valid: false, reason: 'unavailable' }); } catch (error) { captureException(error, { diff --git a/apps/web/src/app/api/organizations/[id]/models/route.test.ts b/apps/web/src/app/api/organizations/[id]/models/route.test.ts index 2324530655..96e0b8fdd0 100644 --- a/apps/web/src/app/api/organizations/[id]/models/route.test.ts +++ b/apps/web/src/app/api/organizations/[id]/models/route.test.ts @@ -11,18 +11,12 @@ jest.mock('@/lib/ai-gateway/auto-routing-table-cache', () => ({ jest.mock('@/lib/ai-gateway/auto-model/resolution', () => ({ getAutoFreeCandidates: jest.fn(), })); -jest.mock('@/lib/ai-gateway/models', () => ({ - ...jest.requireActual('@/lib/ai-gateway/models'), - filterByFeature: jest.fn(), -})); const mockedHandleTRPCRequest = jest.mocked(handleTRPCRequest); const { getCachedRoutingTable } = jest.requireMock('@/lib/ai-gateway/auto-routing-table-cache'); const { getAutoFreeCandidates } = jest.requireMock('@/lib/ai-gateway/auto-model/resolution'); -const { filterByFeature } = jest.requireMock('@/lib/ai-gateway/models'); const mockedGetCachedRoutingTable = jest.mocked(getCachedRoutingTable); const mockedGetAutoFreeCandidates = jest.mocked(getAutoFreeCandidates); -const mockedFilterByFeature = jest.mocked(filterByFeature); const listAvailableModels = jest.fn(); function makeModel(id: string): OpenRouterModel { @@ -50,10 +44,6 @@ function request(headers?: Record) { describe('GET /api/organizations/[id]/models', () => { beforeEach(() => { jest.resetAllMocks(); - mockedFilterByFeature.mockImplementation( - (models: Array<{ id: string }>, feature: string | null) => - feature === 'code-review' ? models.filter(model => model.id !== 'feature/excluded') : models - ); mockedGetCachedRoutingTable.mockResolvedValue(null); mockedGetAutoFreeCandidates.mockResolvedValue([]); mockedHandleTRPCRequest.mockImplementation(async (request, handler) => { @@ -90,30 +80,6 @@ describe('GET /api/organizations/[id]/models', () => { }); }); - test('builds auto-routing choices from the feature-filtered catalog', async () => { - const efficientModel = makeModel('kilo-auto/efficient'); - const allowedModel = makeModel('openai/gpt-5.4-mini'); - const excludedModel = makeModel('feature/excluded'); - listAvailableModels.mockResolvedValue({ - data: [efficientModel, allowedModel, excludedModel], - }); - mockedGetCachedRoutingTable.mockResolvedValue({ - routes: { - 'implementation/code_generation': [{ model: allowedModel.id }, { model: excludedModel.id }], - }, - } as never); - - const response = await GET(request({ 'x-kilocode-feature': 'code-review' }), { - params: Promise.resolve({ id: 'org-1' }), - }); - - // The feature-excluded model must appear in neither the top-level list nor - // the Auto Efficient choices — enrichment runs after filterByFeature. - await expect(response.json()).resolves.toEqual({ - data: [{ ...efficientModel, autoRouting: { models: [allowedModel.id] } }, allowedModel], - }); - }); - test('returns the catalog unchanged when auto models are absent', async () => { const model = makeModel('openai/gpt-5.4-mini'); listAvailableModels.mockResolvedValue({ data: [model] }); diff --git a/apps/web/src/app/api/organizations/[id]/models/route.ts b/apps/web/src/app/api/organizations/[id]/models/route.ts index eb00852863..2b90319e21 100644 --- a/apps/web/src/app/api/organizations/[id]/models/route.ts +++ b/apps/web/src/app/api/organizations/[id]/models/route.ts @@ -1,21 +1,17 @@ import type { NextRequest } from 'next/server'; import type { OpenRouterModelsResponse } from '@/lib/organizations/organization-types'; import { handleTRPCRequest } from '@/lib/trpc-route-handler'; -import { FEATURE_HEADER, validateFeatureHeader } from '@/lib/feature-detection'; -import { filterByFeature } from '@/lib/ai-gateway/models'; import { addAutoRoutingModels } from '@/lib/ai-gateway/auto-routing-models'; export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const organizationId = (await params).id; - const feature = validateFeatureHeader(request.headers.get(FEATURE_HEADER)); - return handleTRPCRequest(request, async caller => { const result = await caller.organizations.settings.listAvailableModels({ organizationId, }); return { ...result, - data: await addAutoRoutingModels(filterByFeature(result.data, feature)), + data: await addAutoRoutingModels(result.data), }; }); } diff --git a/apps/web/src/app/api/organizations/[id]/models/validate/route.ts b/apps/web/src/app/api/organizations/[id]/models/validate/route.ts index cb4dc1e42e..528c02cdbf 100644 --- a/apps/web/src/app/api/organizations/[id]/models/validate/route.ts +++ b/apps/web/src/app/api/organizations/[id]/models/validate/route.ts @@ -2,8 +2,6 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import * as z from 'zod'; import { handleTRPCRequest } from '@/lib/trpc-route-handler'; -import { FEATURE_HEADER, validateFeatureHeader } from '@/lib/feature-detection'; -import { filterByFeature } from '@/lib/ai-gateway/models'; const BodySchema = z.object({ modelId: z.string().trim().min(1) }); @@ -29,13 +27,9 @@ export async function POST( } const organizationId = (await params).id; - const feature = validateFeatureHeader(request.headers.get(FEATURE_HEADER)); - return handleTRPCRequest(request, async caller => { const result = await caller.organizations.settings.listAvailableModels({ organizationId }); - const available = filterByFeature(result.data, feature).some( - model => model.id === bodyResult.data.modelId - ); + const available = result.data.some(model => model.id === bodyResult.data.modelId); return available ? { valid: true } : { valid: false, reason: 'unavailable' }; }); } diff --git a/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts b/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts index 29613d98ef..7892737805 100644 --- a/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts +++ b/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts @@ -34,7 +34,7 @@ import { getXKiloCodeVersionNumber } from '@/lib/userAgent'; import { normalizeModelId } from '@/lib/ai-gateway/providers/openrouter'; import { createParser, type EventSourceMessage } from 'eventsource-parser'; import { sentryRootSpan } from '../getRootSpan'; -import { findKiloExclusiveModel, shouldRedactErrorResponse } from '@/lib/ai-gateway/models'; +import { shouldRedactErrorResponse } from '@/lib/ai-gateway/models'; import type { MicrodollarUsageContext, MicrodollarUsageStats, @@ -341,15 +341,6 @@ export function organizationAutoConfigurationResponse(message: string) { ); } -export function featureExclusiveModelResponse(modelId: string) { - const exclusiveTo = findKiloExclusiveModel(modelId)?.exclusive_to ?? []; - const error = `${modelId} is only available for ${exclusiveTo.join(', ')}. Use ${KILO_AUTO_FREE_MODEL.id} as a free alternative.`; - return NextResponse.json( - { error, error_type: ProxyErrorType.feature_exclusive_model, message: error }, - { status: 403 } - ); -} - export function storeAndPreviousResponseIdIsNotSupported() { const error = 'The store and previous_response_id fields are not supported.'; return NextResponse.json( diff --git a/apps/web/src/lib/ai-gateway/model-api-kinds.test.ts b/apps/web/src/lib/ai-gateway/model-api-kinds.test.ts index e2e8eaa01c..6696230c21 100644 --- a/apps/web/src/lib/ai-gateway/model-api-kinds.test.ts +++ b/apps/web/src/lib/ai-gateway/model-api-kinds.test.ts @@ -20,7 +20,6 @@ jest.mock('@/lib/ai-gateway/models', () => { gateway: 'alibaba', internal_id: 'stub-internal', pricing: null, - exclusive_to: [], inference_provider_restriction: [], }; return { diff --git a/apps/web/src/lib/ai-gateway/models.ts b/apps/web/src/lib/ai-gateway/models.ts index d38ed6bf17..60ddc6378c 100644 --- a/apps/web/src/lib/ai-gateway/models.ts +++ b/apps/web/src/lib/ai-gateway/models.ts @@ -2,7 +2,6 @@ * Utility functions for working with AI models */ -import type { FeatureValue } from '@/lib/feature-detection'; import { KILO_AUTO_BALANCED_MODEL, KILO_AUTO_EFFICIENT_MODEL, @@ -132,24 +131,3 @@ export function isDeadFreeModel(model: string): boolean { export function findKiloExclusiveModel(model: string): KiloExclusiveModel | null { return kiloExclusiveModels.find(m => m.public_id === model && m.status !== 'disabled') ?? null; } - -/** - * Returns true if the model should be excluded for the given feature. - * A model is excluded when its `exclusive_to` list is non-empty, the feature is known, - * and the feature is not in `exclusive_to`. - * When feature is null (no header sent), the model is always included. - */ -export function isExcludedForFeature(modelId: string, feature: FeatureValue | null): boolean { - const model = kiloExclusiveModels.find(m => m.public_id === modelId); - if (!model?.exclusive_to.length) return false; - if (!feature) return false; - return !model.exclusive_to.includes(feature); -} - -/** Filters out models that are not available for the given feature. */ -export function filterByFeature( - models: T[], - feature: FeatureValue | null -): T[] { - return models.filter(m => !isExcludedForFeature(m.id, feature)); -} 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 77c9ea9382..d552681efa 100644 --- a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts +++ b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts @@ -41,7 +41,6 @@ export const claude_opus_4_8_stealth_model: KiloExclusiveModel = { gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], pricing: CLAUDE_OPUS_STEALTH_PRICING, - exclusive_to: [], inference_provider_restriction: [], }; @@ -57,7 +56,6 @@ export const claude_opus_4_7_stealth_model: KiloExclusiveModel = { gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], pricing: CLAUDE_OPUS_STEALTH_PRICING, - exclusive_to: [], inference_provider_restriction: [], }; @@ -85,7 +83,6 @@ export const claude_sonnet_4_6_stealth_model: KiloExclusiveModel = { gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], pricing: CLAUDE_SONNET_STEALTH_PRICING, - exclusive_to: [], inference_provider_restriction: [], }; @@ -101,7 +98,6 @@ export const claude_opus_4_6_stealth_model: KiloExclusiveModel = { gateway: 'martian', flags: ['reasoning', 'vision', 'stealth', 'requires-data-collection'], pricing: CLAUDE_OPUS_STEALTH_PRICING, - exclusive_to: [], inference_provider_restriction: [], }; @@ -116,7 +112,6 @@ export const claude_sonnet_clawsetup_model: KiloExclusiveModel = { gateway: 'openrouter', flags: ['reasoning', 'vision', 'vercel-routing'], pricing: null, - exclusive_to: [], inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/deepseek.ts b/apps/web/src/lib/ai-gateway/providers/deepseek.ts index 75e98a7f25..13c6ece768 100644 --- a/apps/web/src/lib/ai-gateway/providers/deepseek.ts +++ b/apps/web/src/lib/ai-gateway/providers/deepseek.ts @@ -26,7 +26,6 @@ export const deepseek_v4_pro_discounted_model: KiloExclusiveModel = { }, }, ], - exclusive_to: [], inference_provider_restriction: ['deepseek'], }; @@ -52,7 +51,6 @@ const deepseek_v4_flash_discounted_model: KiloExclusiveModel = { }, }, ], - exclusive_to: [], inference_provider_restriction: ['deepseek'], }; diff --git a/apps/web/src/lib/ai-gateway/providers/google.ts b/apps/web/src/lib/ai-gateway/providers/google.ts index fb4a8267ac..8d1a9e430c 100644 --- a/apps/web/src/lib/ai-gateway/providers/google.ts +++ b/apps/web/src/lib/ai-gateway/providers/google.ts @@ -18,7 +18,6 @@ export const gemma_4_26b_a4b_it_free_model: KiloExclusiveModel = { gateway: 'openrouter', internal_id: GEMMA_4_26B_A4B_IT_ID, pricing: null, - exclusive_to: [], inference_provider_restriction: [], }; 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 index fbceed83a7..be6ddb2b55 100644 --- 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 @@ -26,7 +26,6 @@ function makeModel( flags: [], gateway: 'openrouter', pricing: null, - exclusive_to: [], inference_provider_restriction: [], ...overrides, }; 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 26466a7aac..12ddbe7dfd 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 @@ -1,4 +1,3 @@ -import type { FeatureValue } from '@/lib/feature-detection'; import { OpenRouterInferenceProviderIdSchema, type OpenRouterInferenceProviderId, @@ -76,8 +75,6 @@ export type KiloExclusiveModel = { gateway: ProviderId; internal_id: string; pricing: PricingTiers | 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. diff --git a/apps/web/src/lib/ai-gateway/providers/meta.ts b/apps/web/src/lib/ai-gateway/providers/meta.ts index e75ea1569f..e833ec42a3 100644 --- a/apps/web/src/lib/ai-gateway/providers/meta.ts +++ b/apps/web/src/lib/ai-gateway/providers/meta.ts @@ -22,6 +22,5 @@ export const muse_spark_1_1_model: KiloExclusiveModel = { }, }, ], - exclusive_to: [], inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts index 1679b155bc..d9ca20032c 100644 --- a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts +++ b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts @@ -31,6 +31,5 @@ export const gpt_5_6_sol_stealth_model: KiloExclusiveModel = { }, }, ], - 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 f4d2b6ff62..7dc1c112b3 100644 --- a/apps/web/src/lib/ai-gateway/providers/qwen.ts +++ b/apps/web/src/lib/ai-gateway/providers/qwen.ts @@ -82,7 +82,6 @@ export const qwen36_plus_stealth_model: KiloExclusiveModel = { ], KILO_STEALTH_DISCOUNT_FACTOR ), - exclusive_to: [], inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/ai-gateway/providers/seed.ts b/apps/web/src/lib/ai-gateway/providers/seed.ts index 6084f98e62..5a40f174b5 100644 --- a/apps/web/src/lib/ai-gateway/providers/seed.ts +++ b/apps/web/src/lib/ai-gateway/providers/seed.ts @@ -12,6 +12,5 @@ export const seed_20_code_free_model: KiloExclusiveModel = { gateway: 'seed', 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 8be0701652..5021ea9e0f 100644 --- a/apps/web/src/lib/ai-gateway/providers/stepfun.ts +++ b/apps/web/src/lib/ai-gateway/providers/stepfun.ts @@ -16,6 +16,5 @@ export const stepfun_37_flash_free_model: KiloExclusiveModel = { gateway: 'openrouter', internal_id: 'stepfun/step-3.7-flash', pricing: null, - exclusive_to: [], inference_provider_restriction: ['stepfun'], }; diff --git a/apps/web/src/lib/ai-gateway/providers/streamlake.ts b/apps/web/src/lib/ai-gateway/providers/streamlake.ts index ccc3992ea9..26e151ace6 100644 --- a/apps/web/src/lib/ai-gateway/providers/streamlake.ts +++ b/apps/web/src/lib/ai-gateway/providers/streamlake.ts @@ -12,6 +12,5 @@ export const kat_coder_pro_v2_5_free_model: KiloExclusiveModel = { gateway: 'streamlake', internal_id: 'ep-fsp5wc-1783487206835267047', pricing: null, - exclusive_to: [], inference_provider_restriction: [], }; diff --git a/apps/web/src/lib/proxy-error-types.ts b/apps/web/src/lib/proxy-error-types.ts index fe0db0cc48..b2bfa5d076 100644 --- a/apps/web/src/lib/proxy-error-types.ts +++ b/apps/web/src/lib/proxy-error-types.ts @@ -13,7 +13,6 @@ export const proxyErrorTypeSchema = z.enum([ 'model_not_allowed', 'discontinued_free_model', 'model_not_found', - 'feature_exclusive_model', 'unsupported_field', 'authentication_required', 'missing_client_ip', From dd7ee49cc14a52c4ab603910f108a0691c7478cd Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:13:58 +0000 Subject: [PATCH 2/5] fix: retain exclusive model lookup import Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts b/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts index 7892737805..fc2ceaf290 100644 --- a/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts +++ b/apps/web/src/lib/ai-gateway/llm-proxy-helpers.ts @@ -34,7 +34,7 @@ import { getXKiloCodeVersionNumber } from '@/lib/userAgent'; import { normalizeModelId } from '@/lib/ai-gateway/providers/openrouter'; import { createParser, type EventSourceMessage } from 'eventsource-parser'; import { sentryRootSpan } from '../getRootSpan'; -import { shouldRedactErrorResponse } from '@/lib/ai-gateway/models'; +import { findKiloExclusiveModel, shouldRedactErrorResponse } from '@/lib/ai-gateway/models'; import type { MicrodollarUsageContext, MicrodollarUsageStats, From 6dabe5a802da9587da511d698baa124ede60d8d5 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:19:14 +0000 Subject: [PATCH 3/5] fix: remove unused models request parameter Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/api/openrouter/models/route.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/api/openrouter/models/route.ts b/apps/web/src/app/api/openrouter/models/route.ts index a47b0330b3..7cc5b6a96c 100644 --- a/apps/web/src/app/api/openrouter/models/route.ts +++ b/apps/web/src/app/api/openrouter/models/route.ts @@ -1,4 +1,3 @@ -import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { captureException } from '@sentry/nextjs'; import type { OpenRouterModelsResponse } from '@/lib/organizations/organization-types'; @@ -24,9 +23,9 @@ async function tryGetUserFromAuth() { * Test using: * curl -vvv 'http://localhost:3000/api/openrouter/models' */ -export async function GET( - request: NextRequest -): Promise> { +export async function GET(): Promise< + NextResponse<{ error: string; message?: string } | OpenRouterModelsResponse> +> { const auth = await tryGetUserFromAuth(); try { const result = auth?.organizationId From f0843485b15801aa7dd9bf0bc9895f73ff72819b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:25:41 +0000 Subject: [PATCH 4/5] fix: preserve models route handler signature Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/api/openrouter/models/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/api/openrouter/models/route.ts b/apps/web/src/app/api/openrouter/models/route.ts index 7cc5b6a96c..b73d981a59 100644 --- a/apps/web/src/app/api/openrouter/models/route.ts +++ b/apps/web/src/app/api/openrouter/models/route.ts @@ -1,3 +1,4 @@ +import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { captureException } from '@sentry/nextjs'; import type { OpenRouterModelsResponse } from '@/lib/organizations/organization-types'; @@ -23,7 +24,7 @@ async function tryGetUserFromAuth() { * Test using: * curl -vvv 'http://localhost:3000/api/openrouter/models' */ -export async function GET(): Promise< +export async function GET(_request: NextRequest): Promise< NextResponse<{ error: string; message?: string } | OpenRouterModelsResponse> > { const auth = await tryGetUserFromAuth(); From 89a0e64e2dc60c29553d8c1495c13f80d91a0464 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:30:28 +0000 Subject: [PATCH 5/5] style: format models route signature Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/app/api/openrouter/models/route.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/api/openrouter/models/route.ts b/apps/web/src/app/api/openrouter/models/route.ts index b73d981a59..aa7ebfe23c 100644 --- a/apps/web/src/app/api/openrouter/models/route.ts +++ b/apps/web/src/app/api/openrouter/models/route.ts @@ -24,9 +24,9 @@ async function tryGetUserFromAuth() { * Test using: * curl -vvv 'http://localhost:3000/api/openrouter/models' */ -export async function GET(_request: NextRequest): Promise< - NextResponse<{ error: string; message?: string } | OpenRouterModelsResponse> -> { +export async function GET( + _request: NextRequest +): Promise> { const auth = await tryGetUserFromAuth(); try { const result = auth?.organizationId