-
-
Notifications
You must be signed in to change notification settings - Fork 1
Streaming Responses
This guide covers everything you need to know about using streaming responses with php-chatbot.
- Overview
- Supported Models
- Basic Usage
- Backend Implementation
- Frontend Integration
- Error Handling
- Best Practices
- Troubleshooting
Streaming responses allow your chatbot to send replies as they are generated, rather than waiting for the complete response. This creates a more interactive and responsive user experience, similar to how ChatGPT displays responses.
Benefits:
- Better UX: Users see responses appearing in real-time
- Perceived Performance: Feels faster even though total time may be similar
- Progressive Disclosure: Long responses are more digestible
- Modern Experience: Matches expectations from popular AI interfaces
| Provider | Streaming Support | Notes |
|---|---|---|
| OpenAI | ✅ Yes | All models (GPT-4, GPT-3.5, etc.) |
| Anthropic | ✅ Yes | All Claude models |
| Google Gemini | ✅ Yes | All Gemini models |
| xAI | ✅ Yes | All Grok models |
| Meta | ✅ Yes | All LLaMA models |
| DeepSeek | ❌ No | Coming soon |
| Ollama | ❌ No | Coming soon |
| DefaultAiModel | ❌ No | Fallback model doesn't support streaming |
<?php
use Rumenx\PhpChatbot\PhpChatbot;
use Rumenx\PhpChatbot\Models\OpenAiModel;
// Create model and chatbot
$model = new OpenAiModel($_ENV['OPENAI_API_KEY']);
$chatbot = new PhpChatbot($model);
// Stream response
foreach ($chatbot->askStream('Explain quantum computing in simple terms') as $chunk) {
echo $chunk;
flush(); // Important: Send to output immediately
}Always check if a model supports streaming before using askStream():
<?php
use Rumenx\PhpChatbot\Contracts\StreamableModelInterface;
if ($model instanceof StreamableModelInterface && $model->supportsStreaming()) {
// Use streaming
foreach ($chatbot->askStream($message) as $chunk) {
echo $chunk;
}
} else {
// Fallback to regular response
$reply = $chatbot->ask($message);
echo $reply;
}Streaming respects all context parameters:
<?php
$context = [
'prompt' => 'You are a helpful coding assistant.',
'temperature' => 0.7,
'max_tokens' => 500
];
foreach ($chatbot->askStream('How do I write a PHP function?', $context) as $chunk) {
echo $chunk;
}<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Rumenx\PhpChatbot\PhpChatbot;
use Rumenx\PhpChatbot\Models\ModelFactory;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ChatbotController extends Controller
{
public function stream(Request $request): StreamedResponse
{
$message = $request->input('message');
$config = config('phpchatbot');
$model = ModelFactory::make($config);
$chatbot = new PhpChatbot($model, $config);
return new StreamedResponse(function () use ($chatbot, $message) {
// Set SSE headers
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Disable nginx buffering
try {
foreach ($chatbot->askStream($message) as $chunk) {
echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
flush();
}
echo "data: [DONE]\n\n";
flush();
} catch (\Exception $e) {
echo "data: " . json_encode(['error' => $e->getMessage()]) . "\n\n";
flush();
}
});
}
}<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Rumenx\PhpChatbot\PhpChatbot;
use Rumenx\PhpChatbot\Models\ModelFactory;
class ChatbotController extends AbstractController
{
public function stream(Request $request): StreamedResponse
{
$message = $request->request->get('message');
$config = require $this->getParameter('kernel.project_dir').'/src/Config/phpchatbot.php';
$model = ModelFactory::make($config);
$chatbot = new PhpChatbot($model, $config);
return new StreamedResponse(function () use ($chatbot, $message) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
foreach ($chatbot->askStream($message) as $chunk) {
echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
flush();
}
echo "data: [DONE]\n\n";
flush();
});
}
}<?php
// api/stream.php
require_once __DIR__ . '/../vendor/autoload.php';
use Rumenx\PhpChatbot\PhpChatbot;
use Rumenx\PhpChatbot\Models\OpenAiModel;
// Set SSE headers
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
// Disable output buffering
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', false);
$message = $_POST['message'] ?? 'Hello';
$model = new OpenAiModel($_ENV['OPENAI_API_KEY']);
$chatbot = new PhpChatbot($model);
try {
foreach ($chatbot->askStream($message) as $chunk) {
echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
echo "data: [DONE]\n\n";
flush();
} catch (Exception $e) {
echo "data: " . json_encode(['error' => $e->getMessage()]) . "\n\n";
flush();
}function streamChatResponse(message) {
const chatDisplay = document.getElementById('chat-response');
chatDisplay.textContent = '';
const eventSource = new EventSource(`/api/chatbot/stream?message=${encodeURIComponent(message)}`);
eventSource.onmessage = function(event) {
if (event.data === '[DONE]') {
eventSource.close();
console.log('Stream completed');
return;
}
try {
const data = JSON.parse(event.data);
if (data.error) {
console.error('Stream error:', data.error);
chatDisplay.textContent += '\n[Error: ' + data.error + ']';
eventSource.close();
return;
}
if (data.chunk) {
chatDisplay.textContent += data.chunk;
}
} catch (e) {
console.error('Parse error:', e);
}
};
eventSource.onerror = function(error) {
console.error('EventSource error:', error);
eventSource.close();
};
}
// Usage
streamChatResponse('What is machine learning?');import React, { useState, useEffect, useRef } from 'react';
function StreamingChat() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const eventSourceRef = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
if (!message.trim() || isStreaming) return;
setResponse('');
setIsStreaming(true);
// Close any existing connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
const eventSource = new EventSource(
`/api/chatbot/stream?message=${encodeURIComponent(message)}`
);
eventSourceRef.current = eventSource;
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
setIsStreaming(false);
return;
}
try {
const data = JSON.parse(event.data);
if (data.chunk) {
setResponse(prev => prev + data.chunk);
}
} catch (e) {
console.error('Parse error:', e);
}
};
eventSource.onerror = () => {
eventSource.close();
setIsStreaming(false);
};
};
useEffect(() => {
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
}, []);
return (
<div className="streaming-chat">
<form onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Ask something..."
disabled={isStreaming}
/>
<button type="submit" disabled={isStreaming}>
{isStreaming ? 'Streaming...' : 'Send'}
</button>
</form>
{response && (
<div className="response">
<pre>{response}</pre>
{isStreaming && <span className="cursor">▋</span>}
</div>
)}
</div>
);
}
export default StreamingChat;<template>
<div class="streaming-chat">
<form @submit.prevent="handleSubmit">
<input
v-model="message"
type="text"
placeholder="Ask something..."
:disabled="isStreaming"
/>
<button type="submit" :disabled="isStreaming">
{{ isStreaming ? 'Streaming...' : 'Send' }}
</button>
</form>
<div v-if="response" class="response">
<pre>{{ response }}</pre>
<span v-if="isStreaming" class="cursor">▋</span>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue';
const message = ref('');
const response = ref('');
const isStreaming = ref(false);
let eventSource = null;
const handleSubmit = () => {
if (!message.value.trim() || isStreaming.value) return;
response.value = '';
isStreaming.value = true;
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource(
`/api/chatbot/stream?message=${encodeURIComponent(message.value)}`
);
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
isStreaming.value = false;
return;
}
try {
const data = JSON.parse(event.data);
if (data.chunk) {
response.value += data.chunk;
}
} catch (e) {
console.error('Parse error:', e);
}
};
eventSource.onerror = () => {
eventSource.close();
isStreaming.value = false;
};
};
onUnmounted(() => {
if (eventSource) {
eventSource.close();
}
});
</script>
<style scoped>
.cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
</style>import { Component, OnDestroy } from '@angular/core';
@Component({
selector: 'app-streaming-chat',
template: `
<div class="streaming-chat">
<form (submit)="handleSubmit($event)">
<input
[(ngModel)]="message"
type="text"
placeholder="Ask something..."
[disabled]="isStreaming"
name="message"
/>
<button type="submit" [disabled]="isStreaming">
{{ isStreaming ? 'Streaming...' : 'Send' }}
</button>
</form>
<div *ngIf="response" class="response">
<pre>{{ response }}</pre>
<span *ngIf="isStreaming" class="cursor">▋</span>
</div>
</div>
`
})
export class StreamingChatComponent implements OnDestroy {
message = '';
response = '';
isStreaming = false;
private eventSource?: EventSource;
handleSubmit(event: Event): void {
event.preventDefault();
if (!this.message.trim() || this.isStreaming) return;
this.response = '';
this.isStreaming = true;
if (this.eventSource) {
this.eventSource.close();
}
this.eventSource = new EventSource(
`/api/chatbot/stream?message=${encodeURIComponent(this.message)}`
);
this.eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
this.eventSource?.close();
this.isStreaming = false;
return;
}
try {
const data = JSON.parse(event.data);
if (data.chunk) {
this.response += data.chunk;
}
} catch (e) {
console.error('Parse error:', e);
}
};
this.eventSource.onerror = () => {
this.eventSource?.close();
this.isStreaming = false;
};
}
ngOnDestroy(): void {
if (this.eventSource) {
this.eventSource.close();
}
}
}<?php
use Rumenx\PhpChatbot\Contracts\StreamableModelInterface;
try {
// Check if model supports streaming
if (!$model instanceof StreamableModelInterface) {
throw new \RuntimeException('Model does not support streaming');
}
if (!$model->supportsStreaming()) {
throw new \RuntimeException('Streaming not available for this model');
}
// Stream response
foreach ($chatbot->askStream($message) as $chunk) {
echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
flush();
}
echo "data: [DONE]\n\n";
} catch (\RuntimeException $e) {
// Model doesn't support streaming - fallback
$reply = $chatbot->ask($message);
echo "data: " . json_encode(['chunk' => $reply]) . "\n\n";
echo "data: [DONE]\n\n";
} catch (\Exception $e) {
// General error
echo "data: " . json_encode(['error' => $e->getMessage()]) . "\n\n";
}
flush();const eventSource = new EventSource('/api/chatbot/stream?message=' + encodeURIComponent(message));
eventSource.onmessage = function(event) {
if (event.data === '[DONE]') {
eventSource.close();
return;
}
try {
const data = JSON.parse(event.data);
if (data.error) {
console.error('Stream error:', data.error);
displayError(data.error);
eventSource.close();
return;
}
if (data.chunk) {
appendToChat(data.chunk);
}
} catch (e) {
console.error('Parse error:', e);
eventSource.close();
}
};
eventSource.onerror = function(error) {
console.error('EventSource failed:', error);
eventSource.close();
displayError('Connection lost. Please try again.');
};
// Add timeout
setTimeout(() => {
if (eventSource.readyState !== EventSource.CLOSED) {
eventSource.close();
displayError('Request timed out');
}
}, 30000); // 30 seconds<?php
if ($model instanceof StreamableModelInterface && $model->supportsStreaming()) {
// Use streaming
} else {
// Use regular ask()
}For PHP streaming to work properly:
<?php
// In your PHP configuration or script
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', false);
// Flush after each chunk
echo $chunk;
flush();Always set SSE headers:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // For nginxAlways close EventSource connections:
// React example
useEffect(() => {
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
};
}, []);Prevent hanging connections:
const timeout = setTimeout(() => {
eventSource.close();
console.error('Stream timeout');
}, 60000); // 60 seconds
eventSource.onmessage = (event) => {
clearTimeout(timeout); // Reset on each message
// ... handle message
};Implement rate limiting for streaming endpoints:
<?php
// Laravel example
Route::post('/api/chatbot/stream', [ChatbotController::class, 'stream'])
->middleware('throttle:10,1'); // 10 requests per minuteProblem: No chunks are received on frontend.
Solutions:
- Check output buffering is disabled
- Verify headers are set correctly
- Check nginx configuration (disable buffering)
- Ensure
flush()is called after each chunk - Check browser console for errors
Problem: All chunks arrive together at the end.
Solutions:
- Disable output buffering:
@ini_set('output_buffering', 'off') - Add to nginx config:
proxy_buffering off; - Ensure proper flushing
Problem: EventSource shows error immediately.
Solutions:
- Check CORS headers if frontend is on different domain
- Verify endpoint URL is correct
- Check server logs for PHP errors
- Ensure Content-Type is
text/event-stream
Problem: High memory usage during streaming.
Solutions:
- Streaming uses less memory than buffering entire response
- If still high, check for memory leaks in your code
- Monitor with:
echo "Memory: " . memory_get_usage() . "\n";
If using nginx, add to your site config:
location /api/chatbot/stream {
proxy_pass http://your-backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}- Streaming uses similar total time but improves perceived performance
- Connection overhead is slightly higher than regular requests
- Memory efficient - chunks are processed and discarded
- Works well with HTTP/2 for multiplexing multiple streams
- Apply same rate limiting as regular endpoints
- Validate and sanitize input before streaming
- Implement authentication/authorization
- Set reasonable timeouts
- Monitor for abuse patterns
- Frontend Integration - More frontend examples
- API Reference - Complete API documentation
- Best Practices - Production deployment tips
- Examples - Real-world implementations