"use server"; import { revalidatePath } from "next/cache"; import { requireAdmin } from "@/lib/auth-helpers"; import { getAiCredentials, saveAiSettings } from "@/lib/ai-settings"; export type TestAiConnectionResult = { error?: string; models?: string[] }; /** Builds the provider's `/models` endpoint from an admin-entered base URL. */ function buildModelsUrl(apiUrl: string): URL { const withTrailingSlash = apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`; return new URL("models", withTrailingSlash); } function extractModelIds(body: unknown): string[] { const data = body && typeof body === "object" ? (body as { data?: unknown }).data : undefined; if (!Array.isArray(data)) return []; return data .map((entry) => (entry && typeof entry === "object" ? (entry as { id?: unknown }).id : undefined)) .filter((id): id is string => typeof id === "string") .sort((a, b) => a.localeCompare(b)); } /** * Calls the given (not-necessarily-saved-yet) endpoint's OpenAI-compatible * `/models` list, so an admin can verify a URL/key pair actually works -- * and get real model ids to pick from -- before committing to it. */ export async function testAiConnection( apiUrl: string, apiKey: string ): Promise { await requireAdmin(); const trimmedUrl = apiUrl.trim(); if (!trimmedUrl) return { error: "Enter an API URL first." }; let modelsUrl: URL; try { modelsUrl = buildModelsUrl(trimmedUrl); } catch { return { error: "That doesn't look like a valid URL." }; } if (modelsUrl.protocol !== "http:" && modelsUrl.protocol !== "https:") { return { error: "The API URL must be http:// or https://." }; } // A blank key field means "use whatever's already saved" -- lets an // admin re-test without having to retype a key they can no longer see. let effectiveKey = apiKey.trim(); if (!effectiveKey) { const saved = await getAiCredentials(); effectiveKey = saved.apiKey ?? ""; } let response: Response; try { response = await fetch(modelsUrl, { headers: effectiveKey ? { Authorization: `Bearer ${effectiveKey}` } : {}, signal: AbortSignal.timeout(10_000), }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { error: `Couldn't reach that URL -- ${message}` }; } if (!response.ok) { if (response.status === 401 || response.status === 403) { return { error: `The server rejected the API key (${response.status}).` }; } return { error: `The server responded with ${response.status} ${response.statusText}.` }; } let body: unknown; try { body = await response.json(); } catch { return { error: "Connected, but the response wasn't valid JSON." }; } const models = extractModelIds(body); if (models.length === 0) { return { error: "Connected, but the server didn't list any models." }; } return { models }; } export async function saveAiConfig(data: { apiUrl: string; apiKey: string; model: string; }): Promise<{ error?: string }> { await requireAdmin(); const apiUrl = data.apiUrl.trim(); const model = data.model.trim(); if (!apiUrl) return { error: "Enter an API URL." }; if (!model) return { error: "Choose a model." }; try { const url = new URL(apiUrl); if (url.protocol !== "http:" && url.protocol !== "https:") { return { error: "The API URL must be http:// or https://." }; } } catch { return { error: "That doesn't look like a valid URL." }; } await saveAiSettings({ apiUrl, apiKey: data.apiKey.trim(), model }); revalidatePath("/admin"); return {}; }