fix(edge): meter and size-cap knowledge embeddings in embed-chunks and search-knowledge

This commit is contained in:
Yun Chan 2026-09-28 00:53:48 +09:00
parent b35676c75c
commit 1afaea7214
12 changed files with 1111 additions and 270 deletions

View file

@ -0,0 +1,61 @@
// 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[] }
},
}
}