Documentation Index Fetch the complete documentation index at: https://mintlify.com/Capevace/llm-magic/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Streaming allows your application to receive and display AI responses in real-time as they’re generated, rather than waiting for the complete response. This creates a much better user experience, especially for long responses.
Quick Start
Basic Streaming
Use the stream() method instead of send() to enable streaming:
use Mateffy\ Magic ;
$messages = Magic :: chat ()
-> prompt ( 'Write a short story about a robot' )
-> stream ();
// The response is streamed in real-time
// Access the complete text after streaming finishes
echo $messages -> text ();
Difference: stream() vs send()
// Without streaming - waits for complete response
$messages = Magic :: chat ()
-> prompt ( 'Explain quantum computing' )
-> send (); // Blocks until complete
// With streaming - receives response in chunks
$messages = Magic :: chat ()
-> prompt ( 'Explain quantum computing' )
-> stream (); // Returns chunks as they arrive
Real-time Progress
onMessageProgress Callback
Receive chunks as they arrive:
Magic :: chat ()
-> prompt ( 'Write a long article about AI' )
-> onMessageProgress ( function ( $message ) {
// Called for each chunk during streaming
echo $message -> text ();
flush (); // Send to browser immediately
})
-> stream ();
Complete Message Callback
Get notified when complete messages are received:
Magic :: chat ()
-> prompt ( 'Explain machine learning' )
-> onMessage ( function ( $message ) {
// Called when a complete message is received
logger () -> info ( 'Complete message received' , [
'type' => get_class ( $message ),
'content' => $message -> text (),
]);
})
-> stream ();
Streaming in Web Applications
Laravel HTTP Streaming
Stream responses directly to the browser:
use Illuminate\Http\ Request ;
use Mateffy\ Magic ;
Route :: get ( '/api/chat/stream' , function ( Request $request ) {
return response () -> stream ( function () use ( $request ) {
$prompt = $request -> input ( 'prompt' );
Magic :: chat ()
-> prompt ( $prompt )
-> onMessageProgress ( function ( $message ) {
echo "data: " . json_encode ([
'text' => $message -> text (),
]) . " \n\n " ;
if ( ob_get_level () > 0 ) {
ob_flush ();
}
flush ();
})
-> stream ();
echo "data: [DONE] \n\n " ;
}, 200 , [
'Content-Type' => 'text/event-stream' ,
'Cache-Control' => 'no-cache' ,
'X-Accel-Buffering' => 'no' ,
]);
});
JavaScript Client (Server-Sent Events)
Consume the stream in your frontend:
const eventSource = new EventSource ( '/api/chat/stream?prompt=' + encodeURIComponent ( prompt ));
const outputDiv = document . getElementById ( 'output' );
eventSource . onmessage = ( event ) => {
if ( event . data === '[DONE]' ) {
eventSource . close ();
return ;
}
const data = JSON . parse ( event . data );
outputDiv . textContent += data . text ;
};
eventSource . onerror = ( error ) => {
console . error ( 'Stream error:' , error );
eventSource . close ();
};
React Example
Use streaming in a React component:
import { useState , useEffect } from 'react' ;
function ChatInterface () {
const [ output , setOutput ] = useState ( '' );
const [ loading , setLoading ] = useState ( false );
const streamResponse = async ( prompt ) => {
setLoading ( true );
setOutput ( '' );
const eventSource = new EventSource (
`/api/chat/stream?prompt= ${ encodeURIComponent ( prompt ) } `
);
eventSource . onmessage = ( event ) => {
if ( event . data === '[DONE]' ) {
eventSource . close ();
setLoading ( false );
return ;
}
const data = JSON . parse ( event . data );
setOutput ( prev => prev + data . text );
};
eventSource . onerror = () => {
eventSource . close ();
setLoading ( false );
};
};
return (
< div >
< div className = "output" > { output } </ div >
{ loading && < div className = "loading" > Streaming... </ div > }
</ div >
);
}
Streaming works seamlessly with tool calls:
use Mateffy\ Magic ;
use Mateffy\Magic\Chat\Messages\ Step ;
Magic :: chat ()
-> messages ([
Step :: user ( 'What is the weather in Paris?' ),
])
-> tools ([
'get_weather' => function ( string $city ) {
// This executes during the stream
return WeatherAPI :: get ( $city );
},
])
-> onMessageProgress ( function ( $message ) {
// Shows both text and tool call progress
echo $message -> text ();
flush ();
})
-> stream ();
Monitor tool execution during streaming:
Magic :: chat ()
-> messages ([ Step :: user ( 'Search for flights to Paris' )])
-> tools ([
'search_flights' => function ( string $destination ) {
echo "[Searching flights to { $destination }...] \n " ;
flush ();
$results = FlightAPI :: search ( $destination );
echo "[Found { $results -> count ()} flights] \n " ;
flush ();
return $results ;
},
])
-> onMessageProgress ( function ( $message ) {
echo $message -> text ();
flush ();
})
-> stream ();
Advanced Streaming
Custom Data Packets
Access raw streaming data:
Magic :: chat ()
-> prompt ( 'Write a story' )
-> onDataPacket ( function ( $packet ) {
// Raw data from the LLM provider
logger () -> debug ( 'Stream packet' , [ 'packet' => $packet ]);
})
-> stream ();
Token Statistics During Streaming
Track token usage as the response streams:
Magic :: chat ()
-> prompt ( 'Explain quantum physics' )
-> onTokenStats ( function ( $stats ) {
echo "Tokens used: { $stats ['total_tokens']} \n " ;
flush ();
})
-> stream ();
Error Handling
Handle errors during streaming:
try {
Magic :: chat ()
-> prompt ( 'Your prompt here' )
-> onMessageProgress ( function ( $message ) {
echo $message -> text ();
flush ();
})
-> stream ();
} catch ( \ Throwable $e ) {
echo "Stream error: { $e -> getMessage ()} \n " ;
logger () -> error ( 'Stream failed' , [
'error' => $e -> getMessage (),
'trace' => $e -> getTraceAsString (),
]);
}
Complete Streaming Example
Here’s a complete Laravel controller with streaming:
use Illuminate\Http\ Request ;
use Mateffy\ Magic ;
use Mateffy\Magic\Chat\Messages\ Step ;
class ChatController extends Controller
{
public function stream ( Request $request )
{
$validated = $request -> validate ([
'messages' => 'required|array' ,
'model' => 'string|nullable' ,
]);
return response () -> stream ( function () use ( $validated ) {
$messages = collect ( $validated [ 'messages' ])
-> map ( fn ( $msg ) => match ( $msg [ 'role' ]) {
'user' => Step :: user ( $msg [ 'content' ]),
'assistant' => Step :: assistant ( $msg [ 'content' ]),
})
-> toArray ();
try {
Magic :: chat ()
-> model ( $validated [ 'model' ] ?? 'google/gemini-2.0-flash-lite' )
-> messages ( $messages )
-> tools ([
'search' => function ( string $query ) {
$this -> sendEvent ( 'tool_call' , [
'tool' => 'search' ,
'query' => $query ,
]);
$results = SearchService :: search ( $query );
$this -> sendEvent ( 'tool_result' , [
'tool' => 'search' ,
'count' => count ( $results ),
]);
return $results ;
},
])
-> onMessageProgress ( function ( $message ) {
$this -> sendEvent ( 'message' , [
'text' => $message -> text (),
]);
})
-> onTokenStats ( function ( $stats ) {
$this -> sendEvent ( 'tokens' , $stats );
})
-> stream ();
$this -> sendEvent ( 'done' , [ 'status' => 'complete' ]);
} catch ( \ Throwable $e ) {
$this -> sendEvent ( 'error' , [
'message' => $e -> getMessage (),
]);
}
}, 200 , [
'Content-Type' => 'text/event-stream' ,
'Cache-Control' => 'no-cache' ,
'X-Accel-Buffering' => 'no' ,
]);
}
private function sendEvent ( string $type , array $data ) : void
{
echo "event: { $type } \n " ;
echo "data: " . json_encode ( $data ) . " \n\n " ;
if ( ob_get_level () > 0 ) {
ob_flush ();
}
flush ();
}
}
Frontend for Complete Example
class ChatClient {
constructor ( endpoint ) {
this . endpoint = endpoint ;
this . eventSource = null ;
}
stream ( messages , callbacks = {}) {
const params = new URLSearchParams ({
messages: JSON . stringify ( messages ),
});
this . eventSource = new EventSource ( ` ${ this . endpoint } ? ${ params } ` );
this . eventSource . addEventListener ( 'message' , ( e ) => {
const data = JSON . parse ( e . data );
callbacks . onMessage ?.( data );
});
this . eventSource . addEventListener ( 'tool_call' , ( e ) => {
const data = JSON . parse ( e . data );
callbacks . onToolCall ?.( data );
});
this . eventSource . addEventListener ( 'tool_result' , ( e ) => {
const data = JSON . parse ( e . data );
callbacks . onToolResult ?.( data );
});
this . eventSource . addEventListener ( 'tokens' , ( e ) => {
const data = JSON . parse ( e . data );
callbacks . onTokens ?.( data );
});
this . eventSource . addEventListener ( 'done' , ( e ) => {
this . eventSource . close ();
callbacks . onDone ?.();
});
this . eventSource . addEventListener ( 'error' , ( e ) => {
const data = JSON . parse ( e . data );
this . eventSource . close ();
callbacks . onError ?.( data );
});
}
stop () {
this . eventSource ?. close ();
}
}
// Usage
const client = new ChatClient ( '/api/chat/stream' );
client . stream (
[
{ role: 'user' , content: 'Tell me about Paris' },
],
{
onMessage : ( data ) => {
document . getElementById ( 'output' ). textContent += data . text ;
},
onToolCall : ( data ) => {
console . log ( 'Tool called:' , data . tool , data . query );
},
onToolResult : ( data ) => {
console . log ( 'Tool result:' , data );
},
onTokens : ( data ) => {
document . getElementById ( 'token-count' ). textContent =
`Tokens: ${ data . total_tokens } ` ;
},
onDone : () => {
console . log ( 'Stream complete' );
},
onError : ( data ) => {
console . error ( 'Stream error:' , data . message );
},
}
);
Best Practices
Always Use Streaming Use stream() for better UX, especially for responses longer than a few sentences.
Flush Output Call flush() after echoing content to ensure immediate browser delivery.
Handle Errors Wrap streaming in try-catch and provide user feedback on errors.
Set Headers Use proper headers for Server-Sent Events: text/event-stream and no-cache.
Disable Output Buffering
Set X-Accel-Buffering: no header to prevent proxy buffering.
Monitor Connection
Check if the client disconnects to stop unnecessary processing.
Optimize Chunk Size
Balance between responsiveness and overhead - very small chunks increase overhead.
Rate Limiting
Implement rate limiting on streaming endpoints to prevent abuse.
Troubleshooting
Stream Not Updating in Browser
Make sure you’re flushing output:
echo $content ;
if ( ob_get_level () > 0 ) {
ob_flush ();
}
flush ();
Nginx Buffering
Disable nginx buffering for streaming endpoints:
location /api/chat/stream {
proxy_pass http://backend;
proxy_buffering off ;
proxy_cache off ;
proxy_set_header Connection '' ;
proxy_http_version 1.1 ;
chunked_transfer_encoding off ;
}
Large Responses Timeout
Increase PHP execution time for long streams:
set_time_limit ( 300 ); // 5 minutes
Magic :: chat ()
-> prompt ( 'Write a very long article' )
-> stream ();
Next Steps
Chat API Learn more about the Chat API features
Extraction Extract structured data from documents
Embeddings Generate embeddings for semantic search
API Reference Explore the complete API documentation