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:| Key | Type | Description |
|---|
event | ?string | The SSE event name. May be null for unnamed events. Not currently used by the parser but must be present. |
data | string | The 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.
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.
Emitted once, the first time a payload contains any of the response-level fields. Carries:
| Metadata key | Source |
|---|
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.
Emitted once per tool-call slot index the first time that slot’s id field appears in choices[0].delta.tool_calls. Carries:
| Property | Source |
|---|
index | delta.tool_calls[n].index |
id | delta.tool_calls[n].id |
name | delta.tool_calls[n].function.name |
Subsequent events for the same index do not produce another ToolCallStartPart, even if they carry a non-empty id.
Emitted for each event where delta.tool_calls[n].function.arguments is a non-empty string. Carries:
| Property | Source |
|---|
index | delta.tool_calls[n].index |
argsJson | delta.tool_calls[n].function.arguments (raw JSON fragment) |
id | delta.tool_calls[n].id (nullable — present only if the field exists) |
name | delta.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.
Emitted when choices[0].finish_reason is present in an event. Carries:
| Metadata key | Source |
|---|
choice_index | choices[0]['index'] |
choice_finish_reason | choices[0]['finish_reason'] |
Only keys present in the choice object are included.
7. FinishPart
Always emitted last, after all events have been consumed. Carries:
| Property | Description |
|---|
finishReason | FinishReason enum mapped from the last seen finish_reason via MapsFinishReason::map(). Defaults to FinishReason::Stop when no finish_reason was encountered. |
usage | Usage 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_reason → FinishReason mapping is:
| Raw string | FinishReason |
|---|
"stop", "end_turn" | FinishReason::Stop |
"length", "max_tokens" | FinishReason::Length |
"tool_calls", "function_call" | FinishReason::ToolCalls |
"content_filter" | FinishReason::ContentFilter |
null / missing | FinishReason::Stop |
| anything else | FinishReason::Unknown |
"[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.