1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
import { z } from 'zod'
export const ROUTE_PATH = '/api/blog_search'
const EMBEDDING_DIMENSION = 1024 as const
const MAX_TEXT_LENGTH = 1_000
const MAX_RETRIES = 5
const WORKERS_AI_MAX_RETRIES = 3
const RequestBodySchema = z.object({
text: z.string(),
})
const AIResponseSchema = z.object({
data: z.array(z.array(z.number()).length(EMBEDDING_DIMENSION)).length(1),
})
const textResponse = (msg: string, status = 400) => new Response(msg, { status })
export async function handle(c: HonoContext) {
let json: unknown
try {
json = await c.req.json()
} catch {
return textResponse('Bad Request: Invalid JSON', 400)
}
const body = RequestBodySchema.safeParse(json)
if (!body.success) {
return textResponse(`Bad Request: ${body.error.message}`, 400)
}
const text = body.data.text.trim()
if (text.length > MAX_TEXT_LENGTH) {
return textResponse('Bad Request: Text is too long', 400)
}
let embedding: number[] | null = null
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const raw = await c.env.AI.run(
'@cf/baai/bge-m3',
{ text },
{
gateway: {
id: 'workers-embedding',
retries: { maxAttempts: WORKERS_AI_MAX_RETRIES },
},
},
)
const parsed = AIResponseSchema.safeParse(raw)
if (!parsed.success) {
return textResponse(`Internal Server Error: ${parsed.error.message}`, 500)
}
embedding = parsed.data.data[0]
break
} catch (err) {
console.error(`Embedding attempt ${attempt + 1} failed`, err)
}
}
if (!embedding) {
return textResponse('Internal Server Error: Embedding failed', 500)
}
// ─── Respond with binary vector ────────────────────────────────────────────
const buffer = new Float32Array(embedding).buffer
const headers: Record<string, string> = {
'Content-Type': 'application/octet-stream',
}
return new Response(buffer, { status: 200, headers })
}
|