import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' async function requireAdmin() { const session = await auth() if (!session?.user?.id || (session.user as any).role !== 'ADMIN') return null return session } /** * GET /api/admin/models?type=ollama&url= * GET /api/admin/models?type=custom&url=&key=&kind=tags|embeddings * GET /api/admin/models?type=deepseek&key=&kind=tags|embeddings * GET /api/admin/models?type=openrouter&key=&kind=tags|embeddings * GET /api/admin/models?type=mistral&key=&kind=tags|embeddings * GET /api/admin/models?type=zai&key=&kind=tags|embeddings * GET /api/admin/models?type=lmstudio&url= * * Route API for fetching AI model lists from providers. */ export async function GET(request: NextRequest) { if (!(await requireAdmin())) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) } const { searchParams } = request.nextUrl const type = searchParams.get('type') const rawUrl = searchParams.get('url') ?? '' const apiKey = searchParams.get('key') ?? undefined const kind = searchParams.get('kind') ?? 'tags' // Provider-specific base URLs (used when url param is empty) const PROVIDER_URLS: Record = { deepseek: 'https://api.deepseek.com/v1', openrouter: 'https://openrouter.ai/api/v1', mistral: 'https://api.mistral.ai/v1', zai: 'https://api.zukijourney.com/v1', lmstudio: 'http://localhost:1234/v1', } try { // Ollama: uses native /api/tags endpoint if (type === 'ollama') { if (!rawUrl) { return NextResponse.json({ success: false, models: [], error: 'url parameter is required for Ollama' }) } const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/api$/, '') const res = await fetch(`${baseUrl}/api/tags`, { headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(5000), }) if (!res.ok) { return NextResponse.json({ success: false, models: [], error: `Ollama ${res.status}` }) } const data = await res.json() const models: string[] = (data.models ?? []).map((m: { name: string }) => m.name) return NextResponse.json({ success: true, models }) } // All other providers: use OpenAI-compatible /v1/models endpoint const baseUrl = rawUrl ? rawUrl.replace(/\/$/, '') : (PROVIDER_URLS[type || ''] || '') if (!baseUrl) { return NextResponse.json({ success: false, models: [], error: 'url parameter is required' }) } const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['Authorization'] = `Bearer ${apiKey}` // For OpenRouter, add required headers if (type === 'openrouter') { headers['HTTP-Referer'] = 'https://localhost:3000' headers['X-Title'] = 'Memento AI' } // Try provider-specific embeddings endpoint first for embeddings kind if (kind === 'embeddings') { try { const embRes = await fetch(`${baseUrl}/embeddings/models`, { headers, signal: AbortSignal.timeout(8000), }) if (embRes.ok) { const embData = await embRes.json() const embModels: string[] = (embData.data ?? []) .map((m: { id: string }) => m.id) .filter(Boolean) .sort() if (embModels.length > 0) { return NextResponse.json({ success: true, models: embModels }) } } } catch { // Fall through to /v1/models with keyword filter } } const res = await fetch(`${baseUrl}/models`, { headers, signal: AbortSignal.timeout(8000), }) if (!res.ok) { return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` }) } const data = await res.json() let models: string[] = (data.data ?? []) .map((m: { id: string }) => m.id) .filter(Boolean) .sort() if (kind === 'embeddings') { const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm'] const filtered = models.filter((id) => keywords.some((kw) => id.toLowerCase().includes(kw)) ) if (filtered.length > 0) models = filtered } return NextResponse.json({ success: true, models }) } catch (err: any) { return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' }) } }