spb/wp-spboucher-ai
Public
1import { AI_PROVIDER } from "@/lib/env";2/** Optional OpenAI embeddings. Model availability is verified at runtime (GET /v1/models/<id>) before first use; failures degrade to lexical only. */3const MODEL = process.env.OPENAI_EMBEDDING_MODEL ?? "text-embedding-3-small";4export const EMBEDDING_DIM = 1536;5let verified: boolean | null = null;6export function embeddingsEnabled() { return AI_PROVIDER === "openai" && !!process.env.OPENAI_API_KEY; }7export async function verifyEmbeddingModel(): Promise<{ ok: boolean; model: string; reason?: string }> {8 if (!embeddingsEnabled()) return { ok: false, model: MODEL, reason: "AI provider disabled" };9 if (verified !== null) return { ok: verified, model: MODEL };10 try {11 const r = await fetch(`https://api.openai.com/v1/models/${encodeURIComponent(MODEL)}`, { headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, signal: AbortSignal.timeout(15000) });12 verified = r.ok; return { ok: r.ok, model: MODEL, reason: r.ok ? undefined : `HTTP ${r.status}` };13 } catch (e) { verified = false; return { ok: false, model: MODEL, reason: (e as Error).message }; }14}15/** Embed up to 64 inputs per call; inputs are truncated to ~8,000 characters (model limit 8,191 tokens). Returns null when disabled. */16export async function embed(inputs: string[]): Promise<number[][] | null> {17 if (!embeddingsEnabled()) return null;18 const v = await verifyEmbeddingModel(); if (!v.ok) return null;19 const out: number[][] = [];20 for (let i = 0; i < inputs.length; i += 64) {21 const batch = inputs.slice(i, i + 64).map((s) => s.slice(0, 8000));22 const r = await fetch("https://api.openai.com/v1/embeddings", { method: "POST", headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, input: batch, dimensions: EMBEDDING_DIM }), signal: AbortSignal.timeout(60000) });23 if (!r.ok) throw new Error(`embeddings HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);24 const j = (await r.json()) as { data: { index: number; embedding: number[] }[] };25 for (const d of j.data.sort((a, b) => a.index - b.index)) out.push(d.embedding);26 }27 return out;28}29export const EMBEDDING_MODEL = MODEL;30