Skip to content

Commit 81bf93b

Browse files
feat(litellm): add LiteLLM as AI gateway provider (#4739)
* feat: add LiteLLM as AI gateway provider * fix: add litellm to attachments, provider store, utils, and block guards * fix: add frontend model discovery pipeline for litellm provider Add API route, contract, query hook case, and ProviderModelsLoader entry so litellm models are fetched and synced to the store on workspace load, matching the vllm/ollama/openrouter/fireworks pattern. Also fixes defaultModel to empty string and adds litellm/ prefix early-return in blocks/utils.ts (reviewer feedback). * fix: remove azureEndpoint fallback from LiteLLM provider Copy-paste artifact from vLLM provider. LiteLLM should only use LITELLM_BASE_URL, not fall back to azureEndpoint which could cause requests to be routed to the wrong server. * fix(litellm): close audit gaps from PR #4644 - byok.ts: add litellm branch to getApiKeyWithBYOK so workflow block execution can resolve the proxy key instead of throwing "API key is required for litellm ..." - check-api-validation-contracts.ts: bump route baseline 755 -> 756 to account for the new /api/providers/litellm/models route - .env.example: document LITELLM_BASE_URL / LITELLM_API_KEY - copilot edit-workflow validation: include LiteLLM in the list of user-configured prefixed providers shown to the model - providers/utils.ts: drop stray optional-chain on providers.litellm to match the vllm pattern - lint: apply biome formatting fixes (multi-line if, SVG path, multi-line DYNAMIC_MODEL_PROVIDERS) * fix(litellm): final parity gaps from second audit - blocks/utils.ts getModelOptions(): include litellm models in the combined model dropdown — was previously dropping any proxy-discovered models from the agent block model picker. - get-blocks-metadata-tool.ts mockProvidersState: add litellm bucket so the server-side copilot block-metadata fallback can render model options when the providers store is not initialized. - blocks/utils.test.ts: add litellm to mock providers state (initial + beforeEach reset) and add a parallel store-bucket guard test mirroring the vLLM case. - providers/utils.test.ts: add parallel getApiKey test for litellm. * feat(litellm): use official LiteLLM brand icon and color - icons.tsx: replace the placeholder letterform with the official LiteLLM brand mark embedded as a PNG data URI in an SVG image. - models.ts: set color: #040229 on the litellm provider definition to match the brand background. * chore(litellm): validate /v1/models response with shared schema in initialize() Match the API route handler — both code paths now run the same vllmUpstreamResponseSchema.parse() over the upstream /v1/models JSON instead of a raw type-cast, so malformed upstream payloads surface a descriptive ZodError instead of a downstream TypeError. Addresses Greptile review feedback on PR #4739. --------- Co-authored-by: RheagalFire <arishalam121@gmail.com>
1 parent d62f9ca commit 81bf93b

23 files changed

Lines changed: 899 additions & 8 deletions

File tree

apps/sim/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
4848
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
4949
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
5050
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
51+
# LITELLM_BASE_URL=http://localhost:4000 # Base URL for your LiteLLM proxy (OpenAI-compatible)
52+
# LITELLM_API_KEY= # Optional bearer token if your LiteLLM proxy requires auth
5153
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
5254
# 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.
5355
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import {
5+
providerModelsResponseSchema,
6+
vllmUpstreamResponseSchema,
7+
} from '@/lib/api/contracts/providers'
8+
import { env } from '@/lib/core/config/env'
9+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
11+
12+
const logger = createLogger('LiteLLMModelsAPI')
13+
14+
export const GET = withRouteHandler(async (_request: NextRequest) => {
15+
if (isProviderBlacklisted('litellm')) {
16+
logger.info('LiteLLM provider is blacklisted, returning empty models')
17+
return NextResponse.json({ models: [] })
18+
}
19+
20+
const baseUrl = (env.LITELLM_BASE_URL || '').replace(/\/$/, '')
21+
22+
if (!baseUrl) {
23+
logger.info('LITELLM_BASE_URL not configured')
24+
return NextResponse.json({ models: [] })
25+
}
26+
27+
try {
28+
logger.info('Fetching LiteLLM models', { baseUrl })
29+
30+
const headers: Record<string, string> = {
31+
'Content-Type': 'application/json',
32+
}
33+
34+
if (env.LITELLM_API_KEY) {
35+
headers.Authorization = `Bearer ${env.LITELLM_API_KEY}`
36+
}
37+
38+
const response = await fetch(`${baseUrl}/v1/models`, {
39+
headers,
40+
next: { revalidate: 60 },
41+
})
42+
43+
if (!response.ok) {
44+
logger.warn('LiteLLM service is not available', {
45+
status: response.status,
46+
statusText: response.statusText,
47+
})
48+
return NextResponse.json({ models: [] })
49+
}
50+
51+
const data = vllmUpstreamResponseSchema.parse(await response.json())
52+
const allModels = data.data.map((model) => `litellm/${model.id}`)
53+
const models = filterBlacklistedModels(allModels)
54+
55+
logger.info('Successfully fetched LiteLLM models', {
56+
count: models.length,
57+
filtered: allModels.length - models.length,
58+
models,
59+
})
60+
61+
return NextResponse.json(providerModelsResponseSchema.parse({ models }))
62+
} catch (error) {
63+
logger.error('Failed to fetch LiteLLM models', {
64+
error: getErrorMessage(error, 'Unknown error'),
65+
baseUrl,
66+
})
67+
68+
return NextResponse.json({ models: [] })
69+
}
70+
})

apps/sim/app/workspace/[workspaceId]/providers/provider-models-loader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useParams } from 'next/navigation'
66
import { useProviderModels } from '@/hooks/queries/providers'
77
import {
88
updateFireworksProviderModels,
9+
updateLiteLLMProviderModels,
910
updateOllamaProviderModels,
1011
updateOpenRouterProviderModels,
1112
updateVLLMProviderModels,
@@ -32,6 +33,8 @@ function useSyncProvider(provider: ProviderName, workspaceId?: string) {
3233
updateOllamaProviderModels(data.models)
3334
} else if (provider === 'vllm') {
3435
updateVLLMProviderModels(data.models)
36+
} else if (provider === 'litellm') {
37+
updateLiteLLMProviderModels(data.models)
3538
} else if (provider === 'openrouter') {
3639
void updateOpenRouterProviderModels(data.models)
3740
if (data.modelInfo) {
@@ -61,6 +64,7 @@ export function ProviderModelsLoader() {
6164
useSyncProvider('base')
6265
useSyncProvider('ollama')
6366
useSyncProvider('vllm')
67+
useSyncProvider('litellm')
6468
useSyncProvider('openrouter')
6569
useSyncProvider('fireworks', workspaceId)
6670
return null

apps/sim/blocks/utils.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const { mockProviders } = vi.hoisted(() => ({
2727
base: { models: [] as string[], isLoading: false },
2828
ollama: { models: [] as string[], isLoading: false },
2929
vllm: { models: [] as string[], isLoading: false },
30+
litellm: { models: [] as string[], isLoading: false },
3031
openrouter: { models: [] as string[], isLoading: false },
3132
fireworks: { models: [] as string[], isLoading: false },
3233
},
@@ -101,6 +102,7 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
101102
base: { models: [], isLoading: false },
102103
ollama: { models: [], isLoading: false },
103104
vllm: { models: [], isLoading: false },
105+
litellm: { models: [], isLoading: false },
104106
openrouter: { models: [], isLoading: false },
105107
fireworks: { models: [], isLoading: false },
106108
}
@@ -185,6 +187,11 @@ describe('getApiKeyCondition / shouldRequireApiKeyForModel', () => {
185187
expect(evaluateCondition('my-custom-model')).toBe(false)
186188
})
187189

190+
it('does not require API key when model is in the LiteLLM store bucket', () => {
191+
mockProviders.value.litellm.models = ['litellm/anthropic/claude-sonnet-4-6']
192+
expect(evaluateCondition('litellm/anthropic/claude-sonnet-4-6')).toBe(false)
193+
})
194+
188195
it('requires API key when model is in the fireworks store bucket', () => {
189196
mockProviders.value.fireworks.models = ['fireworks/llama-3']
190197
expect(evaluateCondition('fireworks/llama-3')).toBe(true)

apps/sim/blocks/utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@ export function getModelOptions() {
5151
const baseModels = providersState.providers.base.models
5252
const ollamaModels = providersState.providers.ollama.models
5353
const vllmModels = providersState.providers.vllm.models
54+
const litellmModels = providersState.providers.litellm.models
5455
const openrouterModels = providersState.providers.openrouter.models
5556
const fireworksModels = providersState.providers.fireworks.models
5657
const allModels = Array.from(
5758
new Set([
5859
...baseModels,
5960
...ollamaModels,
6061
...vllmModels,
62+
...litellmModels,
6163
...openrouterModels,
6264
...fireworksModels,
6365
])
@@ -160,12 +162,13 @@ function shouldRequireApiKeyForModel(model: string): boolean {
160162
) {
161163
return false
162164
}
163-
if (normalizedModel.startsWith('vllm/')) {
165+
if (normalizedModel.startsWith('vllm/') || normalizedModel.startsWith('litellm/')) {
164166
return false
165167
}
166168

167169
const storeProvider = getProviderFromStore(normalizedModel)
168-
if (storeProvider === 'ollama' || storeProvider === 'vllm') return false
170+
if (storeProvider === 'ollama' || storeProvider === 'vllm' || storeProvider === 'litellm')
171+
return false
169172
if (storeProvider) return true
170173

171174
if (isOllamaConfigured) {

apps/sim/components/icons.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4439,6 +4439,20 @@ export function VllmIcon(props: SVGProps<SVGSVGElement>) {
44394439
)
44404440
}
44414441

4442+
export function LitellmIcon(props: SVGProps<SVGSVGElement>) {
4443+
return (
4444+
<svg {...props} fill='none' viewBox='0 0 72 72' xmlns='http://www.w3.org/2000/svg'>
4445+
<title>LiteLLM</title>
4446+
<image
4447+
href='data:image/png;base64,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'
4448+
width='72'
4449+
height='72'
4450+
preserveAspectRatio='xMidYMid meet'
4451+
/>
4452+
</svg>
4453+
)
4454+
}
4455+
44424456
export function PosthogIcon(props: SVGProps<SVGSVGElement>) {
44434457
return (
44444458
<svg

apps/sim/hooks/queries/providers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { requestJson } from '@/lib/api/client/request'
55
import {
66
getBaseProviderModelsContract,
77
getFireworksProviderModelsContract,
8+
getLitellmProviderModelsContract,
89
getOllamaProviderModelsContract,
910
getOpenRouterProviderModelsContract,
1011
getVllmProviderModelsContract,
@@ -54,6 +55,8 @@ async function requestProviderModels(
5455
return requestJson(getOllamaProviderModelsContract, { signal })
5556
case 'vllm':
5657
return requestJson(getVllmProviderModelsContract, { signal })
58+
case 'litellm':
59+
return requestJson(getLitellmProviderModelsContract, { signal })
5760
case 'openrouter':
5861
return requestJson(getOpenRouterProviderModelsContract, { signal })
5962
case 'fireworks':

apps/sim/lib/api-key/byok.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ export async function getApiKeyWithBYOK(
7474
return { apiKey: userProvidedKey || env.VLLM_API_KEY || 'empty', isBYOK: false }
7575
}
7676

77+
const isLitellmModel =
78+
provider === 'litellm' || useProvidersStore.getState().providers.litellm.models.includes(model)
79+
if (isLitellmModel) {
80+
return { apiKey: userProvidedKey || env.LITELLM_API_KEY || 'empty', isBYOK: false }
81+
}
82+
7783
const isFireworksModel =
7884
provider === 'fireworks' ||
7985
useProvidersStore.getState().providers.fireworks.models.includes(model)

apps/sim/lib/api/contracts/providers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ export const getOpenRouterProviderModelsContract = defineRouteContract({
207207
},
208208
})
209209

210+
export const getLitellmProviderModelsContract = defineRouteContract({
211+
method: 'GET',
212+
path: '/api/providers/litellm/models',
213+
response: {
214+
mode: 'json',
215+
schema: providerModelsResponseSchema,
216+
},
217+
})
218+
210219
export const getFireworksProviderModelsContract = defineRouteContract({
211220
method: 'GET',
212221
path: '/api/providers/fireworks/models',

apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ function callOptionsWithFallback(
768768
base: { models: staticModels.map((m) => m.id) },
769769
ollama: { models: [] },
770770
vllm: { models: [] },
771+
litellm: { models: [] },
771772
openrouter: { models: [] },
772773
fireworks: { models: [] },
773774
},

0 commit comments

Comments
 (0)