Fency.ai

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.

Prerequisites

You need a publishable key for the React SDK and a secret key for your server routes. This example uses sessions and client tokens 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.

Create a shared helper api/createFencySession.ts:

import 'server-only'

import { NextResponse } from 'next/server'

export async function createFencySession(body: Record<string, unknown>) {
  const secretKey = process.env.FENCY_SECRET_KEY
  if (!secretKey) {
    throw new Error('FENCY_SECRET_KEY is not defined.')
  }

  try {
    const response = await fetch('https://api.fency.ai/v1/sessions', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${secretKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
    const data = await response.json()
    return NextResponse.json(data, { status: response.status })
  } catch (error) {
    console.error('Fency session API error:', error)
    return NextResponse.json(
      { error: 'Failed to create session' },
      { status: 502 },
    )
  }
}

Create the stream session route at api/stream-session/route.ts:

import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { createFencySession } from '../createFencySession'

export async function POST() {
  const userId = await getAuthorizedUserId()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  return createFencySession({ createStream: {} })
}

Create the agent task session route at api/agent-task-session/route.ts:

import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { createFencySession } from '../createFencySession'

export async function POST() {
  const userId = await getAuthorizedUserId()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  return createFencySession({
    createAgentTask: {
      taskType: 'STREAMING_CHAT_COMPLETION',
      metadata: { userId },
    },
  })
}

Client provider and hooks

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'

const fency = loadFency({
  publishableKey: process.env.NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY!,
})

async function fetchCreateStreamClientToken() {
  const res = await fetch('/streaming-chat-completion/api/stream-session', {
    method: 'POST',
  })
  if (!res.ok) {
    throw new Error('Failed to create stream session')
  }
  const data = (await res.json()) as { clientToken?: string }
  if (!data.clientToken) {
    throw new Error('No clientToken in session response')
  }
  return { clientToken: data.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 display streaming progress in components/Chat.tsx:

import { AgentTaskProgress, useAgentTasks } from '@fencyai/react'
import { useState } from 'react'

export function Chat() {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [input, setInput] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const { agentTasks, createAgentTask } = useAgentTasks({})

  async function sendMessage(text: string) {
    const trimmed = text.trim()
    if (!trimmed || isSubmitting) {
      return
    }

    setIsSubmitting(true)
    const nextMessages: ChatMessage[] = [
      ...messages,
      { role: 'USER', content: trimmed },
    ]
    setMessages(nextMessages)

    try {
      const response = await createAgentTask(
        {
          type: 'StreamingChatCompletion',
          messages: nextMessages,
          model: 'anthropic/claude-sonnet-4.6',
        },
        {
          fetchCreateAgentTaskClientToken: async () => {
            const res = await fetch(
              '/streaming-chat-completion/api/agent-task-session',
              { method: 'POST' },
            )
            if (!res.ok) {
              throw new Error('Failed to create agent task session')
            }
            const data = (await res.json()) as { clientToken?: string }
            if (!data.clientToken) {
              throw new Error('No clientToken in session response')
            }
            return { clientToken: data.clientToken }
          },
        },
      )

      if (
        response.type !== 'success' ||
        response.response.taskType !== 'StreamingChatCompletion'
      ) {
        return
      }

      const assistant = response.response.response.messages.at(-1)
      if (assistant?.role === 'ASSISTANT') {
        setMessages([
          ...nextMessages,
          { role: 'ASSISTANT', content: assistant.content },
        ])
      }
    } catch {
      // Task errors also surface on the agent task
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <div>
      {/* Render messages and show AgentTaskProgress for streaming tasks */}
      {agentTasks.map((task) => (
        <AgentTaskProgress key={task.id} agentTask={task} />
      ))}
    </div>
  )
}

The AgentTaskProgress component displays the streaming text as it arrives.

Running locally

Clone the repository and install dependencies:

git clone https://github.com/fencyai/fency-react-examples.git
cd fency-react-examples
npm install

Copy .env.example to .env.local and add your Fency keys:

cp .env.example .env.local

Set 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 -y

Start the development server:

npm run dev

Open http://localhost:3000/streaming-chat-completion. Sign up from the header, then send a message to see tokens stream in real time.

On this page