const VOYAGE_API_URL = "https://api.voyageai.com/v1"; const EMBED_MODEL = "voyage-3"; const RERANK_MODEL = "rerank-2"; const EMBED_BATCH_SIZE = 32; function getApiKey() { const key = process.env.VOYAGE_API_KEY; if (!key) throw new Error("VOYAGE_API_KEY is not set"); return key; } async function voyagePost(path, body) { const res = await fetch(`${VOYAGE_API_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${getApiKey()}`, }, body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text(); throw new Error(`Voyage ${path} failed (${res.status}): ${text}`); } return res.json(); } // Embed text - voyage-3. inputType must be "document" (ingest) or "query" (search). export async function embedTexts(texts, inputType = "document") { if (texts.length === 0) return []; const embeddings = []; for (let i = 0; i < texts.length; i += EMBED_BATCH_SIZE) { const batch = texts.slice(i, i + EMBED_BATCH_SIZE); const json = await voyagePost("/embeddings", { model: EMBED_MODEL, input: batch, input_type: inputType, }); const ordered = (json.data || []) .slice() .sort((a, b) => (a.index ?? 0) - (b.index ?? 0)); embeddings.push(...ordered.map((d) => d.embedding)); } return embeddings; } export async function rerank(query, documents) { if (!documents || documents.length === 0) return []; const texts = documents.map((d) => (typeof d === "string" ? d : d.text)); const json = await voyagePost("/rerank", { model: RERANK_MODEL, query, documents: texts, }); return (json.data || []).map((item) => ({ index: item.index, document: documents[item.index], relevanceScore: item.relevance_score, })); }