What's Changed
⚠️ BREAKING CHANGES
1. Removed Chunk
Class and ChunkType
Enum
What changed:
- The
Prism\Prism\Text\Chunk
class has been removed - The
Prism\Prism\Enums\ChunkType
enum has been removed PendingRequest::asStream()
now returnsGenerator<StreamEvent>
instead ofGenerator<Chunk>
Before:
use Prism\Prism\Text\Chunk;
use Prism\Prism\Enums\ChunkType;
foreach ($prism->text()->asStream() as $chunk) {
// $chunk is instance of Chunk
echo $chunk->text;
if ($chunk->chunkType === ChunkType::ToolCall) {
// Handle tool calls
}
if ($chunk->finishReason) {
// Stream complete
}
}
After:
use Prism\Prism\Streaming\Events\{
StreamStartEvent,
TextDeltaEvent,
TextCompleteEvent,
ToolCallEvent,
StreamEndEvent
};
foreach ($prism->text()->asStream() as $event) {
// $event is instance of StreamEvent
match ($event::class) {
StreamStartEvent::class => /* stream started */,
TextDeltaEvent::class => echo $event->delta,
ToolCallEvent::class => /* handle tool call */,
StreamEndEvent::class => /* stream complete */,
default => /* other events */
};
}
2. Exception Renamed
What changed:
PrismChunkDecodeException
renamed toPrismStreamDecodeException
Migration:
// Before
use Prism\Prism\Exceptions\PrismChunkDecodeException;
try {
// ...
} catch (PrismChunkDecodeException $e) {
// ...
}
// After
use Prism\Prism\Exceptions\PrismStreamDecodeException;
try {
// ...
} catch (PrismStreamDecodeException $e) {
// ...
}
3. Stream Event Type Changes
The streaming output is now composed of 12 distinct event types, each with specific purposes:
Event | Purpose |
---|---|
StreamStartEvent
| Emitted once at stream initialization |
TextStartEvent
| Emitted before first text delta |
TextDeltaEvent
| Contains incremental text chunks |
TextCompleteEvent
| Text generation complete |
ThinkingStartEvent
| Reasoning/thinking block started |
ThinkingEvent
| Incremental thinking content |
ThinkingCompleteEvent
| Thinking block complete |
ToolCallEvent
| Tool invocation requested |
ToolResultEvent
| Tool execution result |
CitationEvent
| Source citation (Anthropic) |
ErrorEvent
| Recoverable or fatal error |
StreamEndEvent
| Stream complete with final metadata |
Migration Guide
Basic Streaming
Before (Chunk-based):
$response = '';
foreach ($prism->text()->asStream() as $chunk) {
$response .= $chunk->text;
}
echo $response;
After (Event-based):
use Prism\Prism\Streaming\Events\TextDeltaEvent;
$response = '';
foreach ($prism->text()->asStream() as $event) {
if ($event instanceof TextDeltaEvent) {
$response .= $event->delta;
}
}
echo $response;
Handling Tool Calls
Before:
foreach ($prism->text()->asStream() as $chunk) {
if ($chunk->chunkType === ChunkType::ToolCall) {
foreach ($chunk->toolCalls as $toolCall) {
// Handle tool call
}
}
}
After:
use Prism\Prism\Streaming\Events\ToolCallEvent;
foreach ($prism->text()->asStream() as $event) {
if ($event instanceof ToolCallEvent) {
// Handle tool call
$toolCall = $event->toolCall;
}
}
Detecting Stream Completion
Before:
foreach ($prism->text()->asStream() as $chunk) {
if ($chunk->finishReason) {
echo "Stream finished: {$chunk->finishReason->value}";
}
}
After:
use Prism\Prism\Streaming\Events\StreamEndEvent;
foreach ($prism->text()->asStream() as $event) {
if ($event instanceof StreamEndEvent) {
echo "Stream finished: {$event->finishReason->value}";
// Access usage metadata: $event->usage
}
}
Handling Thinking/Reasoning
Before:
foreach ($prism->text()->asStream() as $chunk) {
if ($chunk->chunkType === ChunkType::Thinking) {
echo "Thinking: {$chunk->text}";
}
}
After:
use Prism\Prism\Streaming\Events\ThinkingEvent;
foreach ($prism->text()->asStream() as $event) {
if ($event instanceof ThinkingEvent) {
echo "Thinking: {$event->delta}";
}
}
New Features
1. Granular Stream Events
Each phase of streaming now emits specific events, enabling fine-grained control:
use Prism\Prism\Streaming\Events\{
StreamStartEvent,
TextStartEvent,
TextDeltaEvent,
TextCompleteEvent,
StreamEndEvent
};
foreach ($prism->text()->asStream() as $event) {
match ($event::class) {
StreamStartEvent::class => log('Stream started', [
'model' => $event->model,
'provider' => $event->provider,
]),
TextStartEvent::class => log('Text generation started'),
TextDeltaEvent::class => echo $event->delta,
TextCompleteEvent::class => log('Text generation complete'),
StreamEndEvent::class => log('Stream ended', [
'finish_reason' => $event->finishReason->value,
'usage' => $event->usage,
]),
};
}
2. Streaming Adapters
Three new adapters for different streaming protocols:
Server-Sent Events (SSE)
return $prism->text()
->withPrompt('Tell me a story')
->asEventStreamResponse(); // Returns StreamedResponse with SSE format
Data Protocol (Vercel AI SDK compatible)
return $prism->text()
->withPrompt('Tell me a story')
->asDataStreamResponse(); // Returns StreamedResponse with data protocol
Laravel Broadcasting
use Illuminate\Broadcasting\Channel;
return $prism->text()
->withPrompt('Tell me a story')
->asBroadcastResponse(new Channel('prism-stream'));
3. onComplete Callback
Register callbacks that execute when streaming completes:
$prism->text()
->withPrompt('Analyze this data')
->onComplete(function ($request, $messages) {
// Save conversation to database
Conversation::create([
'messages' => $messages,
'model' => $request->model,
]);
})
->asStream();
The callback receives:
$request
: The originalPendingRequest
instance$messages
: Collection of finalMessage
objects
4. StreamCollector Utility
Automatically collects stream events into final messages:
use Prism\Prism\Streaming\StreamCollector;
$stream = $prism->text()->asStream();
$collector = new StreamCollector($stream, $request, $callback);
foreach ($collector->collect() as $event) {
// Events are yielded while being collected
}
// After iteration, access collected messages
$messages = $collector->messages();
5. Testing Improvements
New fake methods for testing streaming:
use Prism\Prism\Testing\PrismFake;
PrismFake::fake([
'mistral-large-latest' => PrismFake::streamResponse([
'Text response content',
]),
]);
foreach ($prism->text()->asStream() as $event) {
// Test against stream events
}
Internal Improvements
Unified StreamState Architecture
All 9 streaming providers now use a consistent StreamState
object for managing streaming state:
Providers Refactored:
- Anthropic (AnthropicStreamState extension)
- OpenAI (base StreamState)
- Gemini (base StreamState)
- Groq (base StreamState)
- Mistral (base StreamState)
- Ollama (OllamaStreamState extension)
- XAI (base StreamState)
- DeepSeek (base StreamState)
- OpenRouter (base StreamState)
Benefits:
- Consistent state management across all providers
- Fluent API for state mutations (
withMessageId()
,appendText()
, etc.) - Provider extensibility via inheritance (e.g.,
OllamaStreamState
for token accumulation) - Type-safe operations with strong typing throughout
- Easier debugging with centralized state tracking
Example (before/after):
// Before: Scattered instance properties
protected string $messageId = '';
protected bool $streamStarted = false;
protected string $currentText = '';
$this->messageId = EventID::generate();
$this->streamStarted = true;
$this->currentText .= $delta;
// After: Unified state object with fluent API
protected StreamState $state;
$this->state
->withMessageId(EventID::generate())
->markStreamStarted()
->appendText($delta);
Full Changelog: v0.91.1...v0.92.0