fix(edge): meter and size-cap knowledge embeddings in embed-chunks and search-knowledge
This commit is contained in:
parent
b35676c75c
commit
1afaea7214
12 changed files with 1111 additions and 270 deletions
61
server/supabase/functions/_shared/openai-embeddings.ts
Normal file
61
server/supabase/functions/_shared/openai-embeddings.ts
Normal 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[] }
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue