| 1 | import { neon } from "@neon/ai-sdk-provider"; |
| 2 | import { streamText } from "ai"; |
| 3 | |
| 4 | export default { |
| 5 | async fetch(request: Request) { |
| 6 | const url = new URL(request.url); |
| 7 | if (url.pathname === "/" && request.method === "GET") { |
| 8 | return new Response(page, { |
| 9 | headers: { |
| 10 | "content-type": "text/html; charset=utf-8", |
| 11 | "cache-control": "no-store", |
| 12 | }, |
| 13 | }); |
| 14 | } |
| 15 | if (url.pathname !== "/chat" || request.method !== "POST") |
| 16 | return new Response("Not found", { status: 404 }); |
| 17 | if ( |
| 18 | !process.env.EXAMPLE_API_KEY || |
| 19 | request.headers.get("authorization") !== |
| 20 | `Bearer ${process.env.EXAMPLE_API_KEY}` |
| 21 | ) { |
| 22 | return new Response("Unauthorized", { status: 401 }); |
| 23 | } |
| 24 | const input = await request.json().catch(() => undefined); |
| 25 | if ( |
| 26 | !input || |
| 27 | typeof input.prompt !== "string" || |
| 28 | input.prompt.trim().length === 0 || |
| 29 | input.prompt.length > 4000 |
| 30 | ) |
| 31 | return new Response("Expected a prompt of at most 4000 characters", { |
| 32 | status: 400, |
| 33 | }); |
| 34 | if (process.env.AI_ALLOW_PAID !== "true") |
| 35 | return new Response( |
| 36 | "Paid inference is disabled. Set NEON_AI_ALLOW_PAID=true explicitly.", |
| 37 | { status: 503 }, |
| 38 | ); |
| 39 | if (!process.env.AI_MODEL) |
| 40 | return new Response("Configure NEON_AI_MODEL before deployment.", { |
| 41 | status: 503, |
| 42 | }); |
| 43 | const result = streamText({ |
| 44 | model: neon(process.env.AI_MODEL), |
| 45 | prompt: input.prompt, |
| 46 | maxOutputTokens: 128, |
| 47 | maxRetries: 0, |
| 48 | abortSignal: request.signal, |
| 49 | }); |
| 50 | return result.toUIMessageStreamResponse({ |
| 51 | onError: () => |
| 52 | "Gateway rejected the request. Check model access, paid-plan entitlement and prepaid credits. No credits were purchased.", |
| 53 | }); |
| 54 | }, |
| 55 | }; |
| 56 | |
| 57 | const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Neon AI Gateway</title> |
| 58 | <style>body{font:16px system-ui;background:#101815;color:#e6f3ec;padding:24px;margin:0}main{max-width:680px;margin:5vh auto}label{display:block;margin:16px 0 6px}input,textarea,button{font:inherit;padding:12px;border-radius:8px;box-sizing:border-box}input,textarea{width:100%;background:#182a21;color:white;border:1px solid #6b8878}button{border:0;background:#c5f5a8;margin:12px 12px 12px 0;cursor:pointer}pre{white-space:pre-wrap;overflow-wrap:anywhere;min-height:140px;background:#182a21;padding:16px}small{color:#a8bbaf}</style></head> |
| 59 | <body><main><h1>Neon AI Gateway</h1><p>Stream from a configurable model. Requests stop when you cancel.</p><form id="chat"><label for="key">Example API key</label><input type="password" id="key" autocomplete="off" required><label for="prompt">Prompt</label><textarea id="prompt" maxlength="4000" required>Say hello in one short sentence.</textarea><button>Send</button><button type="button" id="cancel">Cancel</button></form><pre id="result" role="status" aria-live="polite">Ready. Paid inference must be explicitly enabled.</pre><small>The API key entered here authenticates this example, not the Neon account. Gateway tokens remain server-side. This example never buys credits.</small></main> |
| 60 | <script type="module"> |
| 61 | let controller;const result=document.querySelector('#result'); |
| 62 | document.querySelector('#cancel').onclick=()=>controller?.abort(); |
| 63 | document.querySelector('#chat').onsubmit=async(event)=>{event.preventDefault();controller?.abort();controller=new AbortController();result.textContent='';try{const response=await fetch('/chat',{method:'POST',signal:controller.signal,headers:{'content-type':'application/json',authorization:'Bearer '+document.querySelector('#key').value},body:JSON.stringify({prompt:document.querySelector('#prompt').value})});if(!response.ok)throw new Error('HTTP '+response.status+': '+await response.text());const reader=response.body.getReader();const decoder=new TextDecoder();let pending='';for(;;){const {value,done}=await reader.read();if(done)break;pending+=decoder.decode(value,{stream:true});let end;while((end=pending.indexOf('\\n'))>=0){const line=pending.slice(0,end).trim();pending=pending.slice(end+1);if(!line.startsWith('data: ')||line==='data: [DONE]')continue;const event=JSON.parse(line.slice(6));if(event.type==='text-delta')result.textContent+=event.delta;if(event.type==='error')result.textContent+='\\n'+event.errorText;}}}catch(error){result.textContent+=(error.name==='AbortError'?'\\nCancelled.':'\\n'+error.message);}}; |
| 64 | </script></body></html>`; |