Organize/lib/ai-settings.ts

85 lines
2.7 KiB
TypeScript

import "server-only";
import { prisma } from "@/lib/db";
// Fixed id of the one-and-only AiSettings row (see prisma/schema.prisma).
const SETTINGS_ID = "singleton";
export interface AiSettingsView {
apiUrl: string | null;
model: string | null;
hasApiKey: boolean;
// Last 4 characters only (e.g. "••••••••f00d") -- enough to confirm
// *which* key is configured without ever sending the real one back to
// the browser. Null when no key is set.
apiKeyMasked: string | null;
}
function maskKey(key: string): string {
return `••••••••${key.slice(-4)}`;
}
/**
* Builds an OpenAI-compatible endpoint URL from an admin-entered base URL
* (e.g. "https://host/api/v1") plus a path (e.g. "models",
* "chat/completions"). Shared by every call site that talks to the
* configured provider, so the trailing-slash handling only lives once --
* `new URL(path, base)` silently drops the base's last path segment
* (turning ".../v1" into ".../models" instead of ".../v1/models") unless
* the base ends in "/".
*/
export function joinApiUrl(apiUrl: string, path: string): URL {
const withTrailingSlash = apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
return new URL(path, withTrailingSlash);
}
/** The client-safe view of the AI settings -- never includes the real key. */
export async function getAiSettingsView(): Promise<AiSettingsView> {
const settings = await prisma.aiSettings.findUnique({ where: { id: SETTINGS_ID } });
return {
apiUrl: settings?.apiUrl ?? null,
model: settings?.model ?? null,
hasApiKey: !!settings?.apiKey,
apiKeyMasked: settings?.apiKey ? maskKey(settings.apiKey) : null,
};
}
/**
* The real, unmasked key -- for server-side use only (calling the
* provider). Never pass this to a client component or return it from a
* Server Action.
*/
export async function getAiCredentials(): Promise<{
apiUrl: string | null;
apiKey: string | null;
}> {
const settings = await prisma.aiSettings.findUnique({ where: { id: SETTINGS_ID } });
return { apiUrl: settings?.apiUrl ?? null, apiKey: settings?.apiKey ?? null };
}
/**
* `apiKey` omitted (or empty) means "leave the currently saved key
* alone" -- the form field is never prefilled with the real key, so
* there's nothing to resubmit when the admin isn't changing it.
*/
export async function saveAiSettings(data: {
apiUrl: string;
apiKey?: string;
model: string;
}): Promise<void> {
await prisma.aiSettings.upsert({
where: { id: SETTINGS_ID },
create: {
id: SETTINGS_ID,
apiUrl: data.apiUrl,
apiKey: data.apiKey || null,
model: data.model,
},
update: {
apiUrl: data.apiUrl,
model: data.model,
...(data.apiKey ? { apiKey: data.apiKey } : {}),
},
});
}