Barazo AppView backend
barazo.forum
1import type { Logger } from '../lib/logger.js'
2
3// ---------------------------------------------------------------------------
4// Types
5// ---------------------------------------------------------------------------
6
7export interface EmbeddingService {
8 /** Generate an embedding vector for the given text. Returns null on failure or when disabled. */
9 generateEmbedding(text: string): Promise<number[] | null>
10 /** Whether the embedding service is configured and available. */
11 isEnabled(): boolean
12}
13
14/** OpenAI-compatible embedding response. */
15interface EmbeddingResponse {
16 data: ReadonlyArray<{ embedding: number[] }>
17}
18
19// ---------------------------------------------------------------------------
20// Implementation
21// ---------------------------------------------------------------------------
22
23/**
24 * Create an embedding service that calls an OpenAI-compatible embedding API.
25 *
26 * If `embeddingUrl` is undefined or empty, the service operates in disabled mode:
27 * `isEnabled()` returns false and `generateEmbedding()` always returns null.
28 *
29 * On network or API errors the service logs a warning and returns null -- it
30 * never throws. This allows the search route to gracefully degrade to
31 * full-text-only search when the embedding backend is unavailable.
32 */
33export function createEmbeddingService(
34 embeddingUrl: string | undefined,
35 dimensions: number,
36 logger: Logger
37): EmbeddingService {
38 const enabled = typeof embeddingUrl === 'string' && embeddingUrl.length > 0
39
40 return {
41 isEnabled(): boolean {
42 return enabled
43 },
44
45 async generateEmbedding(text: string): Promise<number[] | null> {
46 if (!enabled || !embeddingUrl) {
47 return null
48 }
49
50 try {
51 const response = await fetch(embeddingUrl, {
52 method: 'POST',
53 headers: { 'Content-Type': 'application/json' },
54 body: JSON.stringify({
55 input: text,
56 model: 'default',
57 dimensions,
58 }),
59 signal: AbortSignal.timeout(10_000),
60 })
61
62 if (!response.ok) {
63 logger.warn(
64 { status: response.status, url: embeddingUrl },
65 'Embedding API returned non-OK status'
66 )
67 return null
68 }
69
70 const body = (await response.json()) as EmbeddingResponse
71 const embedding = body.data[0]?.embedding
72
73 if (!Array.isArray(embedding) || embedding.length === 0) {
74 logger.warn('Embedding API returned empty or invalid embedding')
75 return null
76 }
77
78 return embedding
79 } catch (err: unknown) {
80 logger.warn({ err }, 'Failed to generate embedding')
81 return null
82 }
83 },
84 }
85}