import { NextRequest, NextResponse } from 'next/server'; import { isAuthorized } from '@/lib/auth/admin'; import { prisma } from '@/lib/db'; import { getLlmSettings, upsertSetting, SETTING_KEYS, } from '@/lib/llm'; import type { LlmProviderId } from '@/lib/settings'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; const PROVIDERS: LlmProviderId[] = ['openai', 'anthropic', 'ollama']; const PRO_MODEL_KEYS: Record = { openai: 'llm.model.openai', anthropic: 'llm.model.anthropic', ollama: 'llm.model.ollama', }; /** Last-4 for display only — full secrets never leave the server. */ function last4(secret: string): string { return secret.length >= 4 ? `…${secret.slice(-4)}` : ''; } function isProvider(v: unknown): v is LlmProviderId { return v === 'openai' || v === 'anthropic' || v === 'ollama'; } async function readSetting(key: string): Promise { const row = await prisma.setting.findUnique({ where: { key } }); return row?.value ?? null; } interface SettingsBody { provider?: unknown; model?: unknown; // legacy single-model field models?: Partial>; apiKeys?: { openai?: unknown; anthropic?: unknown }; ollamaBase?: unknown; synthesisPrompt?: unknown; } async function toResponseBody() { const s = await getLlmSettings(); const [dbProvider, dbModel, dbOpenai, dbAnthropic] = await Promise.all([ readSetting(SETTING_KEYS.provider), readSetting(SETTING_KEYS.model), readSetting(SETTING_KEYS.openaiKey), readSetting(SETTING_KEYS.anthropicKey), ]); const models = {} as Record; for (const p of PROVIDERS) models[p] = (await readSetting(PRO_MODEL_KEYS[p])) ?? s.model; // A legacy single-model override applies to the provider it was set for. const legacyOwner = isProvider(dbProvider) ? dbProvider : 'openai'; if (dbModel) models[legacyOwner] = dbModel; return { settings: { provider: s.provider, models, apiKeys: { openai: '', anthropic: '' }, // secrets are never echoed ollamaBase: s.ollamaBaseUrl, synthesisPrompt: s.synthesisPrompt, }, keySources: { openai: Boolean(dbOpenai), anthropic: Boolean(dbAnthropic), ollama: true, }, keys: { openaiLast4: last4(s.openaiApiKey), anthropicLast4: last4(s.anthropicApiKey), }, }; } export async function GET(req: NextRequest) { if (!isAuthorized(req)) { return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); } return NextResponse.json(await toResponseBody()); } export async function PUT(req: NextRequest) { if (!isAuthorized(req)) { return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); } const body = (await req.json().catch(() => null)) as SettingsBody | null; if (!body) return NextResponse.json({ error: 'invalid json' }, { status: 400 }); // Provider if (body.provider !== undefined) { if (!isProvider(body.provider)) { return NextResponse.json( { error: 'provider must be openai | anthropic | ollama' }, { status: 400 }, ); } await upsertSetting(SETTING_KEYS.provider, body.provider); } const active = (body.provider as LlmProviderId | undefined) ?? (await getLlmSettings()).provider; // Models — per-provider, with single-model fallback for simple clients if (body.models && typeof body.models === 'object') { for (const p of PROVIDERS) { const m = String(body.models[p] ?? '').trim(); if (m) { await upsertSetting(PRO_MODEL_KEYS[p], m); if (p === active) await upsertSetting(SETTING_KEYS.model, m); } } } else if (typeof body.model === 'string' && body.model.trim()) { await upsertSetting(SETTING_KEYS.model, body.model.trim()); await upsertSetting(PRO_MODEL_KEYS[active], body.model.trim()); } // API keys (secrets; blank from the UI means "keep using the env var") if (body.apiKeys) { const openaiKey = String(body.apiKeys.openai ?? '').trim(); const anthropicKey = String(body.apiKeys.anthropic ?? '').trim(); if (openaiKey) await upsertSetting(SETTING_KEYS.openaiKey, openaiKey, true); if (anthropicKey) await upsertSetting(SETTING_KEYS.anthropicKey, anthropicKey, true); } // Ollama base URL const ollamaBase = String(body.ollamaBase ?? '').trim(); if (ollamaBase) await upsertSetting(SETTING_KEYS.ollamaBase, ollamaBase); // Synthesis prompt const prompt = String(body.synthesisPrompt ?? '').trim(); if (prompt) await upsertSetting(SETTING_KEYS.synthesisPrompt, prompt); return NextResponse.json(await toResponseBody()); }