A streaming response should deliver useful text before the complete answer exists. Server-sent events, or SSE, provide a straightforward format for that job: the server sends named events over one HTTP response, and the browser processes them incrementally. This first-run walkthrough connects an OpenAI-compatible model stream to a browser without exposing your API key.
1. Set up the server-side model connection
Keep the model request on your application server. SwiftRelay fits this setup because it works with OpenAI-compatible SDKs and bills per token. Sign up for SwiftRelay, create an API key, and store it in a server-side environment variable rather than browser code.
For the OpenAI SDK, configure the base URL as https://api.fastapirelay.com/v1. Set the API key from your environment and choose an available model identifier from your account. Gemini 2.5 Flash is one model you can consider for this walkthrough; use the actual API identifier rather than assuming its display name is accepted.
2. Create a POST route for the streaming request
Add a server endpoint such as /api/stream that accepts a JSON prompt. Authenticate the caller, validate the input, and apply request limits before opening the stream. Keeping prompts in a POST body avoids placing their contents in URLs and ordinary URL access logs.
Start the upstream SDK request with streaming enabled. In an OpenAI Chat Completions integration, the essential call is:
const stream = await client.chat.completions.create({ model: modelId, messages: [{ role: "user", content: prompt }], stream: true });
Catch failures before committing the response headers where possible. An invalid API key or rejected request should produce an ordinary HTTP error, not an apparently successful stream with no explanation.
3. Frame each text delta as an SSE event
Return a response with Content-Type: text/event-stream and Cache-Control: no-cache. Ensure your framework actually flushes incremental writes. For this minimal implementation, ignore non-text deltas and extract text from chunk.choices[0]?.delta?.content as you iterate over the upstream stream.
Write each text fragment as a named event with a JSON payload. For example, write the string event: delta\ndata: {"text":"Hello"}\n\n. The blank line terminates the event; without it, the client may keep waiting.
Use JSON.stringify to serialize the payload instead of inserting model text directly into the event format. Generated text can contain newlines, quotation marks, and other characters that would otherwise break your framing.
After successful upstream completion, send event: done\ndata: {}\n\n and close the response. This is your application's event protocol, not an assumption that upstream events arrive in the same format.
4. Read the POST response with browser fetch
Use fetch with method POST, a JSON body, and an AbortController signal. Native EventSource is convenient for GET endpoints, but it does not support a POST body. With fetch, your browser application must parse the SSE response itself.
Check response.ok before reading response.body. Decode incoming bytes with a streaming TextDecoder, retain unfinished text between reads, and extract complete events at blank-line boundaries. Support both LF and CRLF line endings, and combine multiple data lines according to SSE rules.
Network chunks are not event boundaries. One read can contain several events or only part of one. Parse JSON only after assembling a complete event, then append the text from delta events. Render through textContent or a text node rather than interpreting model output as HTML.
5. Connect cancellation and visible failures
Make a Stop button abort the browser fetch. On the server, detect when the response connection closes and cancel the upstream request using the SDK's supported abort mechanism. Otherwise, generation can continue after the reader leaves.
If an upstream failure occurs after streaming starts, send a named application error event when the connection still permits it, then close. Show a useful message without exposing provider details or credentials. Treat a connection ending without your done event as an incomplete answer, not a successful completion.
6. Check the deployed streaming path
Test through your production proxy, not just localhost. Disable response buffering where necessary and check whether compression middleware delays event delivery. Verify multiline output, cancellation, upstream errors, and dropped connections. Finally, honor server write backpressure so a slow reader cannot cause unbounded buffering.