feat: add OpenRouter as an AI provider (AIIA-86)
All checks were successful
Deploy to VPS / deploy (push) Successful in 41s
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:
2
drizzle/0004_add_openrouter_provider.sql
Normal file
2
drizzle/0004_add_openrouter_provider.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Add 'openrouter' to the api_key_provider enum
|
||||
ALTER TYPE "api_key_provider" ADD VALUE IF NOT EXISTS 'openrouter';
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -977,6 +977,7 @@ export const documents = pgTable(
|
||||
export const apiKeyProviderEnum = pgEnum("api_key_provider", [
|
||||
"anthropic",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"ollama",
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user