Skip to content

Commit 005ecaf

Browse files
authored
feat: remote ai
1 parent 08cfc88 commit 005ecaf

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { eventHandler, getValidatedRouterParams, readValidatedBody } from 'h3'
2+
import { z } from 'zod'
3+
import { hubAi } from '../../../utils/ai'
4+
import { requireNuxtHubAuthorization } from '../../../../../utils/auth'
5+
import { requireNuxtHubFeature } from '../../../../../utils/features'
6+
7+
const statementValidation = z.object({
8+
model: z.string().min(1).max(1e6).trim(),
9+
params: z.record(z.string(), z.any()).optional()
10+
})
11+
12+
export default eventHandler(async (event) => {
13+
await requireNuxtHubAuthorization(event)
14+
requireNuxtHubFeature('ai')
15+
16+
// https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods
17+
const { command } = await getValidatedRouterParams(event, z.object({
18+
command: z.enum(['run'])
19+
}).parse)
20+
const ai = hubAi()
21+
22+
if (command === 'run') {
23+
const { model, params } = await readValidatedBody(event, statementValidation.pick({ model: true, params: true }).parse)
24+
// @ts-expect-error Ai type defines all the compatible models, however Zod is only validating for string
25+
return ai.run(model, params)
26+
}
27+
})

src/runtime/ai/server/utils/ai.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { ofetch } from 'ofetch'
2+
import { joinURL } from 'ufo'
13
import { createError } from 'h3'
2-
import type { Ai } from '@nuxthub/core'
4+
import type { H3Error } from 'h3'
5+
import type { Ai } from '../../../../types/ai'
36
import { requireNuxtHubFeature } from '../../../utils/features'
7+
import { useRuntimeConfig } from '#imports'
48

59
let _ai: Ai
610

@@ -22,11 +26,56 @@ export function hubAi(): Ai {
2226
if (_ai) {
2327
return _ai
2428
}
29+
const hub = useRuntimeConfig().hub
2530
// @ts-expect-error globalThis.__env__ is not defined
2631
const binding = process.env.AI || globalThis.__env__?.AI || globalThis.AI
32+
if (hub.remote && hub.projectUrl && !binding) {
33+
_ai = proxyHubAi(hub.projectUrl, hub.projectSecretKey || hub.userToken)
34+
return _ai
35+
}
2736
if (binding) {
2837
_ai = binding as Ai
2938
return _ai
3039
}
3140
throw createError('Missing Cloudflare AI binding (AI)')
3241
}
42+
43+
/**
44+
* Access remote Workers AI.
45+
*
46+
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
47+
* @param secretKey The secret key to authenticate to the remote endpoint
48+
*
49+
* @example ```ts
50+
* const ai = proxyHubAi('https://my-deployed-project.nuxt.dev', 'my-secret-key')
51+
* await ai.run('@cf/meta/llama-3-8b-instruct', {
52+
* prompt: "What is the origin of the phrase 'Hello, World'"
53+
* })
54+
* ```
55+
*
56+
* @see https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods
57+
*/
58+
export function proxyHubAi(projectUrl: string, secretKey?: string): Ai {
59+
requireNuxtHubFeature('ai')
60+
61+
const aiAPI = ofetch.create({
62+
baseURL: joinURL(projectUrl, '/api/_hub/ai'),
63+
method: 'POST',
64+
headers: {
65+
Authorization: `Bearer ${secretKey}`
66+
}
67+
})
68+
return {
69+
async run(model: string, params?: Record<string, unknown>) {
70+
return aiAPI('/run', { body: { model, params } }).catch(handleProxyError)
71+
}
72+
} as Ai
73+
}
74+
75+
function handleProxyError(err: H3Error) {
76+
throw createError({
77+
statusCode: err.statusCode,
78+
// @ts-expect-error not aware of data property
79+
message: err.data?.message || err.message
80+
})
81+
}

0 commit comments

Comments
 (0)