Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/openai-compatible/llms.txt

Use this file to discover all available pages before exploring further.

ChatStreamParser is a stateless, final utility class in the aisdk/openai-compatible package that converts a raw sequence of Server-Sent Events (SSE) from an OpenAI-compatible streaming /chat/completions response into an ordered stream of strongly typed StreamPart objects. It is implemented as a PHP Generator: you iterate over it with a foreach loop and each yield delivers the next StreamPart as soon as it is available, without buffering the entire response in memory. At the end of the stream a FinishPart is always emitted, carrying the accumulated FinishReason and Usage totals.

Namespace

AiSdk\OpenAICompatible\ChatStreamParser

Method: parse()

public static function parse(
    iterable $events,
    string $providerName
): Generator
Consumes an iterable of SSE event arrays and yields typed StreamPart objects. The generator is lazy — it only processes the next event when the caller advances the iterator.

Parameters

events
iterable<int, array{event: ?string, data: string}>
required
An iterable (array, Generator, or any Traversable) where each element is an associative array with exactly two keys:
KeyTypeDescription
event?stringThe SSE event name. May be null for unnamed events. Not currently used by the parser but must be present.
datastringThe raw SSE data line. The parser automatically skips items where data is an empty string or the sentinel value "[DONE]". All other values are decoded with json_decode.
Non-array results from json_decode (malformed JSON) are silently skipped.
providerName
string
required
The provider namespace string, e.g. "openai". Used as the key in ProviderMetadataPart payloads so downstream code can retrieve provider-specific fields by name.

Emitted StreamPart subtypes

The parser yields parts in the order they are encountered in the event stream. The table below describes each subtype, the conditions that trigger it, and the data it carries.

1. ProviderMetadataPart — response-level metadata

Emitted once, the first time a payload contains any of the response-level fields. Carries:
Metadata keySource
id$payload['id']
object$payload['object']
created$payload['created']
model$payload['model']
system_fingerprint$payload['system_fingerprint']
service_tier$payload['service_tier']
Only keys present in the payload are included.

2. TextDeltaPart

Emitted for each event where choices[0].delta.content is a non-empty string. Each part carries the incremental text fragment. Concatenating all TextDeltaPart values in order reconstructs the complete assistant message.

3. ReasoningDeltaPart

Emitted for each event where choices[0].delta.reasoning_content or (as a fallback) choices[0].delta.reasoning is a non-empty string. The first key found is used. Carries the incremental reasoning fragment.
reasoning_content takes precedence over reasoning. Only one ReasoningDeltaPart is emitted per event regardless of which key is present.

4. ToolCallStartPart

Emitted once per tool-call slot index the first time that slot’s id field appears in choices[0].delta.tool_calls. Carries:
PropertySource
indexdelta.tool_calls[n].index
iddelta.tool_calls[n].id
namedelta.tool_calls[n].function.name
Subsequent events for the same index do not produce another ToolCallStartPart, even if they carry a non-empty id.

5. ToolCallDeltaPart

Emitted for each event where delta.tool_calls[n].function.arguments is a non-empty string. Carries:
PropertySource
indexdelta.tool_calls[n].index
argsJsondelta.tool_calls[n].function.arguments (raw JSON fragment)
iddelta.tool_calls[n].id (nullable — present only if the field exists)
namedelta.tool_calls[n].function.name (nullable — present only if the field exists)
argsJson is a raw JSON fragment: individual deltas are not valid JSON on their own. The accumulator (e.g. StreamState) is responsible for concatenating fragments and then decoding the final JSON string.

6. ProviderMetadataPart — choice-level metadata

Emitted when choices[0].finish_reason is present in an event. Carries:
Metadata keySource
choice_indexchoices[0]['index']
choice_finish_reasonchoices[0]['finish_reason']
Only keys present in the choice object are included.

7. FinishPart

