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 * * Route API (not a Server Action) for fetching AI model lists from Ollama or * OpenAI-compatible providers. Using a Route Handler instead of a Server Action * is the correct architecture for client-side GET requests: Server Actions are * for data mutations, and calling them from useEffect pushes items into the * App Router's internal action queue, which is drained during render (inside * AppRouter's useMemo), triggering React Error #310 when multiple calls stack up. */ 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' if (!rawUrl) { return NextResponse.json({ success: false, models: [], error: 'url parameter is required' }) } const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/v1$/, '') try { if (type === 'ollama') { 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 }) } if (type === 'custom') { const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['Authorization'] = `Bearer ${apiKey}` if (kind === 'embeddings') { // Try provider-specific embeddings endpoint first (e.g. OpenRouter) try { const embRes = await fetch(`${baseUrl}/v1/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}/v1/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 }) } return NextResponse.json({ success: false, models: [], error: `Unknown type: ${type}` }) } catch (err: any) { return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' }) } }