Files
professor/app/api/models/route.ts
Aodhan Collins f644937604 Initial commit — AI-powered coding tutor (Professor)
Next.js 16, React 19, Monaco editor, Anthropic SDK, multi-provider AI,
Wandbox Python execution, iframe HTML preview, SQLite auth + session persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 21:48:34 +00:00

85 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import type { ProviderId } from '@/types';
import { PROVIDER_MAP } from '@/lib/providers';
interface ModelsRequestBody {
provider: ProviderId;
apiKey?: string;
baseUrl?: string;
}
export async function POST(req: NextRequest) {
let body: ModelsRequestBody;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
const { provider, apiKey, baseUrl } = body;
const def = PROVIDER_MAP[provider];
const base = baseUrl?.trim() || def.defaultBaseUrl;
try {
let models: string[] = [];
if (provider === 'anthropic') {
const key = apiKey?.trim() || process.env.ANTHROPIC_API_KEY;
if (!key) {
return NextResponse.json({ error: 'No API key — enter one above or set ANTHROPIC_API_KEY in .env.local' }, { status: 400 });
}
const res = await fetch('https://api.anthropic.com/v1/models?limit=100', {
headers: {
'x-api-key': key,
'anthropic-version': '2023-06-01',
},
});
if (!res.ok) {
const text = await res.text().catch(() => '');
return NextResponse.json({ error: `Anthropic: ${res.status} ${text}` }, { status: res.status });
}
const data = await res.json();
models = (data.data ?? []).map((m: { id: string }) => m.id);
}
else if (provider === 'openrouter') {
const headers: Record<string, string> = {};
if (apiKey?.trim()) headers['Authorization'] = `Bearer ${apiKey.trim()}`;
const res = await fetch('https://openrouter.ai/api/v1/models', { headers });
if (!res.ok) {
const text = await res.text().catch(() => '');
return NextResponse.json({ error: `OpenRouter: ${res.status} ${text}` }, { status: res.status });
}
const data = await res.json();
// Sort by id for easier browsing
models = (data.data ?? [])
.map((m: { id: string }) => m.id)
.sort((a: string, b: string) => a.localeCompare(b));
}
else {
// LM Studio and Ollama — OpenAI-compatible /v1/models
const res = await fetch(`${base}/models`, {
headers: { Authorization: 'Bearer none' },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
return NextResponse.json({ error: `${def.label}: ${res.status} ${text || 'Connection refused'}` }, { status: res.status });
}
const data = await res.json();
models = (data.data ?? []).map((m: { id: string }) => m.id);
}
return NextResponse.json({ models });
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
const isTimeout = msg.includes('timeout') || msg.includes('abort');
return NextResponse.json(
{ error: isTimeout ? `Could not reach ${def.label} — is it running?` : msg },
{ status: 500 }
);
}
}