docs.ts152 lines · main
1import { SupabaseClient } from '@supabase/supabase-js'
2import { ApplicationError, clippy, UserError } from 'ai-commands/edge'
3import { NextRequest } from 'next/server'
4import OpenAI from 'openai'
5
6export const config = {
7 runtime: 'edge',
8 /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that
9 overlap with the OpenAI API regions.
10
11 Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list
12 Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories
13 */
14 regions: [
15 'arn1',
16 'bom1',
17 'cdg1',
18 'cle1',
19 'cpt1',
20 'dub1',
21 'fra1',
22 'gru1',
23 'hnd1',
24 'iad1',
25 'icn1',
26 'kix1',
27 'lhr1',
28 'pdx1',
29 'sfo1',
30 'sin1',
31 'syd1',
32 ],
33}
34
35const openAiKey = process.env.OPENAI_API_KEY
36const brivenUrl = process.env.NEXT_PUBLIC_BRIVEN_URL
37const brivenServiceKey = process.env.NEXT_PUBLIC_BRIVEN_ANON_KEY
38
39export default async function handler(req: NextRequest) {
40 if (!openAiKey) {
41 return new Response(
42 JSON.stringify({
43 error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
44 }),
45 {
46 status: 500,
47 headers: { 'Content-Type': 'application/json' },
48 }
49 )
50 }
51
52 if (!brivenUrl) {
53 return new Response(
54 JSON.stringify({
55 error:
56 'No NEXT_PUBLIC_BRIVEN_URL set. Create this environment variable to use AI features.',
57 }),
58 {
59 status: 500,
60 headers: { 'Content-Type': 'application/json' },
61 }
62 )
63 }
64
65 if (!brivenServiceKey) {
66 return new Response(
67 JSON.stringify({
68 error:
69 'No NEXT_PUBLIC_BRIVEN_ANON_KEY set. Create this environment variable to use AI features.',
70 }),
71 {
72 status: 500,
73 headers: { 'Content-Type': 'application/json' },
74 }
75 )
76 }
77
78 const { method } = req
79
80 switch (method) {
81 case 'POST':
82 return handlePost(req)
83 default:
84 return new Response(
85 JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
86 {
87 status: 405,
88 headers: { 'Content-Type': 'application/json', Allow: 'POST' },
89 }
90 )
91 }
92}
93
94async function handlePost(request: NextRequest) {
95 const openai = new OpenAI({ apiKey: openAiKey })
96
97 const body = await (request.json() as Promise<{
98 messages: { content: string; role: 'user' | 'assistant' }[]
99 }>)
100
101 const { messages } = body
102
103 if (!messages) {
104 throw new UserError('Missing messages in request data')
105 }
106
107 const brivenClient = new SupabaseClient(brivenUrl!, brivenServiceKey!)
108
109 try {
110 const response = await clippy(openai, brivenClient, messages)
111
112 // Proxy the streamed SSE response from OpenAI
113 return new Response(response.body, {
114 headers: {
115 'Content-Type': 'text/event-stream',
116 },
117 })
118 } catch (error: unknown) {
119 console.error(error)
120 if (error instanceof UserError) {
121 return new Response(
122 JSON.stringify({
123 error: error.message,
124 data: error.data,
125 }),
126 {
127 status: 400,
128 headers: { 'Content-Type': 'application/json' },
129 }
130 )
131 } else if (error instanceof ApplicationError) {
132 // Print out application errors with their additional data
133 console.error(`${error.message}: ${JSON.stringify(error.data)}`)
134 } else {
135 // Print out unexpected errors as is to help with debugging
136 console.error(error)
137 }
138
139 console.log('Returning generic 500 ApplicationError to client')
140
141 // TODO: include more response info in debug environments
142 return new Response(
143 JSON.stringify({
144 error: 'There was an error processing your request',
145 }),
146 {
147 status: 500,
148 headers: { 'Content-Type': 'application/json' },
149 }
150 )
151 }
152}