From 45f2acec6b04ea98f92ece5e7d85c07e2c74ca44 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Tue, 3 Mar 2026 16:15:28 +0100 Subject: [PATCH 1/2] Eliminate most uses of createProviderAwareModelAllowPredicate --- packages/db/src/schema-types.ts | 2 ++ .../api/organizations/[id]/defaults/route.ts | 16 ++++++------- src/lib/integrations/discord-service.ts | 8 +++---- src/lib/integrations/slack-service.ts | 8 +++---- src/lib/model-allow.client.ts | 1 + src/lib/model-allow.server.ts | 11 +++++++++ src/lib/slack-bot/model-allow-list.ts | 12 +++++----- .../organization-settings-router.ts | 24 +++++++++---------- 8 files changed, 48 insertions(+), 34 deletions(-) diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index 82a983fe4..8e14fc1a2 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -125,7 +125,9 @@ export const OrganizationPlanSchema = z.enum(['teams', 'enterprise']); export type OrganizationPlan = z.infer; const OrganizationSettingsSchema = z.object({ + /** @deprecated */ model_allow_list: z.array(z.string()).optional(), + /** @deprecated */ provider_allow_list: z.array(z.string()).optional(), // under development, not yet enforced, will replace model_allow_list and provider_allow_list: diff --git a/src/app/api/organizations/[id]/defaults/route.ts b/src/app/api/organizations/[id]/defaults/route.ts index bf5bd8444..a5d971657 100644 --- a/src/app/api/organizations/[id]/defaults/route.ts +++ b/src/app/api/organizations/[id]/defaults/route.ts @@ -3,7 +3,7 @@ import { getAuthorizedOrgContext } from '@/lib/organizations/organization-auth'; import type { NextRequest } from 'next/server'; import { PRIMARY_DEFAULT_MODEL, getFirstFreeModel, preferredModels } from '@/lib/models'; import { getEnhancedOpenRouterModels } from '@/lib/providers/openrouter'; -import { createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server'; +import { createAllowPredicateFromDenyList } from '@/lib/model-allow.server'; import { getModelIdToProviderSlugsIndex } from '@/lib/providers/openrouter/models-by-provider-index.server'; type DefaultsResponse = { @@ -26,9 +26,9 @@ export async function GET( // Get organization's default model setting let defaultModel = organization.settings?.default_model; - const allowList = organization.settings?.model_allow_list; + const denyList = organization.settings?.model_deny_list; - const isAllowed = createProviderAwareModelAllowPredicate(allowList ?? []); + const isAllowed = createAllowPredicateFromDenyList(denyList ?? []); const findFirstAllowedModel = async (modelIds: readonly string[]) => { for (const modelId of modelIds) { @@ -73,23 +73,23 @@ export async function GET( defaultModel = await findFirstAllowedModel([PRIMARY_DEFAULT_MODEL]); if (!defaultModel) { - if (!allowList?.length) { + if (!denyList?.length) { defaultModel = PRIMARY_DEFAULT_MODEL; } else { - const firstConcreteAllowedModel = allowList.find(modelId => !modelId.endsWith('/*')); + const firstConcreteAllowedModel = denyList.find(modelId => !modelId.endsWith('/*')); defaultModel = firstConcreteAllowedModel; } } - if (!defaultModel && allowList?.length) { + if (!defaultModel && denyList?.length) { defaultModel = await findFirstAllowedModel(preferredModels); } - if (!defaultModel && allowList?.length) { + if (!defaultModel && denyList?.length) { defaultModel = await findFirstAllowedModelFromDbSnapshot(); } - if (!defaultModel && allowList?.length) { + if (!defaultModel && denyList?.length) { defaultModel = await findFirstAllowedModelFromOpenRouter(); } diff --git a/src/lib/integrations/discord-service.ts b/src/lib/integrations/discord-service.ts index d0909d2a5..ad9de06b2 100644 --- a/src/lib/integrations/discord-service.ts +++ b/src/lib/integrations/discord-service.ts @@ -10,7 +10,7 @@ import { DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN } from '@/l import { APP_URL } from '@/lib/constants'; import { getOrganizationById } from '@/lib/organizations/organizations'; import { getDefaultAllowedModel } from '@/lib/slack-bot/model-allow-list'; -import { createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server'; +import { createAllowPredicateFromDenyList } from '@/lib/model-allow.server'; import { minimax_m25_free_model } from '@/lib/providers/minimax'; import { CLAUDE_OPUS_CURRENT_MODEL_ID } from '@/lib/providers/anthropic'; @@ -367,9 +367,9 @@ export async function updateModel( if (owner.type === 'org') { const organization = await getOrganizationById(owner.id); if (organization) { - const modelAllowList = organization.settings?.model_allow_list || []; - if (modelAllowList.length > 0) { - const isAllowed = createProviderAwareModelAllowPredicate(modelAllowList); + const modelDenyList = organization.settings?.model_deny_list || []; + if (modelDenyList.length > 0) { + const isAllowed = createAllowPredicateFromDenyList(modelDenyList); if (!(await isAllowed(modelSlug))) { return { success: false, error: 'Model is not allowed by organization policy' }; } diff --git a/src/lib/integrations/slack-service.ts b/src/lib/integrations/slack-service.ts index c5e157eaf..27a177ae1 100644 --- a/src/lib/integrations/slack-service.ts +++ b/src/lib/integrations/slack-service.ts @@ -12,7 +12,7 @@ import { WebClient } from '@slack/web-api'; import type { OAuthV2Response } from '@slack/oauth'; import { getOrganizationById } from '@/lib/organizations/organizations'; import { getDefaultAllowedModel } from '@/lib/slack-bot/model-allow-list'; -import { createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server'; +import { createAllowPredicateFromDenyList } from '@/lib/model-allow.server'; import { minimax_m25_free_model } from '@/lib/providers/minimax'; import { CLAUDE_OPUS_CURRENT_MODEL_ID } from '@/lib/providers/anthropic'; @@ -481,9 +481,9 @@ export async function updateModel( if (owner.type === 'org') { const organization = await getOrganizationById(owner.id); if (organization) { - const modelAllowList = organization.settings?.model_allow_list || []; - if (modelAllowList.length > 0) { - const isAllowed = createProviderAwareModelAllowPredicate(modelAllowList); + const modelDenyList = organization.settings?.model_deny_list || []; + if (modelDenyList.length > 0) { + const isAllowed = createAllowPredicateFromDenyList(modelDenyList); if (!(await isAllowed(modelSlug))) { return { success: false, error: 'Model is not allowed by organization policy' }; } diff --git a/src/lib/model-allow.client.ts b/src/lib/model-allow.client.ts index ca9b29dd6..33f15f69e 100644 --- a/src/lib/model-allow.client.ts +++ b/src/lib/model-allow.client.ts @@ -50,6 +50,7 @@ function getOrBuildModelProvidersIndex( /** * Client-safe allow-list evaluation that mirrors * [`createProviderAwareModelAllowPredicate()`](src/lib/model-allow.server.ts:12). + * @deprecated */ export function isModelAllowedProviderAwareClient( modelId: string, diff --git a/src/lib/model-allow.server.ts b/src/lib/model-allow.server.ts index 3010e396c..6a55de8e6 100644 --- a/src/lib/model-allow.server.ts +++ b/src/lib/model-allow.server.ts @@ -19,6 +19,7 @@ type ProviderAwareAllowPredicateOptions = { export type ProviderAwareAllowPredicate = (modelId: string) => Promise; +/** @deprecated Use `createAllowPredicateFromDenyList` instead */ export function createProviderAwareModelAllowPredicate( allowList: string[], options?: ProviderAwareAllowPredicateOptions @@ -48,6 +49,16 @@ export function createProviderAwareModelAllowPredicate( }; } +export function createAllowPredicateFromDenyList( + denyList: string[] | undefined +): ProviderAwareAllowPredicate { + const denyListSet = new Set(denyList); + return (modelId: string): Promise => { + const normalizedModelId = normalizeModelId(modelId); + return Promise.resolve(!denyListSet.has(normalizedModelId)); + }; +} + export async function createDenyLists( model_allow_list: string[] | undefined, provider_allow_list: string[] | undefined diff --git a/src/lib/slack-bot/model-allow-list.ts b/src/lib/slack-bot/model-allow-list.ts index f75d2d565..54210cd4e 100644 --- a/src/lib/slack-bot/model-allow-list.ts +++ b/src/lib/slack-bot/model-allow-list.ts @@ -1,6 +1,6 @@ import { PRIMARY_DEFAULT_MODEL, preferredModels } from '@/lib/models'; import { getOrganizationById } from '@/lib/organizations/organizations'; -import { createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server'; +import { createAllowPredicateFromDenyList } from '@/lib/model-allow.server'; /** * Get a default model that is allowed for an organization. @@ -15,14 +15,14 @@ export async function getDefaultAllowedModel( return globalDefault; } - const modelAllowList = organization.settings?.model_allow_list || []; + const modelDenyList = organization.settings?.model_deny_list || []; // If no restrictions, use global default - if (modelAllowList.length === 0) { + if (modelDenyList.length === 0) { return globalDefault; } - const isAllowed = createProviderAwareModelAllowPredicate(modelAllowList); + const isAllowed = createAllowPredicateFromDenyList(modelDenyList); // Check if the organization's default model is allowed const orgDefaultModel = organization.settings?.default_model; @@ -42,7 +42,7 @@ export async function getDefaultAllowedModel( } // Fall back to the first non-wildcard model in the allow list - const firstNonWildcard = modelAllowList.find(m => !m.endsWith('/*')); + const firstNonWildcard = modelDenyList.find(m => !m.endsWith('/*')); if (firstNonWildcard) { return firstNonWildcard; } @@ -50,7 +50,7 @@ export async function getDefaultAllowedModel( // If only wildcards, fall back to global default (admin misconfiguration) console.warn( '[SlackBot] Organization has only wildcard entries in model allow list:', - modelAllowList + modelDenyList ); return globalDefault; } diff --git a/src/routers/organizations/organization-settings-router.ts b/src/routers/organizations/organization-settings-router.ts index dc8f09e81..7dcafc4d1 100644 --- a/src/routers/organizations/organization-settings-router.ts +++ b/src/routers/organizations/organization-settings-router.ts @@ -15,7 +15,7 @@ import * as z from 'zod'; import { createAuditLog } from '@/lib/organizations/organization-audit-logs'; import { getEnhancedOpenRouterModels } from '@/lib/providers/openrouter'; import { requireActiveSubscriptionOrTrial } from '@/lib/organizations/trial-middleware'; -import { createDenyLists, createProviderAwareModelAllowPredicate } from '@/lib/model-allow.server'; +import { createAllowPredicateFromDenyList, createDenyLists } from '@/lib/model-allow.server'; import { KILO_ORGANIZATION_ID } from '@/lib/organizations/constants'; import { listAvailableCustomLlms } from '@/lib/custom-llm/listAvailableCustomLlms'; @@ -158,17 +158,17 @@ export const organizationsSettingsRouter = createTRPCRouter({ }); } - let allowedModels: string[] | undefined; + let deniedModels: string[] | undefined; - if (organization.plan === 'enterprise' && organization?.settings?.model_allow_list) { - allowedModels = organization.settings.model_allow_list; + if (organization.plan === 'enterprise' && organization?.settings?.model_deny_list) { + deniedModels = organization.settings.model_deny_list; } const responseData = await getEnhancedOpenRouterModels(); let filteredModels = responseData.data; - if (allowedModels) { - const isAllowed = createProviderAwareModelAllowPredicate(allowedModels); + if (deniedModels) { + const isAllowed = createAllowPredicateFromDenyList(deniedModels); const models: OpenRouterModel[] = []; for (const model of responseData.data) { if (await isAllowed(model.id)) { @@ -234,11 +234,11 @@ export const organizationsSettingsRouter = createTRPCRouter({ // Check if default_model needs to be cleared if ( - model_allow_list !== undefined && + settingsUpdate.model_deny_list !== undefined && currentSettings.default_model && - model_allow_list.length > 0 + settingsUpdate.model_deny_list.length > 0 ) { - const isAllowed = createProviderAwareModelAllowPredicate(model_allow_list); + const isAllowed = createAllowPredicateFromDenyList(settingsUpdate.model_deny_list); if (!(await isAllowed(currentSettings.default_model))) { // Clear default_model if it's no longer in the allow list @@ -287,9 +287,9 @@ export const organizationsSettingsRouter = createTRPCRouter({ } // Validate default_model against existing model_allow_list - const existingAllowedModels = existingOrg.settings?.model_allow_list; - if (existingAllowedModels && existingAllowedModels.length > 0) { - const isAllowed = createProviderAwareModelAllowPredicate(existingAllowedModels); + const existingDeniedModels = existingOrg.settings?.model_deny_list; + if (existingDeniedModels && existingDeniedModels.length > 0) { + const isAllowed = createAllowPredicateFromDenyList(existingDeniedModels); if (default_model && !(await isAllowed(default_model))) { throw new TRPCError({ From 1cf6fd06bc26e658846d30351d5db7000d992565 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Tue, 3 Mar 2026 16:26:56 +0100 Subject: [PATCH 2/2] Remove isModelAllowedProviderAwareClient --- ...ionProvidersAndModelsConfigurationCard.tsx | 7 +- .../providers-and-models/allowLists.domain.ts | 10 +-- .../useOrganizationConfiguration.ts | 9 +- src/lib/model-allow.client.test.ts | 40 --------- src/lib/model-allow.client.ts | 83 ------------------- 5 files changed, 9 insertions(+), 140 deletions(-) delete mode 100644 src/lib/model-allow.client.test.ts delete mode 100644 src/lib/model-allow.client.ts diff --git a/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx b/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx index c56915a06..4ae8ebbef 100644 --- a/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx +++ b/src/components/organizations/OrganizationProvidersAndModelsConfigurationCard.tsx @@ -13,7 +13,7 @@ import { AvailableModelsDialog } from './providers-and-models/AvailableModelsDia import { useOrganizationConfiguration } from './providers-and-models/useOrganizationConfiguration'; import { useOpenRouterModelsAndProviders } from '@/app/api/openrouter/hooks'; import type { ProviderSelection } from '@/components/models/util'; -import { isModelAllowedProviderAwareClient } from '@/lib/model-allow.client'; +import { normalizeModelId } from '@/lib/model-utils'; type OrganizationProvidersAndModelsConfigurationCardProps = { organizationId: string; @@ -67,8 +67,7 @@ export function computeProviderSelectionsForSummaryCard(params: { .filter(model => { if (!model.endpoint) return false; return ( - modelAllowList.length === 0 || - isModelAllowedProviderAwareClient(model.slug, modelAllowList, openRouterProviders) + modelAllowList.length === 0 || modelAllowList.includes(normalizeModelId(model.slug)) ); }) .map(model => model.slug); @@ -86,7 +85,7 @@ export function computeProviderSelectionsForSummaryCard(params: { const selectedModels = provider.models .filter(model => { if (!model.endpoint) return false; - return isModelAllowedProviderAwareClient(model.slug, modelAllowList, openRouterProviders); + return modelAllowList.includes(normalizeModelId(model.slug)); }) .map(model => model.slug); diff --git a/src/components/organizations/providers-and-models/allowLists.domain.ts b/src/components/organizations/providers-and-models/allowLists.domain.ts index c6445c956..a2be6d1f9 100644 --- a/src/components/organizations/providers-and-models/allowLists.domain.ts +++ b/src/components/organizations/providers-and-models/allowLists.domain.ts @@ -1,4 +1,3 @@ -import { isModelAllowedProviderAwareClient } from '@/lib/model-allow.client'; import { normalizeModelId } from '@/lib/model-utils'; export type OpenRouterModelSlugSnapshot = { @@ -93,7 +92,7 @@ export function computeEnabledProviderSlugs( export function computeAllowedModelIds( draftModelAllowList: ReadonlyArray, openRouterModels: ReadonlyArray, - openRouterProviders: OpenRouterProviderModelsSnapshot + _openRouterProviders: OpenRouterProviderModelsSnapshot ): Set { const allowed = new Set(); @@ -107,12 +106,7 @@ export function computeAllowedModelIds( const allowListArray = [...draftModelAllowList]; for (const model of openRouterModels) { const normalizedModelId = normalizeModelId(model.slug); - const isAllowed = isModelAllowedProviderAwareClient( - normalizedModelId, - allowListArray, - openRouterProviders - ); - if (isAllowed) { + if (allowListArray.includes(normalizedModelId)) { allowed.add(normalizedModelId); } } diff --git a/src/components/organizations/providers-and-models/useOrganizationConfiguration.ts b/src/components/organizations/providers-and-models/useOrganizationConfiguration.ts index 60aa46778..85009b845 100644 --- a/src/components/organizations/providers-and-models/useOrganizationConfiguration.ts +++ b/src/components/organizations/providers-and-models/useOrganizationConfiguration.ts @@ -7,7 +7,7 @@ import { useOpenRouterProviders, } from '@/app/api/openrouter/hooks'; import type { OpenRouterProvider } from '@/lib/organizations/organization-types'; -import { isModelAllowedProviderAwareClient } from '@/lib/model-allow.client'; +import { normalizeModelId } from '@/lib/model-utils'; export type ConfigurationData = { allModelsSelected: boolean; @@ -23,8 +23,7 @@ export function useOrganizationConfiguration(organizationId: string) { useOrganizationWithMembers(organizationId); const { data: modelsData, isLoading: modelsLoading } = useOpenRouterModels(); const { data: providersData, isLoading: providersLoading } = useOpenRouterProviders(); - const { providers: openRouterProviders, isLoading: providersSnapshotLoading } = - useOpenRouterModelsAndProviders(); + const { isLoading: providersSnapshotLoading } = useOpenRouterModelsAndProviders(); const isLoading = orgLoading || modelsLoading || providersLoading || providersSnapshotLoading; @@ -55,14 +54,14 @@ export function useOrganizationConfiguration(organizationId: string) { displayModelAllowList = []; // No exclusions } else { const allowedModelCount = allModelIds.filter(modelId => - isModelAllowedProviderAwareClient(modelId, savedModelAllowList, openRouterProviders) + savedModelAllowList.includes(normalizeModelId(modelId)) ).length; const modelAllowRatio = allowedModelCount / allModelIds.length; // If more than 50% are allowed, treat as "all selected" mode with exclusions if (modelAllowRatio > 0.5) { allModelsSelected = true; displayModelAllowList = allModelIds.filter( - id => !isModelAllowedProviderAwareClient(id, savedModelAllowList, openRouterProviders) + id => !savedModelAllowList.includes(normalizeModelId(id)) ); } else { allModelsSelected = false; diff --git a/src/lib/model-allow.client.test.ts b/src/lib/model-allow.client.test.ts deleted file mode 100644 index df921560e..000000000 --- a/src/lib/model-allow.client.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { isModelAllowedProviderAwareClient } from '@/lib/model-allow.client'; - -describe('isModelAllowedProviderAwareClient', () => { - test('provider-membership wildcard allows model even when model namespace differs', () => { - const openRouterProviders = [ - { - slug: 'cerebras', - models: [{ slug: 'z-ai/glm4.6', endpoint: {} }], - }, - ]; - - expect( - isModelAllowedProviderAwareClient('z-ai/glm4.6', ['cerebras/*'], openRouterProviders) - ).toBe(true); - expect( - isModelAllowedProviderAwareClient('openai/gpt-5.2', ['cerebras/*'], openRouterProviders) - ).toBe(false); - }); - - test('keeps exact + namespace wildcard behavior (including :free normalization)', () => { - const openRouterProviders = [ - { - slug: 'openai', - models: [{ slug: 'openai/gpt-4.1', endpoint: {} }], - }, - ]; - - expect( - isModelAllowedProviderAwareClient('openai/gpt-4.1:free', ['openai/*'], openRouterProviders) - ).toBe(true); - expect( - isModelAllowedProviderAwareClient( - 'openai/gpt-4.1:free', - ['openai/gpt-4.1'], - openRouterProviders - ) - ).toBe(true); - }); -}); diff --git a/src/lib/model-allow.client.ts b/src/lib/model-allow.client.ts deleted file mode 100644 index 33f15f69e..000000000 --- a/src/lib/model-allow.client.ts +++ /dev/null @@ -1,83 +0,0 @@ -import 'client-only'; - -import { normalizeModelId } from '@/lib/model-utils'; -import { - isAllowedByExactOrNamespaceWildcard, - isAllowedByProviderMembershipWildcard, - prepareModelAllowList, -} from '@/lib/model-allow.shared'; - -export type OpenRouterProviderModelsSnapshot = Array<{ - slug: string; - models: Array<{ - slug: string; - endpoint?: unknown; - }>; -}>; - -type ModelProvidersIndex = Map>; - -const modelProvidersIndexCache = new WeakMap< - ReadonlyArray<{ slug: string; models: ReadonlyArray<{ slug: string }> }>, - ModelProvidersIndex ->(); - -function getOrBuildModelProvidersIndex( - openRouterProviders: OpenRouterProviderModelsSnapshot -): ModelProvidersIndex { - const cached = modelProvidersIndexCache.get(openRouterProviders); - if (cached) { - return cached; - } - - const index: ModelProvidersIndex = new Map(); - for (const provider of openRouterProviders) { - for (const model of provider.models) { - const normalizedModelId = normalizeModelId(model.slug); - const providersForModel = index.get(normalizedModelId); - if (providersForModel) { - providersForModel.add(provider.slug); - } else { - index.set(normalizedModelId, new Set([provider.slug])); - } - } - } - - modelProvidersIndexCache.set(openRouterProviders, index); - return index; -} - -/** - * Client-safe allow-list evaluation that mirrors - * [`createProviderAwareModelAllowPredicate()`](src/lib/model-allow.server.ts:12). - * @deprecated - */ -export function isModelAllowedProviderAwareClient( - modelId: string, - allowList: string[], - openRouterProviders: OpenRouterProviderModelsSnapshot -): boolean { - if (allowList.length === 0) { - return true; - } - - const normalizedModelId = normalizeModelId(modelId); - - const { allowListSet, wildcardProviderSlugs } = prepareModelAllowList(allowList); - - if (isAllowedByExactOrNamespaceWildcard(normalizedModelId, allowListSet)) { - return true; - } - - // 3) Provider-membership wildcard match - if (wildcardProviderSlugs.size === 0) { - return false; - } - - const modelProvidersIndex = getOrBuildModelProvidersIndex(openRouterProviders); - const providersForModel = modelProvidersIndex.get(normalizedModelId); - return isAllowedByProviderMembershipWildcard( - providersForModel || new Set(), - wildcardProviderSlugs - ); -}