Noqta
  • Home
  • Services
  • About us
  • Writing
  • Sign in
writing/tutorial/2026/07
● TutorialJul 10, 2026·30 min read

Building a Multi-Model Streaming AI Chat with Together AI SDK and Next.js 15

Learn how to build a production-ready streaming AI chat application using Together AI's TypeScript SDK and Next.js 15 App Router. This tutorial covers SDK setup, streaming API routes, real-time UI updates, and multi-model switching across 200+ open-source models.

AI Bot
AI Bot
Author
·EN · FR · AR

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-chat

When 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-ai

The 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_here

Get 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" >> .gitignore

Step 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:

  1. Accepts messages (the conversation history) and model (the selected LLM) from the request body
  2. Prepends a system prompt to guide the assistant's behavior
  3. Calls together.chat.completions.create with stream: true to enable token streaming
  4. Iterates over the async stream and pushes each text chunk into a ReadableStream
  5. 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 — ReadableStreamDefaultReader reads 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 dev

Open http://localhost:3000 in your browser. You should see:

  1. A chat interface with a model selector in the header
  2. An empty state prompting you to start a conversation
  3. 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 vercel

Add your environment variable in the Vercel dashboard under Settings → Environment Variables, or via CLI:

vercel env add TOGETHER_API_KEY

Together 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-ai SDK
  • 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.

● Tags
#together-ai#nextjs#typescript#ai#streaming#intermediate#30 min read
● Share
● A question?

Talk to a Noqta agent about this article.

AI Bot
AI Bot
Author · noqta
Follow ↗

● Read next

AI Chatbot Integration Guide: Build Intelligent Conversational Interfaces
● Tutorial

AI Chatbot Integration Guide: Build Intelligent Conversational Interfaces

Jan 25, 2026
AI SDK Tutorial Hub: Your Complete Guide to Building AI Applications
● Tutorial

AI SDK Tutorial Hub: Your Complete Guide to Building AI Applications

Jan 25, 2026
Building an AI-Powered SQL Analysis Tool
● Tutorial

Building an AI-Powered SQL Analysis Tool

Nov 25, 2024
Noqta
Terms and Conditions · Privacy Policy
Services
  • AI Automation
  • AI Agents
  • CX Automation
  • Vibe Coding
  • Project Management
  • Quality Assurance
  • Web Development
  • API Integration
  • Business Applications
  • Maintenance
  • Low-Code/No-Code
Links
  • About Us
  • How It Works?
  • News
  • Tutorials
  • Blog
  • Contact
  • FAQ
  • Resources
Regions
  • Saudi Arabia
  • UAE
  • Qatar
  • Bahrain
  • Oman
  • Libya
  • Tunisia
  • Algeria
  • Morocco
Company
  • Noqta, Tunisia, Tunis, phone +216 40 385 594
© Noqta. All rights reserved.