github prism-php/prism v0.92.0

19 hours ago

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 returns Generator<StreamEvent> instead of Generator<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 to PrismStreamDecodeException

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 original PendingRequest instance
  • $messages: Collection of final Message 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

Don't miss a new prism release

NewReleases is sending notifications on new releases.