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\nframes over a long-lived response, consumed in the browser via theEventSourceAPI. base_urloverride: TheAsyncOpenAIconstructor parameter that redirects all SDK HTTP traffic fromapi.openai.comto any OpenAI-compatible endpoint such ashttps://api.together.xyz/v1.StreamingResponse: FastAPI's responseclassthat consumes anasyncgenerator 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/completionsrequest 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:
-
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.
-
Token counting — Together.ai does not
returnusage 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. -
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 +
classheader:AsyncOpenAIfrom the officialopenaipackage gives non-blocking HTTP, andAsyncGeneratortypes the streamingreturnvalue. TheLlama4StreamAdapterclass 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__: ReadTOGETHER_API_KEYfrom the environment; the explicit None check raises anEnvironmentErrorwith an actionable message pointing to the Together.ai dashboard, preventing cryptic 401 errors at request time. InstantiateAsyncOpenAIwithbase_urlpointing to Together.ai's API gateway — the SDK appends/chat/completionsautomatically when you callclient.chat.completions.create. The 2048-token default caps generation length to prevent runaway completions that consume billing credits.stream_chatsignature: ReturnsAsyncGenerator[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
oroperator falls back toself.default_max_tokenswhen the caller omitsmax_tokens. Theawaitis necessary because the SDK performs the initial HTTP handshake asynchronously before returning theAsyncStream[ChatCompletionChunk]iterator.temperature=0.7balances creativity and coherence for chat applications. - Chunk loop:
if not chunk.choicesskips keep-alive frames with an emptychoiceslist, preventingIndexError. Thedelta_content is not Noneguard skips the first chunk (which typically carries role metadata withdelta.content = None) and any null deltas the FP8 checkpoint emits mid-stream. Newlines are escaped to\\nbecause the SSE specification uses literal newlines as frame delimiters — an unescaped\nwould split one logical token into two SSE events. - Termination: When
finish_reasontransitions to a terminal value ("stop"or"length"),yieldthe[DONE]sentinel and break. Thebreakis 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), andTogetherAI(the Together.ai external API), withTogetherAIgiven a display alias. - Line 7: The Browser sends a synchronous POST request to FastAPI at the
/chat/llama4endpoint, 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/completionsendpoint withstream=trueto 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
asyncioforCancelledErrorhandling, FastAPI'sRequestobject for disconnect detection,StreamingResponsefor chunked transfer encoding, andBaseModelfrom Pydantic for request validation. - Lines 8-10: Define
ChatRequestwithmessagesas a required list of dicts andmax_tokensas 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_streamfunction wraps the adapter'sasyncgenerator with two protection layers. It accepts theRequestobject so it can pollrequest.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 upstreamAsyncStreamobject and cancels the HTTP connection to Together.ai. - Lines 24-25: The
except asyncio.CancelledErrorblock 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
Llama4StreamAdapterper request. This is safe because theAsyncOpenAIclient uses connection pooling internally — creating a new adapter does not open a new TCP connection to Together.ai. - Lines 30-37: Return
StreamingResponsewithmedia_type="text/event-stream"to set the correctContent-Typeheader. TheCache-Control: no-cacheheader prevents reverse proxies from buffering the stream, andX-Accel-Buffering: nodisables 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
- ✓Do instantiate
AsyncOpenAIwith an explicitbase_url="https://api.together.xyz/v1"and aTOGETHER_API_KEYsourced from the environment — the SDK appends/chat/completionsautomatically, so a single constructor override redirects all traffic to Together.ai without touching any other call site in your multi-provider pipeline. - ✓Do guard every chunk with
if not chunk.choices: continueandif delta_content is not Nonebefore yielding an SSE frame — Together.ai's FP8 checkpoint emits keep-alive frames with emptychoiceslists and null-content deltas mid-stream; skipping both preventsIndexErrorand spurious empty frames that confuse browserEventSourceparsers. - ✓Do escape newline characters to
\\nbefore yielding each token delta asdata: {escaped}\n\n— the SSE wire format uses literal newline pairs as frame delimiters, so an unescaped\ninside a token splits one logical delta into two events and corrupts the reconstructed text on the client.
Don'ts
- ✗Don't omit the
TOGETHER_API_KEY is Nonecheck in__init__— without it, a missing environment variable propagates as a cryptic 401 from Together.ai at the firststream_chatcall rather than anEnvironmentErrorwith an actionable message at startup, making misconfigured deployments nearly impossible to diagnose. - ✗Don't reuse the same
AsyncOpenAIclient instance across your OpenAI and Together.ai adapters —base_urlandapi_keyare 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. - ✗Don't continue iterating the
async for chunk in streamloop afterfinish_reasonbecomes non-None without abreak— Together.ai can emit additional chunks during connection teardown after the terminal finish signal; without the defensivebreak, your generator may yield content or a second[DONE]sentinel after the stream is logically complete, causing double-close errors in FastAPI'sStreamingResponse.
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.
- Chat Completion API with StreamingChapter overview23 min
More free lessons in Full-Stack GenAI Applications
- Ch 1Build a FastAPI SSE streaming response endpoint
- Ch 1Implement an OpenAI GPT-4o streaming adapter
- Ch 1Implement a Gemini 2.5 Flash streaming adapter with thinking budget
- Ch 1Implement an Anthropic Claude streaming adapter
- Ch 1Build a Llama 4 Maverick streaming adapter via Together.aiYou are here
- Ch 3Implement Anthropic prompt caching with cache_control markers
- Ch 8Build an MCP server exposing business logic as tools