Files
9router/src/app/api/models/test/route.js
T
decolua 7ad538bcf2 # v0.4.29 (2026-05-10)
## Features
- Add Cline & Kilo Code tool cards
- Tailscale TUN mode for stable Funnel TLS
- Sort APIKEY providers by usage, collapse to top 20

## Improvements
- Local Material Symbols font (no Google Fonts)
- Docker base: Bun → Node 22-alpine
- MITM reads aliases from JSON cache (no native sqlite)
- Stream stall timeout (2 min) in open-sse

## Fixes
- Fal.ai key test: use stable models endpoint
2026-05-10 21:56:40 +07:00

116 lines
4.2 KiB
JavaScript

import { NextResponse } from "next/server";
import { getApiKeys } from "@/lib/localDb";
import { UPDATER_CONFIG } from "@/shared/constants/config";
// POST /api/models/test - Ping a single model via internal completions or embeddings
export async function POST(request) {
try {
const { model, kind } = await request.json();
if (!model) return NextResponse.json({ error: "Model required" }, { status: 400 });
const baseUrl = `http://127.0.0.1:${UPDATER_CONFIG.appPort}`;
// Get an active internal API key for auth (if requireApiKey is enabled)
let apiKey = null;
try {
const keys = await getApiKeys();
apiKey = keys.find((k) => k.isActive !== false)?.key || null;
} catch {}
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const start = Date.now();
// Route to appropriate endpoint based on kind
if (kind === "embedding") {
const res = await fetch(`${baseUrl}/api/v1/embeddings`, {
method: "POST",
headers,
body: JSON.stringify({ model, input: "test" }),
signal: AbortSignal.timeout(15000),
});
const latencyMs = Date.now() - start;
const rawText = await res.text().catch(() => "");
let parsed = null;
try { parsed = rawText ? JSON.parse(rawText) : null; } catch {}
if (!res.ok) {
const detail = parsed?.error?.message || parsed?.error || rawText;
return NextResponse.json({ ok: false, latencyMs, error: `HTTP ${res.status}${detail ? `: ${String(detail).slice(0, 240)}` : ""}`, status: res.status });
}
const hasEmbedding = Array.isArray(parsed?.data) && parsed.data.length > 0 && Array.isArray(parsed.data[0]?.embedding);
if (!hasEmbedding) {
return NextResponse.json({ ok: false, latencyMs, status: res.status, error: "Provider returned no embedding data" });
}
return NextResponse.json({ ok: true, latencyMs, error: null, status: res.status });
}
// Default: chat completions
const res = await fetch(`${baseUrl}/api/v1/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify({
model,
max_tokens: 1,
stream: false,
messages: [{ role: "user", content: "hi" }],
}),
signal: AbortSignal.timeout(15000),
});
const latencyMs = Date.now() - start;
const rawText = await res.text().catch(() => "");
let parsed = null;
try {
parsed = rawText ? JSON.parse(rawText) : null;
} catch {}
if (!res.ok) {
const detail = parsed?.error?.message || parsed?.msg || parsed?.message || parsed?.error || rawText;
const error = `HTTP ${res.status}${detail ? `: ${String(detail).slice(0, 240)}` : ""}`;
return NextResponse.json({ ok: false, latencyMs, error, status: res.status });
}
// Some providers may return HTTP 200 but not a real completion for invalid models.
const providerStatus = parsed?.status;
const providerMsg = parsed?.msg || parsed?.message;
const hasProviderErrorStatus = providerStatus !== undefined
&& providerStatus !== null
&& String(providerStatus) !== "200"
&& String(providerStatus) !== "0";
if (hasProviderErrorStatus && providerMsg) {
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: `Provider status ${providerStatus}: ${String(providerMsg).slice(0, 240)}`,
});
}
if (parsed?.error) {
const providerError = parsed?.error?.message || parsed?.error || "Provider returned an error";
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: String(providerError).slice(0, 240),
});
}
const hasChoices = Array.isArray(parsed?.choices) && parsed.choices.length > 0;
if (!hasChoices) {
return NextResponse.json({
ok: false,
latencyMs,
status: res.status,
error: "Provider returned no completion choices for this model",
});
}
return NextResponse.json({ ok: true, latencyMs, error: null, status: res.status });
} catch (err) {
return NextResponse.json({ ok: false, error: err.message }, { status: 500 });
}
}