// aiComplete — a one-shot LLM completion at the edge. // // Demonstrates: calling a third-party API that needs a SECRET (the key lives in // `ctx.env`, never in the bundle), an optional input field, reading the JSON // response, and mapping an upstream failure to a 502. Uses the OpenAI // chat-completions shape; point it at any compatible endpoint via OPENAI_BASE_URL. // // OPENAI_API_KEY=sk-... voltro serverless dev functions/aiComplete.serverless.ts // curl -X POST http://localhost:8910/ -d '{"prompt":"Name three primary colors."}' import { Effect, Schema } from 'effect' import { HttpClient, HttpClientRequest } from '@effect/platform' import { defineServerless, ServerlessHttpError } from '@voltro/serverless' export default defineServerless({ name: 'ai-complete', method: 'POST', input: Schema.Struct({ prompt: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(4000)), system: Schema.optional(Schema.String), }), output: Schema.Struct({ text: Schema.String }), runtime: { memoryMb: 256, timeoutSeconds: 30 }, handler: ({ prompt, system }, ctx) => Effect.gen(function* () { const apiKey = ctx.env.OPENAI_API_KEY if (!apiKey) { return yield* Effect.fail( new ServerlessHttpError({ status: 503, message: 'ai-not-configured', detail: 'Set OPENAI_API_KEY.' }), ) } const baseUrl = ctx.env.OPENAI_BASE_URL ?? 'https://api.openai.com' const model = ctx.env.OPENAI_MODEL ?? 'gpt-4o-mini' const http = yield* HttpClient.HttpClient const request = HttpClientRequest.post(`${baseUrl}/v1/chat/completions`).pipe( HttpClientRequest.setHeader('authorization', `Bearer ${apiKey}`), HttpClientRequest.bodyUnsafeJson({ model, max_tokens: 400, messages: [ ...(system ? [{ role: 'system', content: system }] : []), { role: 'user', content: prompt }, ], }), ) const response = yield* http.execute(request) if (response.status >= 400) { const detail = yield* response.text return yield* Effect.fail(new ServerlessHttpError({ status: 502, message: 'ai-call-failed', detail })) } const body = (yield* response.json) as { choices?: ReadonlyArray<{ message?: { content?: string } }> } return { text: body.choices?.[0]?.message?.content ?? '' } }), })