Prerequisites
Before starting this tutorial, ensure you have:
- Node.js 20+ installed
- Basic knowledge of React and the Next.js App Router
- A Together AI account with an API key
- Familiarity with TypeScript
- A code editor (VS Code recommended)
What You'll Build
Together AI recently closed an $800 million Series C led by Aramco Ventures, cementing its position as the leading platform for open-source AI inference. With access to over 200 models — including Llama 3.3, Mistral, Qwen 2.5, DeepSeek, and Falcon — Together AI offers one of the most flexible multi-model APIs available today.
In this tutorial, you'll build a streaming AI chat application featuring:
- Real-time token streaming using Server-Sent Events (SSE)
- Model switching between popular open-source LLMs
- A clean, responsive chat UI with Next.js 15 App Router
- Type-safe integration with the official Together AI TypeScript SDK
- Production-ready error handling and loading states
Step 1: Create a New Next.js 15 Project
Start by scaffolding a new Next.js 15 project:
npx create-next-app@latest together-chat --typescript --tailwind --app --src-dir
cd together-chatWhen prompted, select:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
Step 2: Install the Together AI SDK
Install the official Together AI TypeScript SDK:
npm install together-aiThe together-ai package is the official SDK maintained by Together AI. It wraps the REST API with full TypeScript types, streaming support, and automatic retries.
Step 3: Configure Environment Variables
Create a .env.local file in your project root:
TOGETHER_API_KEY=your_api_key_hereGet your API key from the Together AI dashboard. Together AI offers $25 in free credits for new accounts — more than enough to complete this tutorial.
Add .env.local to your .gitignore to prevent accidentally committing secrets:
echo ".env.local" >> .gitignoreStep 4: Create the Together AI Client
Create a shared client module at lib/together.ts:
import Together from 'together-ai';
export const together = new Together({
apiKey: process.env.TOGETHER_API_KEY,
});
export const AVAILABLE_MODELS = [
{
id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
name: 'Llama 3.3 70B',
provider: 'Meta',
},
{
id: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
name: 'Mixtral 8x7B',
provider: 'Mistral AI',
},
{
id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',
name: 'Qwen 2.5 72B',
provider: 'Alibaba',
},
{
id: 'deepseek-ai/DeepSeek-V3',
name: 'DeepSeek V3',
provider: 'DeepSeek',
},
];This module exports both the configured Together client and a curated list of popular open-source models available on the platform.
Step 5: Build the Streaming API Route
Create the chat API route at app/api/chat/route.ts:
import { together } from '@/lib/together';
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export const maxDuration = 60;
export async function POST(req: NextRequest) {
const { messages, model } = await req.json();
if (!messages || !Array.isArray(messages)) {
return new Response(JSON.stringify({ error: 'Invalid messages format' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const selectedModel = model || 'meta-llama/Llama-3.3-70B-Instruct-Turbo';
const systemMessage = {
role: 'system' as const,
content: 'You are a helpful AI assistant. Be concise and accurate in your responses.',
};
const stream = await together.chat.completions.create({
messages: [systemMessage, ...messages],
model: selectedModel,
stream: true,
max_tokens: 2048,
temperature: 0.7,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
controller.enqueue(encoder.encode(content));
}
}
} catch (err) {
controller.error(err);
} finally {
controller.close();
}
},
});
return new Response(readable, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
'X-Content-Type-Options': 'nosniff',
},
});
}This route handler:
- Accepts
messages(the conversation history) andmodel(the selected LLM) from the request body - Prepends a system prompt to guide the assistant's behavior
- Calls
together.chat.completions.createwithstream: trueto enable token streaming - Iterates over the async stream and pushes each text chunk into a
ReadableStream - Returns the stream as a plain-text HTTP response
Setting maxDuration = 60 ensures the serverless function stays alive long enough for lengthy completions.
Step 6: Create the Message Types
Add a shared types file at types/chat.ts:
export type Role = 'user' | 'assistant' | 'system';
export interface Message {
id: string;
role: Role;
content: string;
}Step 7: Build the Chat UI Component
Create the main chat component at components/Chat.tsx:
'use client';
import { useState, useRef, useEffect } from 'react';
import { AVAILABLE_MODELS } from '@/lib/together';
import { Message } from '@/types/chat';
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState(AVAILABLE_MODELS[0].id);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content: input.trim(),
};
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
setInput('');
setIsLoading(true);
const assistantId = crypto.randomUUID();
setMessages(prev => [
...prev,
{ id: assistantId, role: 'assistant', content: '' },
]);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: updatedMessages.map(m => ({
role: m.role,
content: m.content,
})),
model: selectedModel,
}),
});
if (!res.ok) throw new Error('Failed to fetch response');
if (!res.body) throw new Error('No response body');
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const result = await reader.read();
if (result.done) break;
const text = decoder.decode(result.value, { stream: true });
setMessages(prev =>
prev.map(m =>
m.id === assistantId
? { ...m, content: m.content + text }
: m
)
);
}
} catch (err) {
setMessages(prev =>
prev.map(m =>
m.id === assistantId
? { ...m, content: 'Error: Could not get a response. Please try again.' }
: m
)
);
} finally {
setIsLoading(false);
}
}
const modelInfo = AVAILABLE_MODELS.find(m => m.id === selectedModel);
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto px-4">
<div className="flex items-center justify-between py-4 border-b">
<h1 className="text-xl font-semibold">Together AI Chat</h1>
<select
value={selectedModel}
onChange={e => setSelectedModel(e.target.value)}
className="text-sm border rounded-lg px-3 py-1.5 bg-white dark:bg-zinc-900"
>
{AVAILABLE_MODELS.map(m => (
<option key={m.id} value={m.id}>
{m.name} ({m.provider})
</option>
))}
</select>
</div>
<div className="flex-1 overflow-y-auto py-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-zinc-400 mt-20">
<p className="text-lg font-medium">Start a conversation</p>
<p className="text-sm mt-1">
Using {modelInfo?.name} by {modelInfo?.provider}
</p>
</div>
)}
{messages.map(message => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2.5 text-sm ${
message.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100'
}`}
>
{message.content || (
<span className="animate-pulse">Thinking...</span>
)}
</div>
</div>
))}
<div ref={bottomRef} />
</div>
<form onSubmit={handleSubmit} className="py-4 border-t">
<div className="flex gap-2">
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Type a message..."
className="flex-1 border rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-zinc-900"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-4 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
>
Send
</button>
</div>
</form>
</div>
);
}Key patterns in this component:
- Optimistic UI — the user message appears immediately before the API responds
- Streaming —
ReadableStreamDefaultReaderreads chunks as they arrive and appends them to the assistant message in real time - Model switching — a dropdown lets the user switch between Llama, Mixtral, Qwen, and DeepSeek at any time
- Auto-scroll — the chat view scrolls to the latest message as new tokens arrive
Step 8: Wire Up the Page
Update app/page.tsx to render the chat:
import Chat from '@/components/Chat';
export default function Home() {
return <Chat />;
}Step 9: Test Your Implementation
Start the development server:
npm run devOpen http://localhost:3000 in your browser. You should see:
- A chat interface with a model selector in the header
- An empty state prompting you to start a conversation
- Real-time streaming text as the AI responds token by token
Test these scenarios to validate everything works:
- Send a simple question and verify streaming renders in real time
- Switch models between messages
- Ask for a long code snippet to test buffer behavior under sustained streaming
Step 10: Add Usage Tracking (Optional)
Together AI's response stream includes token usage in the final chunk. Capture it for monitoring and cost control:
let usage: { total_tokens: number; prompt_tokens: number; completion_tokens: number } | null = null;
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
controller.enqueue(encoder.encode(content));
}
if (chunk.usage) {
usage = chunk.usage;
}
}
if (usage) {
console.log(
`Tokens — prompt: ${usage.prompt_tokens}, ` +
`completion: ${usage.completion_tokens}, ` +
`total: ${usage.total_tokens}`
);
}Use this data to log per-request costs, enforce per-user token budgets, or feed a monitoring dashboard.
Step 11: Deploy to Vercel
Deploy with zero configuration:
npx vercelAdd your environment variable in the Vercel dashboard under Settings → Environment Variables, or via CLI:
vercel env add TOGETHER_API_KEYTogether AI's global inference infrastructure is optimized for low latency, making it a natural fit for Vercel's serverless and edge platforms.
Troubleshooting
Stream stops mid-response
Increase maxDuration in the route handler. Set export const maxDuration = 60 to extend the serverless timeout beyond the default 10 seconds.
TOGETHER_API_KEY is not defined error
Ensure .env.local is in the project root, then restart the dev server. Next.js reads environment variables only at startup.
Model returns an error
Some Together AI models require specific prompt formats. Check the Together AI documentation for system prompt compatibility notes per model family.
Streaming does not work in production
Confirm you are using runtime = 'nodejs' in the route handler. The Edge runtime has limitations with long-running streams from third-party SDKs.
Next Steps
Now that your streaming chat application is working, consider these extensions:
- Conversation persistence — save chat history to Turso or Supabase between sessions
- Tool calling — use Together AI's function calling support for web search or code execution
- RAG pipeline — combine Together AI embeddings with LanceDB for context-aware answers over your own documents
- Fine-tuning — Together AI supports LoRA fine-tuning on custom datasets via the same
together-aiSDK - Image generation — explore Together AI's FLUX-based image models alongside chat completions
Conclusion
You've built a production-ready multi-model streaming AI chat application using Together AI's TypeScript SDK and Next.js 15. The platform's OpenAI-compatible API makes it straightforward to switch between 200+ open-source models, and the official SDK handles streaming, type safety, and retries automatically.
With Together AI's $800M Series C backing from Aramco Ventures, the platform is positioned to significantly expand its model catalog and global infrastructure — making this an excellent foundation to build AI products on.