// server/supabase/functions/_shared/openai-embeddings.ts // Embedding provider port + OpenAI adapter shared by embed-chunks and // search-knowledge. The functions depend on the `EmbeddingProvider` port only, // so the quota/size policy can be tested without the network. export const EMBEDDING_MODEL = 'text-embedding-3-small' export const EMBEDDING_DIMENSIONS = 1536 export const EMBEDDING_PROVIDER_TIMEOUT_MS = 45_000 const OPENAI_EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings' export interface EmbeddingItem { embedding?: unknown index?: unknown } export type EmbeddingOutcome = | { ok: true; data: EmbeddingItem[] } /** `upstream`: transport error or non-2xx status. `invalid`: 2xx with an unusable body. */ | { ok: false; reason: 'upstream' | 'invalid' } export interface EmbeddingProvider { embed(input: string | readonly string[]): Promise } export function isEmbedding(value: unknown): value is number[] { return Array.isArray(value) && value.length === EMBEDDING_DIMENSIONS && value.every((entry) => typeof entry === 'number' && Number.isFinite(entry)) } export function createOpenAIEmbeddingProvider( apiKey: string, fetchImpl: typeof fetch = fetch, ): EmbeddingProvider { return { async embed(input) { let response: Response try { response = await fetchImpl(OPENAI_EMBEDDINGS_URL, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: EMBEDDING_MODEL, input, dimensions: EMBEDDING_DIMENSIONS, }), signal: AbortSignal.timeout(EMBEDDING_PROVIDER_TIMEOUT_MS), }) } catch { return { ok: false, reason: 'upstream' } } if (!response.ok) return { ok: false, reason: 'upstream' } const payload = await response.json().catch(() => null) as { data?: unknown } | null if (!payload || !Array.isArray(payload.data)) return { ok: false, reason: 'invalid' } return { ok: true, data: payload.data as EmbeddingItem[] } }, } }