Streaming

Stream chat completions in real-time with Server-Sent Events

Overview

Stream chat completions token-by-token to provide better user experience and lower perceived latency. Perfect for chatbots, live coding assistants, and interactive applications.

Authentication: API Key or OAuth token
Required Scopes (OAuth): ai:chat

Benefits

  • Immediate Feedback - Users see responses as they're generated
  • Lower Latency - First token arrives in ~1 second
  • Better UX - Feels much faster than waiting for complete response
  • Real-time Updates - Show typing indicators and progress

Stream Chat Completion

Generate a streaming chat completion response.

Endpoint: POST /api/v1/chat/completions

import OpenAI from 'openai'
 
const client = new OpenAI({
  apiKey: process.env.GATEWAY_API_KEY,
  baseURL: 'https://hypery.ai/api/v1'
})
 
const stream = await client.chat.completions.create({
  model: 'anthropic/claude-3.5-sonnet',
  messages: [
    { role: 'user', content: 'Write a poem about coding' }
  ],
  stream: true
})
 
for await (const chunk of stream) {
  const content = chunk.choices[0]?.delta?.content || ''
  process.stdout.write(content)
}

Input Parameters

All standard chat completion parameters are supported. Set stream: true to enable streaming.

ParameterTypeRequiredDescription
modelstringYesModel ID to use
messagesarrayYesArray of message objects
streambooleanYesMust be true for streaming
max_tokensnumberNoMaximum tokens to generate
temperaturenumberNoSampling temperature 0-2
top_pnumberNoNucleus sampling 0-1
stopstring | arrayNoStop sequences

Response: Server-Sent Events stream

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1699564800,"model":"anthropic/claude-3.5-sonnet","choices":[{"index":0,"delta":{"role":"assistant","content":"In"},"finish_reason":null}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1699564800,"model":"anthropic/claude-3.5-sonnet","choices":[{"index":0,"delta":{"content":" lines"},"finish_reason":null}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1699564800,"model":"anthropic/claude-3.5-sonnet","choices":[{"index":0,"delta":{"content":" of"},"finish_reason":null}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1699564800,"model":"anthropic/claude-3.5-sonnet","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Output Fields (Per Chunk)

FieldTypeDescription
idstringUnique completion ID (same across all chunks)
objectstringAlways chat.completion.chunk
creatednumberUnix timestamp
modelstringModel that generated the response
choices[].indexnumberChoice index (usually 0)
choices[].deltaobjectIncremental content update
choices[].delta.rolestringRole (assistant) - only in first chunk
choices[].delta.contentstringToken(s) being streamed
choices[].finish_reasonstring | nullCompletion reason when done (stop, length, tool_calls)

React Integration

Build a streaming chat interface with React:

'use client'
 
import { useState } from 'react'
import OpenAI from 'openai'
 
export function ChatStream() {
  const [response, setResponse] = useState('')
  const [loading, setLoading] = useState(false)
 
  const handleStream = async (prompt) => {
    setLoading(true)
    setResponse('')
 
    const client = new OpenAI({
      apiKey: process.env.NEXT_PUBLIC_GATEWAY_API_KEY,
      baseURL: 'https://hypery.ai/api/v1',
      dangerouslyAllowBrowser: true
    })
 
    const stream = await client.chat.completions.create({
      model: 'anthropic/claude-3.5-sonnet',
      messages: [{ role: 'user', content: prompt }],
      stream: true
    })
 
    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content || ''
      setResponse(prev => prev + content)
    }
 
    setLoading(false)
  }
 
  return (
    <div>
      <button onClick={() => handleStream('Tell me a joke')}>
        Stream Response
      </button>
      <div className="whitespace-pre-wrap">{response}</div>
      {loading && <span className="animate-pulse">▊</span>}
    </div>
  )
}

Raw Fetch API

Stream without using the OpenAI SDK:

async function streamChat(prompt) {
  const response = await fetch(
    'https://hypery.ai/api/v1/chat/completions',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'anthropic/claude-3.5-sonnet',
        messages: [{ role: 'user', content: prompt }],
        stream: true
      })
    }
  )
 
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
 
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
 
    const chunk = decoder.decode(value)
    const lines = chunk.split('\n').filter(line => line.trim())
 
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6)
        if (data === '[DONE]') return
 
        try {
          const parsed = JSON.parse(data)
          const content = parsed.choices[0]?.delta?.content || ''
          process.stdout.write(content)
        } catch (e) {
          // Skip invalid JSON
        }
      }
    }
  }
}

