Free lesson · GenAI Application Engineering

Build a Llama 4 Maverick streaming adapter via Together.ai

You will create a Llama4StreamAdapter class in adapters/llama4_stream.py using openai.AsyncOpenAI configured with base_url='https://api.together.xyz/v1' and api_key from TOGETHER_API_KEY. The adapter exposes stream_chat(messages, model, temperature) -> AsyncGenerator[StreamChunk, None] calling client.chat.completions.create(model='meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8', stream=True). Since Together.ai implements the OpenAI-compatible API, chunk parsing reuses delta.content extraction from OpenAIStreamAdapter. You implement TogetherSettings reading TOGETHER_API_KEY from environment. The adapter adds Together-specific headers via httpx default_headers for tracing. You validate that models exist in SUPPORTED_LLAMA_MODELS and raise ModelNotSupportedError otherwise. Tests mock the Together.ai endpoint and verify StreamChunk output.

Course: Full-Stack GenAI Applications · Chapter 1 · Chat Completion API with Streaming

Free to read — no subscription required.

Introduction

When you ship a multi-provider chat UI, the moment you add an open-weight model the integration story usually fragments — different SDKs, different stream formats, different auth flows. Teams that try to bolt Llama onto a working OpenAI streaming pipeline often end up maintaining two parallel SSE adapters, and a single drift between them shows up in production as half-rendered tokens or stuck cursors. Llama 4 Maverick on Together.ai sidesteps that by exposing the OpenAI wire protocol, so the same AsyncOpenAI client you already use for GPT-4o can talk to a 400-billion-parameter MoE model with a one-line base_url swap. By the end of this lesson you'll be able to build a Llama4StreamAdapter that streams Maverick tokens as SSE frames through FastAPI, with client-disconnect detection that cancels upstream requests so a closed browser tab does not keep billing your account.

Key Terminology

  • Server-Sent Events (SSE): A one-way HTTP streaming protocol where the server emits data: <payload>\n\n frames over a long-lived response, consumed in the browser via the EventSource API.
  • base_url override: The AsyncOpenAI constructor parameter that redirects all SDK HTTP traffic from api.openai.com to any OpenAI-compatible endpoint such as https://api.together.xyz/v1.
  • StreamingResponse: FastAPI's response class that consumes an async generator and writes each yielded chunk to the wire immediately, bypassing the framework's default full-body buffering.
  • Client-disconnect detection: Polling request.is_disconnected() between yielded frames so the server can stop reading the upstream provider stream as soon as the browser closes the connection.

Concepts

Why Together.ai for Llama 4 Maverick

Llama 4 Maverick is a 17B-active-parameter, 128-expert MoE model that routes each token through a subset of experts, achieving throughput comparable to dense models one-tenth its total size. Together.ai provisions this model across clustered NVIDIA H100 nodes with speculative decoding enabled, delivering first-token latencies under 300 milliseconds for typical chat prompts. The critical architectural advantage is API compatibility: Together.ai implements the /v1/chat/completions endpoint with identical request and response schemas to OpenAI, including the stream: true parameter, ChatCompletionChunk objects, and finish_reason signaling. This means your adapter needs zero custom HTTP logic — the official openai Python SDK handles connection pooling, retry backoff, and SSE parsing natively.

  • Together.ai: A managed inference platform that hosts open-weight models behind OpenAI-compatible API endpoints, eliminating the need for self-hosted GPU infrastructure.
  • Llama 4 Maverick: Meta's mixture-of-experts chat model using 17 billion active parameters per forward pass out of 400 billion total, optimized for instruction-following and multi-turn dialogue.
  • OpenAI-compatible API: A REST interface that mirrors the OpenAI /v1/chat/completions request and response schema, enabling SDK reuse across providers.
  • Mixture-of-Experts (MoE): A model architecture where each input token activates only a small subset of "expert" sub-networks, reducing compute cost while maintaining capacity equivalent to the full parameter count.

Error handling and retry strategy

Together.ai's API enforces rate limits that differ from OpenAI's — free-tier accounts are capped at 60 requests per minute and 1,000 tokens per second of throughput. When limits are exceeded, the server returns HTTP 429 with a Retry-After header. The openai SDK handles this transparently through its built-in retry mechanism: it reads the Retry-After value, waits the specified duration, and replays the request up to max_retries times (defaulting to 2). You can override this during client initialization by passing max_retries=5 to AsyncOpenAI for production deployments where brief throttling spikes are expected.

Beyond rate limits, two Together.ai-specific failure modes require attention. First, the FP8-quantized Maverick checkpoint occasionally produces null content deltas mid-stream — not at the start where None deltas are normal, but interspersed between valid tokens. Your delta extraction guard (if delta_content is not None) already handles this. Second, Together.ai returns finish_reason="length" when the response hits max_tokens without a natural stop. Your application should surface this to the user so they know the response was truncated, not complete.

