diff --git a/drizzle/0004_add_openrouter_provider.sql b/drizzle/0004_add_openrouter_provider.sql new file mode 100644 index 0000000..6f39c94 --- /dev/null +++ b/drizzle/0004_add_openrouter_provider.sql @@ -0,0 +1,2 @@ +-- Add 'openrouter' to the api_key_provider enum +ALTER TYPE "api_key_provider" ADD VALUE IF NOT EXISTS 'openrouter'; diff --git a/src/app/(dashboard)/einstellungen/ai-settings.tsx b/src/app/(dashboard)/einstellungen/ai-settings.tsx index d8e9167..c065760 100644 --- a/src/app/(dashboard)/einstellungen/ai-settings.tsx +++ b/src/app/(dashboard)/einstellungen/ai-settings.tsx @@ -91,6 +91,7 @@ export default function AISettingsForm() { > + @@ -103,7 +104,7 @@ export default function AISettingsForm() { type="text" value={settings.model} onChange={(e) => setSettings({ ...settings, model: e.target.value })} - placeholder={settings.provider === 'anthropic' ? 'claude-sonnet-4-20250514' : 'gpt-4o'} + placeholder={settings.provider === 'anthropic' ? 'claude-sonnet-4-20250514' : settings.provider === 'openrouter' ? 'anthropic/claude-sonnet-4' : 'gpt-4o'} className="w-full rounded-lg border border-card-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted/50" /> @@ -111,7 +112,7 @@ export default function AISettingsForm() { const activeKey = apiKeys.find( (k) => k.provider === settings.provider && k.isActive, ); - const providerLabel = settings.provider === 'anthropic' ? 'Anthropic' : 'OpenAI'; + const providerLabel = settings.provider === 'anthropic' ? 'Anthropic' : settings.provider === 'openrouter' ? 'OpenRouter' : 'OpenAI'; async function handleSaveApiKey() { if (!apiKeyInput || apiKeyInput.length < 8) { diff --git a/src/app/(dashboard)/einstellungen/api-key-settings.tsx b/src/app/(dashboard)/einstellungen/api-key-settings.tsx index 00ce142..300687c 100644 --- a/src/app/(dashboard)/einstellungen/api-key-settings.tsx +++ b/src/app/(dashboard)/einstellungen/api-key-settings.tsx @@ -14,6 +14,7 @@ interface ApiKeyEntry { const PROVIDER_LABELS: Record = { anthropic: 'Anthropic', openai: 'OpenAI', + openrouter: 'OpenRouter', ollama: 'Ollama', }; @@ -172,6 +173,7 @@ export default function ApiKeySettings() { > +
diff --git a/src/app/api/settings/ai/route.ts b/src/app/api/settings/ai/route.ts index 12c5fa3..ac27c46 100644 --- a/src/app/api/settings/ai/route.ts +++ b/src/app/api/settings/ai/route.ts @@ -7,7 +7,7 @@ import { eq } from 'drizzle-orm'; import { requirePermission } from '@/lib/auth/rbac'; import type { AIProvider } from '@/lib/ai/providers'; -const VALID_PROVIDERS = new Set(['anthropic', 'openai', 'ollama']); +const VALID_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'ollama']); export async function GET() { const auth = await requirePermission('settings:manage'); diff --git a/src/app/api/settings/api-keys/route.ts b/src/app/api/settings/api-keys/route.ts index 54533f2..9b80da1 100644 --- a/src/app/api/settings/api-keys/route.ts +++ b/src/app/api/settings/api-keys/route.ts @@ -9,7 +9,7 @@ import { encrypt, keyHint } from '@/lib/crypto'; import { logAuditEvent } from '@/lib/auth/audit'; import type { AIProvider } from '@/lib/ai/providers'; -const VALID_PROVIDERS = new Set(['anthropic', 'openai', 'ollama']); +const VALID_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'ollama']); export async function GET() { const auth = await requirePermission('settings:manage'); diff --git a/src/lib/ai/providers/index.ts b/src/lib/ai/providers/index.ts index 85e49f9..036f2d9 100644 --- a/src/lib/ai/providers/index.ts +++ b/src/lib/ai/providers/index.ts @@ -1,5 +1,5 @@ // AI Provider abstraction via Vercel AI SDK v6 -// Supports: Anthropic, OpenAI, Ollama — selected via AI_PROVIDER env var or tenant settings +// Supports: Anthropic, OpenAI, OpenRouter, Ollama — selected via AI_PROVIDER env var or tenant settings // Per-tenant API keys are loaded from tenant_api_keys (AES-256-GCM encrypted) import { createAnthropic, anthropic } from '@ai-sdk/anthropic'; @@ -10,17 +10,19 @@ import { tenants, tenantApiKeys } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; import { decrypt } from '@/lib/crypto'; -export type AIProvider = 'anthropic' | 'openai' | 'ollama'; +export type AIProvider = 'anthropic' | 'openai' | 'openrouter' | 'ollama'; const DEFAULT_MODELS: Record = { anthropic: 'claude-sonnet-4-20250514', openai: 'gpt-4o', + openrouter: 'anthropic/claude-sonnet-4', ollama: 'llama3', }; export function getProvider(): AIProvider { const p = process.env.AI_PROVIDER; if (p === 'openai') return 'openai'; + if (p === 'openrouter') return 'openrouter'; if (p === 'ollama') return 'ollama'; return 'anthropic'; } @@ -45,6 +47,13 @@ function buildModel( } return openai(modelId); } + case 'openrouter': { + const openrouter = createOpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: options?.apiKey ?? process.env.OPENROUTER_API_KEY ?? '', + }); + return openrouter(modelId); + } case 'ollama': { const baseURL = options?.ollamaUrl ?? process.env.OLLAMA_URL ?? 'http://localhost:11434'; const ollama = createOpenAI({ @@ -97,7 +106,7 @@ export async function getModelForTenant(tenantId: string): Promise<{ model: Lang .limit(1); const s = tenant?.settings as Record | undefined; - const provider: AIProvider = (['anthropic', 'openai', 'ollama'].includes(s?.aiProvider ?? '') ? s!.aiProvider : getProvider()) as AIProvider; + const provider: AIProvider = (['anthropic', 'openai', 'openrouter', 'ollama'].includes(s?.aiProvider ?? '') ? s!.aiProvider : getProvider()) as AIProvider; let modelId: string; if (provider === 'ollama') { diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index c7c6b5d..ed22700 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -977,6 +977,7 @@ export const documents = pgTable( export const apiKeyProviderEnum = pgEnum("api_key_provider", [ "anthropic", "openai", + "openrouter", "ollama", ]);