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>
85 lines
2.9 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|