From 17afd30d4fea483b341af1f95a22b68a19782155 Mon Sep 17 00:00:00 2001 From: RheagalFire Date: Sat, 23 May 2026 02:28:48 +0530 Subject: [PATCH 1/2] feat: add LiteLLM as AI gateway provider --- packages/api/ai/config.mts | 7 ++++ packages/shared/src/ai.mts | 2 ++ packages/web/src/routes/settings.tsx | 49 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/packages/api/ai/config.mts b/packages/api/ai/config.mts index 25548652..dc6e1a6f 100644 --- a/packages/api/ai/config.mts +++ b/packages/api/ai/config.mts @@ -57,6 +57,13 @@ export async function getModel(): Promise { }); return openrouter.chat(model); + case 'litellm': + const litellm = createOpenAI({ + apiKey: config.customApiKey || 'sk-1234', + baseURL: aiBaseUrl || 'http://localhost:4000/v1', + }); + return litellm.chat(model); + case 'custom': if (typeof aiBaseUrl !== 'string') { throw new Error('Local AI base URL is not set'); diff --git a/packages/shared/src/ai.mts b/packages/shared/src/ai.mts index 52db8df4..d93b46b9 100644 --- a/packages/shared/src/ai.mts +++ b/packages/shared/src/ai.mts @@ -4,6 +4,7 @@ export const AiProvider = { XAI: 'Xai', Gemini: 'Gemini', OpenRouter: 'openrouter', + LiteLLM: 'litellm', Custom: 'custom', } as const; @@ -16,6 +17,7 @@ export const defaultModels: Record = { [AiProvider.XAI]: 'grok-beta', [AiProvider.Gemini]: 'gemini-1.5-pro-latest', [AiProvider.OpenRouter]: 'anthropic/claude-3-opus-20240229', + [AiProvider.LiteLLM]: 'gpt-4o-mini', } as const; export function isValidProvider(provider: string): provider is AiProviderType { diff --git a/packages/web/src/routes/settings.tsx b/packages/web/src/routes/settings.tsx index 6277592a..d05a8310 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -176,6 +176,16 @@ function AiInfoBanner() { ); + case 'litellm': + return ( +
+

LiteLLM proxy URL required

+ + LiteLLM docs + +
+ ); + case 'custom': return (
@@ -332,6 +342,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) { Xai Gemini openrouter + litellm custom @@ -448,6 +459,44 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) {
)} + {aiProvider === 'litellm' && ( +
+

+ LiteLLM is an AI gateway that routes to 100+ LLM providers through a single proxy. + Enter your proxy URL and API key below. Use LiteLLM model IDs like + "anthropic/claude-3-5-sonnet" or "openai/gpt-4o". +

+
+
+ setBaseUrl(e.target.value)} + /> + setCustomApiKey(e.target.value)} + /> +
+
+ +
+
+
+ )} + {aiProvider === 'custom' && (

From 1b9b65fa75fd7da50dea0a33b218586bf57806b8 Mon Sep 17 00:00:00 2001 From: RheagalFire Date: Sat, 23 May 2026 02:32:25 +0530 Subject: [PATCH 2/2] test: add unit tests for LiteLLM provider --- packages/api/test/litellm-provider.test.mts | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/api/test/litellm-provider.test.mts diff --git a/packages/api/test/litellm-provider.test.mts b/packages/api/test/litellm-provider.test.mts new file mode 100644 index 00000000..82cd2ab3 --- /dev/null +++ b/packages/api/test/litellm-provider.test.mts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { AiProvider, defaultModels, isValidProvider, getDefaultModel } from '../../shared/src/ai.mjs'; + +describe('LiteLLM provider registration', () => { + it('should include litellm in AiProvider enum', () => { + expect(AiProvider.LiteLLM).toBe('litellm'); + }); + + it('should have a default model for litellm', () => { + expect(defaultModels[AiProvider.LiteLLM]).toBeDefined(); + expect(defaultModels[AiProvider.LiteLLM]).toBe('gpt-4o-mini'); + }); + + it('should validate litellm as a valid provider', () => { + expect(isValidProvider('litellm')).toBe(true); + }); + + it('should return default model for litellm', () => { + expect(getDefaultModel('litellm')).toBe('gpt-4o-mini'); + }); + + it('should not break existing providers', () => { + expect(isValidProvider('openai')).toBe(true); + expect(isValidProvider('anthropic')).toBe(true); + expect(isValidProvider('custom')).toBe(true); + expect(isValidProvider('openrouter')).toBe(true); + }); + + it('should reject invalid providers', () => { + expect(isValidProvider('nonexistent')).toBe(false); + }); +});