deckhearth/lib/card-embed.js

91 lines
2.5 KiB
JavaScript
Raw Normal View History

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(',')}]`;
}