const GATEWAY_EMBED_URL = 'https://ai-gateway.vercel.sh/v1/embeddings'; const DEFAULT_EMBED_MODEL = process.env.SCAN_EMBED_MODEL || 'cohere/embed-v4.0'; /** Output dimension — must match migrations/1782000000001_add-card-embeddings.js. */ export const EMBED_DIMENSION = Number(process.env.SCAN_EMBED_DIMENSION || 1024); export class EmbedApiError extends Error { constructor(message, { status } = {}) { super(message); this.name = 'EmbedApiError'; this.status = status; } } function parseEmbeddingResponse(data) { const embedding = data?.data?.[0]?.embedding || data?.embeddings?.[0]?.values || data?.embeddings?.[0] || data?.embedding; if (!Array.isArray(embedding) || embedding.length === 0) { throw new EmbedApiError('Embedding API returned no vector'); } return embedding.map((value) => Number(value)); } function buildEmbedInput(value) { if (typeof value === 'string') { return value; } if (value?.imageUrl) { return { type: 'image_url', image_url: { url: value.imageUrl }, }; } if (value?.imageDataUrl) { return value.imageDataUrl; } throw new EmbedApiError('Invalid embed input'); } /** * Embed image content via Vercel AI Gateway (server-only). * Accepts a JPEG data URL or HTTPS image URL object. */ export async function embedCardImage(input) { const apiKey = process.env.AI_GATEWAY_API_KEY; if (!apiKey) { throw new EmbedApiError('AI_GATEWAY_API_KEY is not configured on the server'); } const payloadInput = buildEmbedInput(input); if (typeof payloadInput === 'string' && !payloadInput.includes(',')) { throw new EmbedApiError('Invalid image data format'); } const response = await fetch(GATEWAY_EMBED_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model: DEFAULT_EMBED_MODEL, input: payloadInput, dimensions: EMBED_DIMENSION, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const message = errorData.error?.message || errorData.message || 'Unknown error'; throw new EmbedApiError(`Embedding API error: ${response.status} - ${message}`, { status: response.status, }); } return parseEmbeddingResponse(await response.json()); } /** Format a float array for pgvector tagged-template queries. */ export function formatEmbeddingForPg(embedding) { return `[${embedding.map((value) => Number(value).toFixed(8)).join(',')}]`; }