import { AUTH_ERROR_MESSAGES, authErrorKind, canonicalModelName, getServerConfig, resolveAuth, resolveModelName, type AuthMode } from "@/lib/config"; import { friendlyErrorMessage, ollamaListModels, ollamaVerifyKey, OllamaError } from "@/lib/ollama"; import { clientKey, rateLimit, rateLimitResponse } from "@/lib/rate-limit"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export type ModelHealth = { configured: string; resolved: string; available: boolean | null; note?: string; }; export type HealthResponse = { ok: boolean; reachable: boolean; mode: "local-daemon" | "direct-cloud"; authMode: AuthMode; /** True when the browser has to provide its own key and hasn't. */ needsKey: boolean; /** null = not checked (no key), true/false = Ollama accepted/rejected the key. */ keyAccepted: boolean | null; baseUrl: string; hasApiKey: boolean; vision: ModelHealth; text: ModelHealth; latencyMs: number | null; modelCount: number | null; error: string | null; kind?: string; checkedAt: string; }; export async function GET(request: Request) { const config = getServerConfig(); const limit = rateLimit(`health:${clientKey(request)}`, config.rateLimits.health, config.rateLimits.windowMs); if (!limit.allowed) return rateLimitResponse(limit); const started = Date.now(); const resolvedAuth = resolveAuth(config, request); const health: HealthResponse = { ok: false, reachable: false, mode: config.directCloud ? "direct-cloud" : "local-daemon", authMode: config.authMode, needsKey: "error" in resolvedAuth, keyAccepted: null, baseUrl: config.baseUrl, hasApiKey: !("error" in resolvedAuth) && Boolean(resolvedAuth.auth.apiKey), vision: { configured: config.visionModel, resolved: resolveModelName(config.visionModel, config.directCloud), available: null, }, text: { configured: config.textModel, resolved: resolveModelName(config.textModel, config.directCloud), available: null, }, latencyMs: null, modelCount: null, error: null, checkedAt: new Date().toISOString(), }; if ("error" in resolvedAuth) { health.error = resolvedAuth.error === "missing_key" ? "No Ollama API key yet. Add your key in the settings dialog — it is stored only in this browser." : AUTH_ERROR_MESSAGES[resolvedAuth.error]; health.kind = authErrorKind(resolvedAuth.error); return Response.json(health satisfies HealthResponse, { headers: { "Cache-Control": "no-store" } }); } try { const models = await ollamaListModels(resolvedAuth.auth); health.reachable = true; health.latencyMs = Date.now() - started; health.modelCount = models.length; // A key is only "accepted" once Ollama authenticated a real request — // model listings are public, so an actual (tiny, cached) chat proves it. if (resolvedAuth.auth.apiKey) { const probe = await ollamaVerifyKey(resolvedAuth.auth, config.textModel, config.healthTimeoutMs * 3); health.keyAccepted = probe.valid; if (probe.valid === false) { health.error = "Ollama rejected this API key. Create a new one at ollama.com/settings/keys and paste it again."; health.kind = "auth"; return Response.json(health satisfies HealthResponse, { headers: { "Cache-Control": "no-store" } }); } if (probe.reason === "no_credits") { health.error = `The key works, but ${config.textModel} is not included in this account's plan. Add usage credits or switch to a free-tier model in .env.local.`; health.kind = "no_credits"; } } const names = new Set(models.map((model) => canonicalModelName(model.name))); for (const key of ["vision", "text"] as const) { const entry = health[key]; const wanted = canonicalModelName(entry.resolved); const listed = names.has(wanted); entry.available = listed; if (!listed) { entry.note = health.mode === "local-daemon" ? `Not in the daemon's model list. Run \`ollama pull ${entry.resolved}\` or check your sign-in.` : `Not returned by ${config.baseUrl}/api/tags. Check the model name and your key's plan.`; } } health.ok = Boolean(health.vision.available && health.text.available && health.keyAccepted !== false); if (!health.ok && !health.error) { health.error = "Ollama is reachable, but at least one configured model is not available."; } } catch (error) { health.error = friendlyErrorMessage(error, config.baseUrl); health.kind = error instanceof OllamaError ? error.kind : "unknown"; if (error instanceof OllamaError && (error.kind === "auth" || error.kind === "http")) { health.keyAccepted = false; } health.latencyMs = null; } return Response.json(health satisfies HealthResponse, { status: 200, headers: { "Cache-Control": "no-store" }, }); }