Provider normalization considerations

When integrating Llama4StreamAdapter alongside the OpenAI, Gemini, and Anthropic adapters from earlier goals, the normalization surface is minimal because Together.ai already speaks the OpenAI wire protocol. However, three differences require attention in a production multi-provider system:

  1. Model capability metadata — Llama 4 Maverick supports a 1,048,576-token context window compared to GPT-4o's 128,000 tokens. Your routing layer must track per-model limits to avoid silent truncation.

  2. Token counting — Together.ai does not return usage objects in streaming responses. If you need token accounting, you must either count tokens client-side using the Llama tokenizer (tiktoken does not support Llama vocabulary) or make a non-streaming call with stream_options={"include_usage": True}, which Together.ai supports as of their March 2026 API update.

  3. System prompt handling — Llama 4 Maverick treats the system message as a prefix to the first user turn rather than a persistent instruction. For multi-turn conversations, repeat critical system instructions in a developer-role message every N turns to prevent instruction drift.

Code Walkthrough

Having just covered the architectural compatibility, retry behavior, and normalization differences in the Concepts section, you can now translate those ideas into a working adapter. The walkthrough below demonstrates the base_url override, the chunk-delta extraction guard, and the disconnect-detection pattern in concrete code.

Adapter class — client initialization and streaming generator

The core insight behind the Together.ai adapter is that openai.AsyncOpenAI accepts a base_url constructor parameter that redirects all HTTP traffic away from api.openai.com to any compatible endpoint. Combined with a separate API key sourced from the TOGETHER_API_KEY environment variable, you get a fully isolated client instance that shares no credentials or routing with your OpenAI adapter. The implementation below defines the Llama4StreamAdapter class in one piece: an init method that reads the API key, instantiates the async client with Together.ai's base URL, and stores the FP8 model identifier as a class constant, followed by a stream_chat async generator that opens a streaming connection, iterates over ChatCompletionChunk objects, extracts token deltas, and yields W3C-compliant SSE frames. Together.ai returns chunks with identical structure to OpenAI — each chunk's choices[0].delta.content holds the token fragment and choices[0].finish_reason signals completion.

Code snippetpython
1import os 2from openai import AsyncOpenAI 3from typing import AsyncGenerator 4 5class Llama4StreamAdapter: 6 """Streaming adapter for Llama 4 Maverick via Together.ai.""" 7 8 MODEL_ID = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" 9 10 def __init__(self) -> None: 11 api_key = os.environ.get("TOGETHER_API_KEY") 12 if api_key is None: 13 raise EnvironmentError( 14 "TOGETHER_API_KEY environment variable is not set. " 15 "Obtain a key at https://api.together.xyz/settings/api-keys" 16 ) 17 self.client = AsyncOpenAI( 18 base_url="https://api.together.xyz/v1", 19 api_key=api_key, 20 ) 21 self.default_max_tokens = 2048 22 23 async def stream_chat( 24 self, 25 messages: list[dict[str, str]], 26 max_tokens: int | None = None, 27 ) -> AsyncGenerator[str, None]: 28 """Yield SSE-formatted frames from Llama 4 Maverick streaming.""" 29 token_budget = max_tokens or self.default_max_tokens 30 31 stream = await self.client.chat.completions.create( 32 model=self.MODEL_ID, 33 messages=messages, 34 max_tokens=token_budget, 35 temperature=0.7, 36 stream=True, 37 ) 38 39 async for chunk in stream: 40 if not chunk.choices: 41 continue 42 43 choice = chunk.choices[0] 44 delta_content = choice.delta.content 45 46 if delta_content is not None: 47 escaped = delta_content.replace("\n", "\\n") 48 yield f"data: {escaped}\n\n" 49 50 if choice.finish_reason is not None: 51 yield "data: [DONE]\n\n" 52 break
  • Imports + class header: AsyncOpenAI from the official openai package gives non-blocking HTTP, and AsyncGenerator types the streaming return value. The Llama4StreamAdapter class encapsulates all Together.ai-specific configuration so calling code remains provider-agnostic.
  • MODEL_ID: The Together.ai model registry identifier as a class-level constant. The FP8 suffix indicates 8-bit floating-point quantization, which Together.ai uses to maximize throughput on H100 GPUs without meaningful quality loss.
  • __init__: Read TOGETHER_API_KEY from the environment; the explicit None check raises an EnvironmentError with an actionable message pointing to the Together.ai dashboard, preventing cryptic 401 errors at request time. Instantiate AsyncOpenAI with base_url pointing to Together.ai's API gateway — the SDK appends /chat/completions automatically when you call client.chat.completions.create. The 2048-token default caps generation length to prevent runaway completions that consume billing credits.
  • stream_chat signature: Returns AsyncGenerator[str, None]; each yielded value is an SSE frame string and the generator does not accept sent values via .asend().
  • Token budget + create call: The or operator falls back to self.default_max_tokens when the caller omits max_tokens. The await is necessary because the SDK performs the initial HTTP handshake asynchronously before returning the AsyncStream[ChatCompletionChunk] iterator. temperature=0.7 balances creativity and coherence for chat applications.
  • Chunk loop: if not chunk.choices skips keep-alive frames with an empty choices list, preventing IndexError. The delta_content is not None guard skips the first chunk (which typically carries role metadata with delta.content = None) and any null deltas the FP8 checkpoint emits mid-stream. Newlines are escaped to \\n because the SSE specification uses literal newlines as frame delimiters — an unescaped \n would split one logical token into two SSE events.
  • Termination: When finish_reason transitions to a terminal value ("stop" or "length"), yield the [DONE] sentinel and break. The break is defensive — Together.ai may send additional chunks after the finish signal during connection teardown.

