Skip to content

Commit 261becd

Browse files
authored
feat(blacklist): added ability to blacklist models & providers (#2709)
* feat(blacklist): added ability to blacklist models & providers * ack PR comments
1 parent 3ecf7a1 commit 261becd

File tree

10 files changed

+146
-48
lines changed

10 files changed

+146
-48
lines changed

apps/sim/app/api/providers/ollama/models/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { env } from '@/lib/core/config/env'
44
import type { ModelsObject } from '@/providers/ollama/types'
5+
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
56

67
const logger = createLogger('OllamaModelsAPI')
78
const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434'
89

910
/**
1011
* Get available Ollama models
1112
*/
12-
export async function GET(request: NextRequest) {
13+
export async function GET(_request: NextRequest) {
14+
if (isProviderBlacklisted('ollama')) {
15+
logger.info('Ollama provider is blacklisted, returning empty models')
16+
return NextResponse.json({ models: [] })
17+
}
18+
1319
try {
1420
logger.info('Fetching Ollama models', {
1521
host: OLLAMA_HOST,
@@ -31,10 +37,12 @@ export async function GET(request: NextRequest) {
3137
}
3238

3339
const data = (await response.json()) as ModelsObject
34-
const models = data.models.map((model) => model.name)
40+
const allModels = data.models.map((model) => model.name)
41+
const models = filterBlacklistedModels(allModels)
3542

3643
logger.info('Successfully fetched Ollama models', {
3744
count: models.length,
45+
filtered: allModels.length - models.length,
3846
models,
3947
})
4048

apps/sim/app/api/providers/openrouter/models/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { filterBlacklistedModels } from '@/providers/utils'
3+
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
44

55
const logger = createLogger('OpenRouterModelsAPI')
66

@@ -30,6 +30,11 @@ export interface OpenRouterModelInfo {
3030
}
3131

3232
export async function GET(_request: NextRequest) {
33+
if (isProviderBlacklisted('openrouter')) {
34+
logger.info('OpenRouter provider is blacklisted, returning empty models')
35+
return NextResponse.json({ models: [], modelInfo: {} })
36+
}
37+
3338
try {
3439
const response = await fetch('https://openrouter.ai/api/v1/models', {
3540
headers: { 'Content-Type': 'application/json' },

apps/sim/app/api/providers/vllm/models/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { env } from '@/lib/core/config/env'
4+
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
45

56
const logger = createLogger('VLLMModelsAPI')
67

78
/**
89
* Get available vLLM models
910
*/
10-
export async function GET(request: NextRequest) {
11+
export async function GET(_request: NextRequest) {
12+
if (isProviderBlacklisted('vllm')) {
13+
logger.info('vLLM provider is blacklisted, returning empty models')
14+
return NextResponse.json({ models: [] })
15+
}
16+
1117
const baseUrl = (env.VLLM_BASE_URL || '').replace(/\/$/, '')
1218

1319
if (!baseUrl) {
@@ -42,10 +48,12 @@ export async function GET(request: NextRequest) {
4248
}
4349

4450
const data = (await response.json()) as { data: Array<{ id: string }> }
45-
const models = data.data.map((model) => `vllm/${model.id}`)
51+
const allModels = data.data.map((model) => `vllm/${model.id}`)
52+
const models = filterBlacklistedModels(allModels)
4653

4754
logger.info('Successfully fetched vLLM models', {
4855
count: models.length,
56+
filtered: allModels.length - models.length,
4957
models,
5058
})
5159

apps/sim/blocks/blocks/agent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isHosted } from '@/lib/core/config/feature-flags'
44
import type { BlockConfig } from '@/blocks/types'
55
import { AuthMode } from '@/blocks/types'
66
import {
7-
getAllModelProviders,
7+
getBaseModelProviders,
88
getHostedModels,
99
getMaxTemperature,
1010
getProviderIcon,
@@ -417,7 +417,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
417417
condition: () => ({
418418
field: 'model',
419419
value: (() => {
420-
const allModels = Object.keys(getAllModelProviders())
420+
const allModels = Object.keys(getBaseModelProviders())
421421
return allModels.filter(
422422
(model) => supportsTemperature(model) && getMaxTemperature(model) === 1
423423
)
@@ -434,7 +434,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
434434
condition: () => ({
435435
field: 'model',
436436
value: (() => {
437-
const allModels = Object.keys(getAllModelProviders())
437+
const allModels = Object.keys(getBaseModelProviders())
438438
return allModels.filter(
439439
(model) => supportsTemperature(model) && getMaxTemperature(model) === 2
440440
)
@@ -555,7 +555,7 @@ Example 3 (Array Input):
555555
if (!model) {
556556
throw new Error('No model selected')
557557
}
558-
const tool = getAllModelProviders()[model]
558+
const tool = getBaseModelProviders()[model]
559559
if (!tool) {
560560
throw new Error(`Invalid model selected: ${model}`)
561561
}

apps/sim/blocks/blocks/evaluator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { isHosted } from '@/lib/core/config/feature-flags'
44
import type { BlockConfig, ParamType } from '@/blocks/types'
55
import type { ProviderId } from '@/providers/types'
66
import {
7-
getAllModelProviders,
7+
getBaseModelProviders,
88
getHostedModels,
99
getProviderIcon,
1010
providers,
@@ -357,7 +357,7 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
357357
if (!model) {
358358
throw new Error('No model selected')
359359
}
360-
const tool = getAllModelProviders()[model as ProviderId]
360+
const tool = getBaseModelProviders()[model as ProviderId]
361361
if (!tool) {
362362
throw new Error(`Invalid model selected: ${model}`)
363363
}

apps/sim/blocks/blocks/router.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isHosted } from '@/lib/core/config/feature-flags'
33
import { AuthMode, type BlockConfig } from '@/blocks/types'
44
import type { ProviderId } from '@/providers/types'
55
import {
6-
getAllModelProviders,
6+
getBaseModelProviders,
77
getHostedModels,
88
getProviderIcon,
99
providers,
@@ -324,7 +324,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
324324
if (!model) {
325325
throw new Error('No model selected')
326326
}
327-
const tool = getAllModelProviders()[model as ProviderId]
327+
const tool = getBaseModelProviders()[model as ProviderId]
328328
if (!tool) {
329329
throw new Error(`Invalid model selected: ${model}`)
330330
}
@@ -508,7 +508,7 @@ export const RouterV2Block: BlockConfig<RouterV2Response> = {
508508
if (!model) {
509509
throw new Error('No model selected')
510510
}
511-
const tool = getAllModelProviders()[model as ProviderId]
511+
const tool = getBaseModelProviders()[model as ProviderId]
512512
if (!tool) {
513513
throw new Error(`Invalid model selected: ${model}`)
514514
}

apps/sim/lib/core/config/env.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export const env = createEnv({
8787
ELEVENLABS_API_KEY: z.string().min(1).optional(), // ElevenLabs API key for text-to-speech in deployed chat
8888
SERPER_API_KEY: z.string().min(1).optional(), // Serper API key for online search
8989
EXA_API_KEY: z.string().min(1).optional(), // Exa AI API key for enhanced online search
90-
DEEPSEEK_MODELS_ENABLED: z.boolean().optional().default(false), // Enable Deepseek models in UI (defaults to false for compliance)
90+
BLACKLISTED_PROVIDERS: z.string().optional(), // Comma-separated provider IDs to hide (e.g., "openai,anthropic")
91+
BLACKLISTED_MODELS: z.string().optional(), // Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
9192

9293
// Azure Configuration - Shared credentials with feature-specific models
9394
AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Shared Azure OpenAI service endpoint

apps/sim/providers/utils.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as environmentModule from '@/lib/core/config/feature-flags'
33
import {
44
calculateCost,
55
extractAndParseJSON,
6+
filterBlacklistedModels,
67
formatCost,
78
generateStructuredOutputInstructions,
89
getAllModelProviders,
@@ -17,6 +18,7 @@ import {
1718
getProviderConfigFromModel,
1819
getProviderFromModel,
1920
getProviderModels,
21+
isProviderBlacklisted,
2022
MODELS_TEMP_RANGE_0_1,
2123
MODELS_TEMP_RANGE_0_2,
2224
MODELS_WITH_REASONING_EFFORT,
@@ -976,3 +978,46 @@ describe('Tool Management', () => {
976978
})
977979
})
978980
})
981+
982+
describe('Provider/Model Blacklist', () => {
983+
describe('isProviderBlacklisted', () => {
984+
it.concurrent('should return false when no providers are blacklisted', () => {
985+
expect(isProviderBlacklisted('openai')).toBe(false)
986+
expect(isProviderBlacklisted('anthropic')).toBe(false)
987+
})
988+
})
989+
990+
describe('filterBlacklistedModels', () => {
991+
it.concurrent('should return all models when no blacklist is set', () => {
992+
const models = ['gpt-4o', 'claude-sonnet-4-5', 'gemini-2.5-pro']
993+
const result = filterBlacklistedModels(models)
994+
expect(result).toEqual(models)
995+
})
996+
997+
it.concurrent('should return empty array for empty input', () => {
998+
const result = filterBlacklistedModels([])
999+
expect(result).toEqual([])
1000+
})
1001+
})
1002+
1003+
describe('getBaseModelProviders blacklist filtering', () => {
1004+
it.concurrent('should return providers when no blacklist is set', () => {
1005+
const providers = getBaseModelProviders()
1006+
expect(Object.keys(providers).length).toBeGreaterThan(0)
1007+
expect(providers['gpt-4o']).toBe('openai')
1008+
expect(providers['claude-sonnet-4-5']).toBe('anthropic')
1009+
})
1010+
})
1011+
1012+
describe('getProviderFromModel execution-time enforcement', () => {
1013+
it.concurrent('should return provider for non-blacklisted models', () => {
1014+
expect(getProviderFromModel('gpt-4o')).toBe('openai')
1015+
expect(getProviderFromModel('claude-sonnet-4-5')).toBe('anthropic')
1016+
})
1017+
1018+
it.concurrent('should be case insensitive', () => {
1019+
expect(getProviderFromModel('GPT-4O')).toBe('openai')
1020+
expect(getProviderFromModel('CLAUDE-SONNET-4-5')).toBe('anthropic')
1021+
})
1022+
})
1023+
})

apps/sim/providers/utils.ts

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger, type Logger } from '@sim/logger'
22
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
33
import type { CompletionUsage } from 'openai/resources/completions'
4-
import { getEnv, isTruthy } from '@/lib/core/config/env'
4+
import { env } from '@/lib/core/config/env'
55
import { isHosted } from '@/lib/core/config/feature-flags'
66
import { isCustomTool } from '@/executor/constants'
77
import {
@@ -131,6 +131,9 @@ function filterBlacklistedModelsFromProviderMap(
131131
): Record<string, ProviderId> {
132132
const filtered: Record<string, ProviderId> = {}
133133
for (const [model, providerId] of Object.entries(providerMap)) {
134+
if (isProviderBlacklisted(providerId)) {
135+
continue
136+
}
134137
if (!isModelBlacklisted(model)) {
135138
filtered[model] = providerId
136139
}
@@ -152,22 +155,39 @@ export function getAllModelProviders(): Record<string, ProviderId> {
152155

153156
export function getProviderFromModel(model: string): ProviderId {
154157
const normalizedModel = model.toLowerCase()
155-
if (normalizedModel in getAllModelProviders()) {
156-
return getAllModelProviders()[normalizedModel]
157-
}
158158

159-
for (const [providerId, config] of Object.entries(providers)) {
160-
if (config.modelPatterns) {
161-
for (const pattern of config.modelPatterns) {
162-
if (pattern.test(normalizedModel)) {
163-
return providerId as ProviderId
159+
let providerId: ProviderId | null = null
160+
161+
if (normalizedModel in getAllModelProviders()) {
162+
providerId = getAllModelProviders()[normalizedModel]
163+
} else {
164+
for (const [id, config] of Object.entries(providers)) {
165+
if (config.modelPatterns) {
166+
for (const pattern of config.modelPatterns) {
167+
if (pattern.test(normalizedModel)) {
168+
providerId = id as ProviderId
169+
break
170+
}
164171
}
165172
}
173+
if (providerId) break
166174
}
167175
}
168176

169-
logger.warn(`No provider found for model: ${model}, defaulting to ollama`)
170-
return 'ollama'
177+
if (!providerId) {
178+
logger.warn(`No provider found for model: ${model}, defaulting to ollama`)
179+
providerId = 'ollama'
180+
}
181+
182+
if (isProviderBlacklisted(providerId)) {
183+
throw new Error(`Provider "${providerId}" is not available`)
184+
}
185+
186+
if (isModelBlacklisted(normalizedModel)) {
187+
throw new Error(`Model "${model}" is not available`)
188+
}
189+
190+
return providerId
171191
}
172192

173193
export function getProvider(id: string): ProviderMetadata | undefined {
@@ -192,35 +212,42 @@ export function getProviderModels(providerId: ProviderId): string[] {
192212
return getProviderModelsFromDefinitions(providerId)
193213
}
194214

195-
interface ModelBlacklist {
196-
models: string[]
197-
prefixes: string[]
198-
envOverride?: string
215+
function getBlacklistedProviders(): string[] {
216+
if (!env.BLACKLISTED_PROVIDERS) return []
217+
return env.BLACKLISTED_PROVIDERS.split(',').map((p) => p.trim().toLowerCase())
199218
}
200219

201-
const MODEL_BLACKLISTS: ModelBlacklist[] = [
202-
{
203-
models: ['deepseek-chat', 'deepseek-v3', 'deepseek-r1'],
204-
prefixes: ['openrouter/deepseek', 'openrouter/tngtech'],
205-
envOverride: 'DEEPSEEK_MODELS_ENABLED',
206-
},
207-
]
220+
export function isProviderBlacklisted(providerId: string): boolean {
221+
const blacklist = getBlacklistedProviders()
222+
return blacklist.includes(providerId.toLowerCase())
223+
}
224+
225+
/**
226+
* Get the list of blacklisted models from env var.
227+
* BLACKLISTED_MODELS supports:
228+
* - Exact model names: "gpt-4,claude-3-opus"
229+
* - Prefix patterns with *: "claude-*,gpt-4-*" (matches models starting with that prefix)
230+
*/
231+
function getBlacklistedModels(): { models: string[]; prefixes: string[] } {
232+
if (!env.BLACKLISTED_MODELS) return { models: [], prefixes: [] }
233+
234+
const entries = env.BLACKLISTED_MODELS.split(',').map((m) => m.trim().toLowerCase())
235+
const models = entries.filter((e) => !e.endsWith('*'))
236+
const prefixes = entries.filter((e) => e.endsWith('*')).map((e) => e.slice(0, -1))
237+
238+
return { models, prefixes }
239+
}
208240

209241
function isModelBlacklisted(model: string): boolean {
210242
const lowerModel = model.toLowerCase()
243+
const blacklist = getBlacklistedModels()
211244

212-
for (const blacklist of MODEL_BLACKLISTS) {
213-
if (blacklist.envOverride && isTruthy(getEnv(blacklist.envOverride))) {
214-
continue
215-
}
216-
217-
if (blacklist.models.includes(lowerModel)) {
218-
return true
219-
}
245+
if (blacklist.models.includes(lowerModel)) {
246+
return true
247+
}
220248

221-
if (blacklist.prefixes.some((prefix) => lowerModel.startsWith(prefix))) {
222-
return true
223-
}
249+
if (blacklist.prefixes.some((prefix) => lowerModel.startsWith(prefix))) {
250+
return true
224251
}
225252

226253
return false

helm/sim/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ app:
117117
ALLOWED_LOGIN_EMAILS: "" # Comma-separated list of allowed email addresses for login
118118
ALLOWED_LOGIN_DOMAINS: "" # Comma-separated list of allowed email domains for login
119119

120+
# LLM Provider/Model Restrictions (leave empty if not restricting)
121+
BLACKLISTED_PROVIDERS: "" # Comma-separated provider IDs to hide from UI (e.g., "openai,anthropic,google")
122+
BLACKLISTED_MODELS: "" # Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*")
123+
120124
# SSO Configuration (Enterprise Single Sign-On)
121125
# Set to "true" AFTER running the SSO registration script
122126
SSO_ENABLED: "" # Enable SSO authentication ("true" to enable)

0 commit comments

Comments
 (0)