Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/api/ai/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export async function getModel(): Promise<LanguageModel> {
});
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');
Expand Down
32 changes: 32 additions & 0 deletions packages/api/test/litellm-provider.test.mts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions packages/shared/src/ai.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const AiProvider = {
XAI: 'Xai',
Gemini: 'Gemini',
OpenRouter: 'openrouter',
LiteLLM: 'litellm',
Custom: 'custom',
} as const;

Expand All @@ -16,6 +17,7 @@ export const defaultModels: Record<AiProviderType, string> = {
[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 {
Expand Down
49 changes: 49 additions & 0 deletions packages/web/src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ function AiInfoBanner() {
</div>
);

case 'litellm':
return (
<div className="flex items-center gap-10 bg-sb-yellow-20 text-sb-yellow-80 rounded-sm text-sm font-medium px-3 py-2">
<p>LiteLLM proxy URL required</p>
<a href="https://docs.litellm.ai/docs/simple_proxy" target="_blank" className="underline">
LiteLLM docs
</a>
</div>
);

case 'custom':
return (
<div className="flex items-center gap-10 bg-sb-yellow-20 text-sb-yellow-80 rounded-sm text-sm font-medium px-3 py-2">
Expand Down Expand Up @@ -332,6 +342,7 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) {
<SelectItem value="Xai">Xai</SelectItem>
<SelectItem value="Gemini">Gemini</SelectItem>
<SelectItem value="openrouter">openrouter</SelectItem>
<SelectItem value="litellm">litellm</SelectItem>
<SelectItem value="custom">custom</SelectItem>
</SelectContent>
</Select>
Expand Down Expand Up @@ -448,6 +459,44 @@ export function AiSettings({ saveButtonLabel }: AiSettingsProps) {
</div>
)}

{aiProvider === 'litellm' && (
<div>
<p className="opacity-70 text-sm mb-4">
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
&quot;anthropic/claude-3-5-sonnet&quot; or &quot;openai/gpt-4o&quot;.
</p>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Input
name="baseUrl"
placeholder="http://localhost:4000/v1"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
/>
<Input
name="customApiKey"
placeholder="LiteLLM API key"
type="password"
value={customApiKey}
onChange={(e) => setCustomApiKey(e.target.value)}
/>
</div>
<div className="flex justify-end">
<Button
className="px-5"
onClick={() =>
updateConfigContext({ aiBaseUrl: baseUrl, customApiKey, aiModel: model })
}
disabled={!customModelSaveEnabled}
>
{saveButtonLabel ?? 'Save'}
</Button>
</div>
</div>
</div>
)}

{aiProvider === 'custom' && (
<div>
<p className="opacity-70 text-sm mb-4">
Expand Down