Request flow through the adapter

The following diagram traces a single streaming request from the FastAPI endpoint through the Llama4StreamAdapter to Together.ai's inference cluster and back to the browser client. Each arrow represents an async boundary where control yields back to the event loop, enabling concurrent request handling.

Llama4StreamAdapter bridges the /chat/llama4 FastAPI endpoint to Together.ai's completion API by calling stream_chat(messages) with stream=true. Each ChatCompletionChunk carrying a token delta (δ₁, δ₂, …) is reformatted into SSE "data: δ\n\n" frames and flushed to the browser in real time. The sequence terminates when Together.ai returns finish_reason=stop, triggering the final "data: [DONE]\n\n" sentinel that signals the browser to close the event stream.

Code snippet mermaid
Loading diagram...
  • Line 1: Declares this as a Mermaid sequence diagram, which visualizes interactions between participants over time.
  • Lines 2-5: Define the four participants (actors) in the diagram: Browser (the client), FastAPI (the backend server), Llama4StreamAdapter (a streaming adapter layer), and TogetherAI (the Together.ai external API), with TogetherAI given a display alias.
  • Line 7: The Browser sends a synchronous POST request to FastAPI at the /chat/llama4 endpoint, carrying a messages payload.
  • Line 8: FastAPI forwards the request to the Llama4StreamAdapter by calling its stream_chat(messages) method.
  • Line 9: The adapter makes an outbound POST request to Together.ai's /v1/chat/completions endpoint with stream=true to initiate a streaming completion.
  • Lines 10-12: Together.ai returns the first streamed chunk (ChatCompletionChunk) containing token delta δ₁. The adapter formats it as an SSE-encoded string "data: δ₁\n\n" and passes it back to FastAPI, which delivers it to the Browser as a Server-Sent Events frame. Dashed arrows (-->>) indicate these are asynchronous response messages.
  • Lines 13-15: The same streaming pattern repeats for the second token delta δ₂ — Together.ai emits the chunk, the adapter formats it as SSE, FastAPI relays it, and the Browser receives the second SSE frame.
  • Lines 16-18: Together.ai sends a final chunk with finish_reason=stop, signaling the completion is done. The adapter translates this into the SSE termination sentinel "data: [DONE]\n\n", which FastAPI forwards to the Browser as the final SSE frame.
  • Line 19: The Browser sends a message to itself (a self-loop arrow), representing the client-side logic that closes the EventSource connection after receiving the [DONE] signal, ending the streaming session.

Notice that the FastAPI layer never buffers the full response — each SSE frame flows through StreamingResponse the instant the adapter yields it. This keeps memory consumption constant regardless of response length, which matters when Llama 4 Maverick generates multi-thousand-token completions.

FastAPI endpoint with disconnect detection

The final piece connects the adapter to a FastAPI route that returns a StreamingResponse with the correct text/event-stream content type. Client disconnect detection is critical for cost control: if a user navigates away mid-stream, you must cancel the upstream Together.ai request to stop billing. The implementation below wraps the adapter's generator in an outer async generator that catches asyncio.CancelledError, which FastAPI raises when the client TCP connection closes. The route handler instantiates Llama4StreamAdapter, validates the incoming request body against a Pydantic model, and returns the StreamingResponse with appropriate cache-control headers to prevent CDN buffering.