Always emitted last, after all events have been consumed. Carries:
PropertyDescription
finishReasonFinishReason enum mapped from the last seen finish_reason via MapsFinishReason::map(). Defaults to FinishReason::Stop when no finish_reason was encountered.
usageUsage object populated from the last event that contained a top-level usage object (typically the final [DONE]-adjacent chunk when stream_options: {include_usage: true} is set). Falls back to Usage::empty().
The finish_reasonFinishReason mapping is:
Raw stringFinishReason
"stop", "end_turn"FinishReason::Stop
"length", "max_tokens"FinishReason::Length
"tool_calls", "function_call"FinishReason::ToolCalls
"content_filter"FinishReason::ContentFilter
null / missingFinishReason::Stop
anything elseFinishReason::Unknown

Input format detail

"[DONE]" and empty-string data values are skipped automatically — your SSE client does not need to filter them before passing the event list to parse().
Each event in the $events iterable should look like this:
// Named event (uncommon for /chat/completions)
['event' => 'message', 'data' => '{"id":"chatcmpl_stream","choices":[...]}']

// Unnamed event (typical)
['event' => null, 'data' => '{"choices":[{"delta":{"content":"Hello"}}]}']

// Terminal sentinel — automatically skipped
['event' => null, 'data' => '[DONE]']

// Empty data — automatically skipped
['event' => null, 'data' => '']

Usage example

The example below uses the exact event sequence from the ChatWireFormatTest suite to demonstrate streaming tool-call accumulation with StreamState:
use AiSdk\OpenAICompatible\ChatStreamParser;
use AiSdk\Streaming\StreamState;

// Raw SSE events — as delivered by your HTTP client
$events = [
    // First chunk: establishes response id, model, and tool-call slot 0
    [
        'event' => null,
        'data'  => json_encode([
            'id'      => 'chatcmpl_stream',
            'model'   => 'gpt-4o',
            'choices' => [[
                'delta' => [
                    'tool_calls' => [[
                        'index'    => 0,
                        'id'       => 'call_1',
                        'function' => ['name' => 'weather', 'arguments' => ''],
                    ]],
                ],
            ]],
        ]),
    ],
    // Second chunk: first arguments fragment
    [
        'event' => null,
        'data'  => json_encode([
            'choices' => [[
                'delta' => [
                    'tool_calls' => [[
                        'index'    => 0,
                        'function' => ['arguments' => '{"ci'],
                    ]],
                ],
            ]],
        ]),
    ],
    // Third chunk: second arguments fragment
    [
        'event' => null,
        'data'  => json_encode([
            'choices' => [[
                'delta' => [
                    'tool_calls' => [[
                        'index'    => 0,
                        'function' => ['arguments' => 'ty":"Oslo"}'],
                    ]],
                ],
            ]],
        ]),
    ],
    // Fourth chunk: finish signal
    [
        'event' => null,
        'data'  => json_encode([
            'choices' => [[
                'index'        => 0,
                'delta'        => [],
                'finish_reason' => 'tool_calls',
            ]],
        ]),
    ],
    // Terminal sentinel — automatically skipped by the parser
    ['event' => null, 'data' => '[DONE]'],
];

$state = new StreamState();

foreach (ChatStreamParser::parse($events, 'openai') as $part) {
    $state->record($part);
}

// Accumulated tool calls
$calls = $state->toolCalls();         // array of ToolCallPart
echo count($calls);                   // 1
echo $calls[0]->name;                 // 'weather'
print_r($calls[0]->arguments);        // ['city' => 'Oslo']

// Provider metadata (from ProviderMetadataPart)
$meta = $state->providerMetadata()['openai'];
echo $meta['id'];                     // 'chatcmpl_stream'
echo $meta['choice_finish_reason'];   // 'tool_calls'

// Finish reason and usage (from FinishPart)
echo $state->finishReason()->name;    // 'ToolCalls'
echo $state->usage()->inputTokens;    // 0  (no usage chunk in this example)
StreamState is the recommended way to accumulate parts from ChatStreamParser. It handles fragment concatenation for ToolCallDeltaPart arguments, tracks FinishReason and Usage from FinishPart, and merges all ProviderMetadataPart payloads into a single map.
Do not pass the Generator to more than one consumer. PHP generators are single-pass — once a value has been yielded and consumed, it cannot be replayed. If you need to inspect parts multiple times, collect them into an array with iterator_to_array() first.

Build docs developers (and LLMs) love