Streaming with Function Calling

Function arguments are streamed incrementally when using tools.

const stream = await client.chat.completions.create({
  model: 'anthropic/claude-3.5-sonnet',
  messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
  tools: [
    {
      type: 'function',
      function: {
        name: 'get_weather',
        description: 'Get weather for a location',
        parameters: {
          type: 'object',
          properties: {
            location: { type: 'string' }
          },
          required: ['location']
        }
      }
    }
  ],
  stream: true
})
 
let functionArgs = ''
 
for await (const chunk of stream) {
  const delta = chunk.choices[0].delta
  
  if (delta.tool_calls) {
    const args = delta.tool_calls[0]?.function?.arguments || ''
    functionArgs += args
  }
  
  if (chunk.choices[0].finish_reason === 'tool_calls') {
    const parsed = JSON.parse(functionArgs)
    console.log('Calling:', parsed)
  }
}

Handle Stream Completion

Detect when the stream finishes and why:

for await (const chunk of stream) {
  const choice = chunk.choices[0]
  
  if (choice.delta.content) {
    process.stdout.write(choice.delta.content)
  }
  
  if (choice.finish_reason === 'stop') {
    console.log('\n✓ Stream complete')
  } else if (choice.finish_reason === 'length') {
    console.log('\n⚠ Max tokens reached')
  } else if (choice.finish_reason === 'tool_calls') {
    console.log('\n🔧 Function called')
  }
}

Finish Reasons

ReasonDescription
stopModel completed naturally
lengthReached max_tokens limit
tool_callsModel wants to call a function
content_filterResponse filtered for safety

Error Handling

Handle stream errors gracefully:

try {
  const stream = await client.chat.completions.create({
    model: 'anthropic/claude-3.5-sonnet',
    messages: [{ role: 'user', content: 'Hello' }],
    stream: true
  })
 
  for await (const chunk of stream) {
    process.stdout.write(chunk.choices[0]?.delta?.content || '')
  }
} catch (error) {
  if (error.code === 'ECONNABORTED') {
    console.error('Stream timeout')
  } else if (error.status === 429) {
    console.error('Insufficient credits')
  } else if (error.status === 402) {
    console.error('Spending limit exceeded')
  } else {
    console.error('Stream error:', error.message)
  }
}

Best Practices

Implement Timeout

Prevent hanging connections:

async function streamWithTimeout(prompt, timeoutMs = 30000) {
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), timeoutMs)
 
  try {
    const stream = await client.chat.completions.create({
      model: 'anthropic/claude-3.5-sonnet',
      messages: [{ role: 'user', content: prompt }],
      stream: true
    }, { signal: controller.signal })
 
    for await (const chunk of stream) {
      // Process chunk
    }
  } finally {
    clearTimeout(timeout)
  }
}

Buffer UI Updates

Batch updates for smoother rendering:

let buffer = ''
const flushInterval = 50 // ms
 
const interval = setInterval(() => {
  if (buffer) {
    setResponse(prev => prev + buffer)
    buffer = ''
  }
}, flushInterval)
 
for await (const chunk of stream) {
  buffer += chunk.choices[0]?.delta?.content || ''
}
 
clearInterval(interval)

Use Smaller Models

For faster time-to-first-token:

ModelSpeedQuality
openai/gpt-4o-miniFastGood
anthropic/claude-3-haikuFastGood
openai/gpt-4oMediumExcellent
anthropic/claude-3.5-sonnetMediumExcellent

Next Steps