Code snippet python
1import asyncio 2from fastapi import FastAPI, Request 3from fastapi.responses import StreamingResponse 4from pydantic import BaseModel 5 6app = FastAPI() 7 8class ChatRequest(BaseModel): 9 messages: list[dict[str, str]] 10 max_tokens: int | None = None 11 12async def guarded_stream( 13 adapter: Llama4StreamAdapter, 14 request: Request, 15 messages: list[dict[str, str]], 16 max_tokens: int | None, 17) -> AsyncGenerator[str, None]: 18 """Wrap adapter stream with disconnect detection.""" 19 try: 20 async for frame in adapter.stream_chat(messages, max_tokens): 21 if await request.is_disconnected(): 22 break 23 yield frame 24 except asyncio.CancelledError: 25 yield "data: [CANCELLED]\n\n" 26 27@app.post("/chat/llama4") 28async def chat_llama4(body: ChatRequest, request: Request): 29 adapter = Llama4StreamAdapter() 30 generator = guarded_stream(adapter, request, body.messages, body.max_tokens) 31 return StreamingResponse( 32 generator, 33 media_type="text/event-stream", 34 headers={ 35 "Cache-Control": "no-cache", 36 "X-Accel-Buffering": "no", 37 }, 38 )
  • Lines 1-4: Import asyncio for CancelledError handling, FastAPI's Request object for disconnect detection, StreamingResponse for chunked transfer encoding, and BaseModel from Pydantic for request validation.
  • Lines 8-10: Define ChatRequest with messages as a required list of dicts and max_tokens as an optional integer defaulting to None. Pydantic validates that each message dict contains string keys and values at deserialization time.
  • Lines 12-17: The guarded_stream function wraps the adapter's async generator with two protection layers. It accepts the Request object so it can poll request.is_disconnected() between frames.
  • Lines 19-23: Inside the try block, the function iterates over the adapter's SSE frames. Before yielding each frame, it checks await request.is_disconnected() — if the client has closed the connection, the loop breaks cleanly, which triggers garbage collection of the upstream AsyncStream object and cancels the HTTP connection to Together.ai.
  • Lines 24-25: The except asyncio.CancelledError block catches the case where ASGI server-level cancellation fires before the disconnect check runs. Yielding a [CANCELLED] frame is optional but useful for logging middleware that monitors stream termination reasons.
  • Lines 27-29: The route handler instantiates a fresh Llama4StreamAdapter per request. This is safe because the AsyncOpenAI client uses connection pooling internally — creating a new adapter does not open a new TCP connection to Together.ai.
  • Lines 30-37: Return StreamingResponse with media_type="text/event-stream" to set the correct Content-Type header. The Cache-Control: no-cache header prevents reverse proxies from buffering the stream, and X-Accel-Buffering: no disables Nginx's proxy buffering specifically, which is the most common cause of SSE frames arriving in batches rather than individually.

Do's and Don'ts

Do's

  1. Do instantiate AsyncOpenAI with an explicit base_url="https://api.together.xyz/v1" and a TOGETHER_API_KEY sourced from the environment — the SDK appends /chat/completions automatically, so a single constructor override redirects all traffic to Together.ai without touching any other call site in your multi-provider pipeline.
  2. Do guard every chunk with if not chunk.choices: continue and if delta_content is not None before yielding an SSE frame — Together.ai's FP8 checkpoint emits keep-alive frames with empty choices lists and null-content deltas mid-stream; skipping both prevents IndexError and spurious empty frames that confuse browser EventSource parsers.
  3. Do escape newline characters to \\n before yielding each token delta as data: {escaped}\n\n — the SSE wire format uses literal newline pairs as frame delimiters, so an unescaped \n inside a token splits one logical delta into two events and corrupts the reconstructed text on the client.

Don'ts

  1. Don't omit the TOGETHER_API_KEY is None check in __init__ — without it, a missing environment variable propagates as a cryptic 401 from Together.ai at the first stream_chat call rather than an EnvironmentError with an actionable message at startup, making misconfigured deployments nearly impossible to diagnose.
  2. Don't reuse the same AsyncOpenAI client instance across your OpenAI and Together.ai adaptersbase_url and api_key are baked into the client at construction time; sharing one instance means either provider's credentials silently route to the wrong endpoint, producing 401s or billing the wrong account.
  3. Don't continue iterating the async for chunk in stream loop after finish_reason becomes non-None without a break — Together.ai can emit additional chunks during connection teardown after the terminal finish signal; without the defensive break, your generator may yield content or a second [DONE] sentinel after the stream is logically complete, causing double-close errors in FastAPI's StreamingResponse.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Application Engineering subscription.

From · cancel anytime

Listen to this lesson

Audio overviews of this lesson's labs and its chapter, from GenBodha Bytes.

More free lessons in Full-Stack GenAI Applications

All free lessons in GenAI Application Engineering