docs.ts152 lines · main
| 1 | import { SupabaseClient } from '@supabase/supabase-js' |
| 2 | import { ApplicationError, clippy, UserError } from 'ai-commands/edge' |
| 3 | import { NextRequest } from 'next/server' |
| 4 | import OpenAI from 'openai' |
| 5 | |
| 6 | export 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 | |
| 35 | const openAiKey = process.env.OPENAI_API_KEY |
| 36 | const brivenUrl = process.env.NEXT_PUBLIC_BRIVEN_URL |
| 37 | const brivenServiceKey = process.env.NEXT_PUBLIC_BRIVEN_ANON_KEY |
| 38 | |
| 39 | export 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 | |
| 94 | async 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 | } |