Streaming chat completion
Stream tokens as they are generated from a chat completion.
This example demonstrates real-time streaming chat completions where tokens appear as the model generates them. Users send messages and see the assistant's response stream in progressively.
- Live demo: react.fency.ai/streaming-chat-completion
- GitHub folder: fency-react-examples/app/streaming-chat-completion
Prerequisites
You need a publishable key for the React SDK and a secret key for your server routes. This example uses sessions to authenticate the client.
The example uses Clerk for user authentication. That is part of the app shell and not specific to Fency.
Server session routes
The example defines two session routes: one for stream sessions and one for
agent task sessions. Each route is standalone so you can read a single file
and see the full POST /v1/sessions call.
Create the stream session route at api/create-stream-session/route.ts:
import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
export async function POST() {
const userId = await getAuthorizedUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const secretKey = process.env.FENCY_SECRET_KEY
if (!secretKey) {
throw new Error('FENCY_SECRET_KEY is not defined.')
}
const response = await fetch('https://api.fency.ai/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ createStream: {} }),
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
}Create the agent task session route at api/create-agent-task-session/route.ts:
import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
export async function POST() {
const userId = await getAuthorizedUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const secretKey = process.env.FENCY_SECRET_KEY
if (!secretKey) {
throw new Error('FENCY_SECRET_KEY is not defined.')
}
const response = await fetch('https://api.fency.ai/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
createAgentTask: {
taskType: 'STREAMING_CHAT_COMPLETION',
metadata: { userId },
},
}),
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
}Client provider and hooks
Both session routes respond with a client token. Parse that response with a
Zod schema in sessionClientTokenSchema.ts so a bad shape throws:
import { z } from 'zod'
export const sessionClientTokenSchema = z.object({
clientToken: z.string(),
})Load the Fency client and wrap your component tree with FencyProvider in page.tsx:
'use client'
import { loadFency } from '@fencyai/js'
import { FencyProvider } from '@fencyai/react'
import { Chat } from './components/Chat'
import { sessionClientTokenSchema } from './sessionClientTokenSchema'
const fency = loadFency({
publishableKey: process.env.NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY!,
})
async function fetchCreateStreamClientToken() {
const res = await fetch('/streaming-chat-completion/api/create-stream-session', {
method: 'POST',
})
if (!res.ok) {
throw new Error('Failed to create stream session')
}
const { clientToken } = sessionClientTokenSchema.parse(await res.json())
return { clientToken }
}
export default function StreamingChatCompletionPage() {
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<FencyProvider
fency={fency}
fetchCreateStreamClientToken={fetchCreateStreamClientToken}
>
<Chat />
</FencyProvider>
</div>
)
}Use useAgentTasks and createAgentTask to send messages and pair each user
message with its streaming task in hooks/useStreamingChat.ts:
'use client'
import { useAgentTasks, type AgentTask } from '@fencyai/react'
import { useState } from 'react'
import type { ChatMessage } from '../ChatMessage'
import { sessionClientTokenSchema } from '../sessionClientTokenSchema'
export type Turn = {
userMessage: ChatMessage
agentTask?: AgentTask
}
async function fetchCreateAgentTaskClientToken() {
const res = await fetch(
'/streaming-chat-completion/api/create-agent-task-session',
{ method: 'POST' },
)
if (!res.ok) {
throw new Error('Failed to create agent task session')
}
const { clientToken } = sessionClientTokenSchema.parse(await res.json())
return { clientToken }
}
export function useStreamingChat() {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const [taskOffset, setTaskOffset] = useState(0)
const { agentTasks, createAgentTask } = useAgentTasks({})
const streamingTasks = agentTasks
.filter((task) => task.params.type === 'StreamingChatCompletion')
.slice(taskOffset)
const turns: Turn[] = messages
.filter((message) => message.role === 'USER')
.map((userMessage, index) => ({
userMessage,
agentTask: streamingTasks[index],
}))
async function sendMessage(text: string) {
setIsSubmitting(true)
const nextMessages: ChatMessage[] = [
...messages,
{ role: 'USER', content: text },
]
setMessages(nextMessages)
try {
const response = await createAgentTask(
{
type: 'StreamingChatCompletion',
messages: nextMessages,
model: 'anthropic/claude-sonnet-4.6',
},
{ fetchCreateAgentTaskClientToken },
)
if (
response.type === 'success' &&
response.response.taskType === 'StreamingChatCompletion'
) {
const assistant = response.response.response.messages.at(-1)
if (assistant?.role === 'ASSISTANT') {
setMessages([
...nextMessages,
{ role: 'ASSISTANT', content: assistant.content },
])
}
}
} finally {
setIsSubmitting(false)
}
}
function startNewChat() {
setMessages([])
setTaskOffset(agentTasks.length)
}
return { turns, isSubmitting, sendMessage, startNewChat }
}Render each turn with AgentTaskProgress in components/ChatTurn.tsx:
import { Alert } from '@mantine/core'
import { AgentTaskProgress } from '@fencyai/react'
import type { Turn } from '../hooks/useStreamingChat'
import { Bubble } from './Bubble'
export function ChatTurn({ turn }: { turn: Turn }) {
const { userMessage, agentTask } = turn
return (
<div>
<Bubble message={userMessage} />
{agentTask?.error ? (
<Alert color="red" mb="md">
{agentTask.error.message}
</Alert>
) : agentTask ? (
<div className="mb-4 w-full">
<AgentTaskProgress agentTask={agentTask} />
</div>
) : null}
</div>
)
}The AgentTaskProgress component displays the streaming text as it arrives.
components/Chat.tsx maps the hook's turns to ChatTurn and wires
sendMessage to the composer form in components/ChatComposer.tsx.
Running locally
Clone the repository and install dependencies:
git clone https://github.com/fencyai/fency-react-examples.git
cd fency-react-examples
npm installCopy .env.example to .env.local and add your Fency keys:
cp .env.example .env.localSet the keys in .env.local:
FENCY_SECRET_KEY=sk_...
NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY=pk_...Initialize Clerk authentication (provisions a development app):
npx -y clerk@latest init --keyless -yStart the development server:
npm run devOpen http://localhost:3000/streaming-chat-completion. Sign up from the header, then send a message to see tokens stream in real time.