61 lines
2.1 KiB
TypeScript
61 lines
2.1 KiB
TypeScript
// 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<EmbeddingOutcome>
|
|
}
|
|
|
|
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[] }
|
|
},
|
|
}
|
|
}
|