feat: add OpenRouter as an AI provider (AIIA-86)
All checks were successful
Deploy to VPS / deploy (push) Successful in 41s

Integrate OpenRouter via its OpenAI-compatible API so users can select
and use OpenRouter models alongside existing Anthropic/OpenAI/Ollama
providers. Adds provider to type system, DB enum, API validation,
buildModel switch, and settings UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO (LegalAI)
2026-04-12 20:50:02 +00:00
parent e60b27cbd4
commit 27132aa383
7 changed files with 22 additions and 7 deletions

View File

@@ -0,0 +1,2 @@
-- Add 'openrouter' to the api_key_provider enum
ALTER TYPE "api_key_provider" ADD VALUE IF NOT EXISTS 'openrouter';

View File

@@ -91,6 +91,7 @@ export default function AISettingsForm() {
>
<option value="anthropic">Anthropic (Claude)</option>
<option value="openai">OpenAI (GPT)</option>
<option value="openrouter">OpenRouter</option>
<option value="ollama">Ollama (Lokal)</option>
</select>
</div>
@@ -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"
/>
</div>
@@ -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) {

View File

@@ -14,6 +14,7 @@ interface ApiKeyEntry {
const PROVIDER_LABELS: Record<string, string> = {
anthropic: 'Anthropic',
openai: 'OpenAI',
openrouter: 'OpenRouter',
ollama: 'Ollama',
};
@@ -172,6 +173,7 @@ export default function ApiKeySettings() {
>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
</select>
</div>
<div>

View File

@@ -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<AIProvider>(['anthropic', 'openai', 'ollama']);
const VALID_PROVIDERS = new Set<AIProvider>(['anthropic', 'openai', 'openrouter', 'ollama']);
export async function GET() {
const auth = await requirePermission('settings:manage');

View File

@@ -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<AIProvider>(['anthropic', 'openai', 'ollama']);
const VALID_PROVIDERS = new Set<AIProvider>(['anthropic', 'openai', 'openrouter', 'ollama']);
export async function GET() {
const auth = await requirePermission('settings:manage');

View File

@@ -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<AIProvider, string> = {
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<string, string> | 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') {

View File

@@ -977,6 +977,7 @@ export const documents = pgTable(
export const apiKeyProviderEnum = pgEnum("api_key_provider", [
"anthropic",
"openai",
"openrouter",
"ollama",
]);