Skip to content
2 changes: 2 additions & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
# LITELLM_BASE_URL=http://localhost:4000 # Base URL for your LiteLLM proxy (OpenAI-compatible)
# LITELLM_API_KEY= # Optional bearer token if your LiteLLM proxy requires auth
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
Expand Down
70 changes: 70 additions & 0 deletions apps/sim/app/api/providers/litellm/models/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import {
providerModelsResponseSchema,
vllmUpstreamResponseSchema,
} from '@/lib/api/contracts/providers'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'

const logger = createLogger('LiteLLMModelsAPI')

export const GET = withRouteHandler(async (_request: NextRequest) => {
if (isProviderBlacklisted('litellm')) {
logger.info('LiteLLM provider is blacklisted, returning empty models')
return NextResponse.json({ models: [] })
}

const baseUrl = (env.LITELLM_BASE_URL || '').replace(/\/$/, '')

if (!baseUrl) {
logger.info('LITELLM_BASE_URL not configured')
return NextResponse.json({ models: [] })
}

try {
logger.info('Fetching LiteLLM models', { baseUrl })

const headers: Record<string, string> = {
'Content-Type': 'application/json',
}

if (env.LITELLM_API_KEY) {
headers.Authorization = `Bearer ${env.LITELLM_API_KEY}`
}

const response = await fetch(`${baseUrl}/v1/models`, {
headers,
next: { revalidate: 60 },
})

if (!response.ok) {
logger.warn('LiteLLM service is not available', {
status: response.status,
statusText: response.statusText,
})
return NextResponse.json({ models: [] })
}

const data = vllmUpstreamResponseSchema.parse(await response.json())
const allModels = data.data.map((model) => `litellm/${model.id}`)
const models = filterBlacklistedModels(allModels)

logger.info('Successfully fetched LiteLLM models', {
count: models.length,
filtered: allModels.length - models.length,
models,
})

return NextResponse.json(providerModelsResponseSchema.parse({ models }))
} catch (error) {
logger.error('Failed to fetch LiteLLM models', {
error: getErrorMessage(error, 'Unknown error'),
baseUrl,
})

return NextResponse.json({ models: [] })
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useParams } from 'next/navigation'
import { useProviderModels } from '@/hooks/queries/providers'
import {
updateFireworksProviderModels,
updateLiteLLMProviderModels,
updateOllamaProviderModels,
updateOpenRouterProviderModels,
updateVLLMProviderModels,
Expand All @@ -32,6 +33,8 @@ function useSyncProvider(provider: ProviderName, workspaceId?: string) {
updateOllamaProviderModels(data.models)
} else if (provider === 'vllm') {
updateVLLMProviderModels(data.models)
} else if (provider === 'litellm') {
updateLiteLLMProviderModels(data.models)
} else if (provider === 'openrouter') {
void updateOpenRouterProviderModels(data.models)
if (data.modelInfo) {
Expand Down Expand Up @@ -61,6 +64,7 @@ export function ProviderModelsLoader() {
useSyncProvider('base')
useSyncProvider('ollama')
useSyncProvider('vllm')
useSyncProvider('litellm')
useSyncProvider('openrouter')
useSyncProvider('fireworks', workspaceId)
return null
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/blocks/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const { mockProviders } = vi.hoisted(() => ({
base: { models: [] as string[], isLoading: false },
ollama: { models: [] as string[], isLoading: false },
vllm: { models: [] as string[], isLoading: false },
litellm: { models: [] as string[], isLoading: false },
openrouter: { models: [] as string[], isLoading: false },
fireworks: { models: [] as string[], isLoading: false },
},
Expand Down Expand Up @@ -101,6 +102,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
base: { models: [], isLoading: false },
ollama: { models: [], isLoading: false },
vllm: { models: [], isLoading: false },
litellm: { models: [], isLoading: false },
openrouter: { models: [], isLoading: false },
fireworks: { models: [], isLoading: false },
}
Expand Down Expand Up @@ -185,6 +187,11 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
expect(evaluateCondition('my-custom-model')).toBe(false)
})

it('does not require API key when model is in the LiteLLM store bucket', () => {
mockProviders.value.litellm.models = ['litellm/anthropic/claude-sonnet-4-6']
expect(evaluateCondition('litellm/anthropic/claude-sonnet-4-6')).toBe(false)
})

it('requires API key when model is in the fireworks store bucket', () => {
mockProviders.value.fireworks.models = ['fireworks/llama-3']
expect(evaluateCondition('fireworks/llama-3')).toBe(true)
Expand Down
7 changes: 5 additions & 2 deletions apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ export function getModelOptions() {
const baseModels = providersState.providers.base.models
const ollamaModels = providersState.providers.ollama.models
const vllmModels = providersState.providers.vllm.models
const litellmModels = providersState.providers.litellm.models
const openrouterModels = providersState.providers.openrouter.models
const fireworksModels = providersState.providers.fireworks.models
const allModels = Array.from(
new Set([
...baseModels,
...ollamaModels,
...vllmModels,
...litellmModels,
...openrouterModels,
...fireworksModels,
])
Expand Down Expand Up @@ -160,12 +162,13 @@ function shouldRequireApiKeyForModel(model: string): boolean {
) {
return false
}
if (normalizedModel.startsWith('vllm/')) {
if (normalizedModel.startsWith('vllm/') || normalizedModel.startsWith('litellm/')) {
return false
}

const storeProvider = getProviderFromStore(normalizedModel)
if (storeProvider === 'ollama' || storeProvider === 'vllm') return false
if (storeProvider === 'ollama' || storeProvider === 'vllm' || storeProvider === 'litellm')
return false
if (storeProvider) return true

if (isOllamaConfigured) {
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4439,6 +4439,20 @@ export function VllmIcon(props: SVGProps<SVGSVGElement>) {
)
}

export function LitellmIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} fill='none' viewBox='0 0 72 72' xmlns='http://www.w3.org/2000/svg'>
<title>LiteLLM</title>
<image
href='data:image/png;base64,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'
width='72'
height='72'
preserveAspectRatio='xMidYMid meet'
/>
</svg>
)
}
Comment thread
waleedlatif1 marked this conversation as resolved.

export function PosthogIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/hooks/queries/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { requestJson } from '@/lib/api/client/request'
import {
getBaseProviderModelsContract,
getFireworksProviderModelsContract,
getLitellmProviderModelsContract,
getOllamaProviderModelsContract,
getOpenRouterProviderModelsContract,
getVllmProviderModelsContract,
Expand Down Expand Up @@ -54,6 +55,8 @@ async function requestProviderModels(
return requestJson(getOllamaProviderModelsContract, { signal })
case 'vllm':
return requestJson(getVllmProviderModelsContract, { signal })
case 'litellm':
return requestJson(getLitellmProviderModelsContract, { signal })
case 'openrouter':
return requestJson(getOpenRouterProviderModelsContract, { signal })
case 'fireworks':
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/lib/api-key/byok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export async function getApiKeyWithBYOK(
return { apiKey: userProvidedKey || env.VLLM_API_KEY || 'empty', isBYOK: false }
}

const isLitellmModel =
provider === 'litellm' || useProvidersStore.getState().providers.litellm.models.includes(model)
if (isLitellmModel) {
return { apiKey: userProvidedKey || env.LITELLM_API_KEY || 'empty', isBYOK: false }
}

const isFireworksModel =
provider === 'fireworks' ||
useProvidersStore.getState().providers.fireworks.models.includes(model)
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/lib/api/contracts/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ export const getOpenRouterProviderModelsContract = defineRouteContract({
},
})

export const getLitellmProviderModelsContract = defineRouteContract({
method: 'GET',
path: '/api/providers/litellm/models',
response: {
mode: 'json',
schema: providerModelsResponseSchema,
},
})

export const getFireworksProviderModelsContract = defineRouteContract({
method: 'GET',
path: '/api/providers/fireworks/models',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ function callOptionsWithFallback(
base: { models: staticModels.map((m) => m.id) },
ollama: { models: [] },
vllm: { models: [] },
litellm: { models: [] },
openrouter: { models: [] },
fireworks: { models: [] },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export function validateValueForSubBlockType(
blockType,
field: fieldName,
value,
error: `Unknown model id "${trimmed}" for block "${blockType}". Read components/blocks/${blockType}.json (the model.options array) for valid ids; prefer entries with recommended: true and avoid deprecated: true. For user-configured models (Ollama, vLLM, OpenRouter, Fireworks), prefix the id with the provider slash, e.g. "ollama/llama3.1:8b".${suggestionText}`,
error: `Unknown model id "${trimmed}" for block "${blockType}". Read components/blocks/${blockType}.json (the model.options array) for valid ids; prefer entries with recommended: true and avoid deprecated: true. For user-configured models (Ollama, vLLM, LiteLLM, OpenRouter, Fireworks), prefix the id with the provider slash, e.g. "ollama/llama3.1:8b".${suggestionText}`,
},
}
}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export const env = createEnv({
OLLAMA_URL: z.string().url().optional(), // Ollama local LLM server URL
VLLM_BASE_URL: z.string().url().optional(), // vLLM self-hosted base URL (OpenAI-compatible)
VLLM_API_KEY: z.string().optional(), // Optional bearer token for vLLM
LITELLM_BASE_URL: z.string().url().optional(), // LiteLLM proxy base URL (OpenAI-compatible)
LITELLM_API_KEY: z.string().optional(), // Optional bearer token for LiteLLM
FIREWORKS_API_KEY: z.string().optional(), // Optional Fireworks AI API key for model listing
COHERE_API_KEY: z.string().min(1).optional(), // Cohere API key for reranker (rerank-v4.0-pro, rerank-v4.0-fast, rerank-v3.5)
COHERE_API_KEY_1: z.string().min(1).optional(), // Primary Cohere API key for rotation
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/providers/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type AttachmentProvider =
| 'fireworks'
| 'ollama'
| 'vllm'
| 'litellm'
| 'xai'
| 'deepseek'
| 'cerebras'
Expand Down Expand Up @@ -93,6 +94,7 @@ const PROVIDER_SUPPORTED_LABELS: Record<AttachmentProvider, string> = {
fireworks: 'images through image_url message parts on vision models',
ollama: 'images through image_url message parts on vision models',
vllm: 'images through image_url message parts on multimodal models',
litellm: 'images through image_url message parts on multimodal models',
xai: 'images through image_url message parts on Grok vision models',
deepseek: 'no file attachments in the current API adapter',
cerebras: 'no file attachments in the current API adapter',
Expand All @@ -109,6 +111,7 @@ export function getAttachmentProvider(providerId: ProviderId | string): Attachme
if (providerId === 'fireworks') return 'fireworks'
if (providerId === 'ollama') return 'ollama'
if (providerId === 'vllm') return 'vllm'
if (providerId === 'litellm') return 'litellm'
if (providerId === 'xai') return 'xai'
if (providerId === 'deepseek') return 'deepseek'
if (providerId === 'cerebras') return 'cerebras'
Expand Down Expand Up @@ -247,6 +250,7 @@ function isMimeTypeSupportedByProvider(
case 'fireworks':
case 'ollama':
case 'vllm':
case 'litellm':
case 'xai':
return isImageMimeType(mimeType)
case 'deepseek':
Expand Down
Loading
Loading