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.

Server-Sent Events (SSE) streaming lets the model’s response arrive incrementally — word by word, tool-call fragment by fragment — instead of waiting for the full completion. ChatStreamParser::parse() converts the raw SSE event stream from any OpenAI-compatible provider into a sequence of strongly-typed StreamPart objects that your application can process without touching the wire format directly.

Enabling streaming in the request

Pass stream: true to ChatRequestBuilder::build(). The builder automatically appends stream_options: {include_usage: true} so token usage is available in the final event rather than being discarded.
use AiSdk\OpenAICompatible\ChatRequestBuilder;
use AiSdk\Requests\TextModelRequest;
use AiSdk\Message;

$body = ChatRequestBuilder::build(
    modelId: 'gpt-4o',
    providerName: 'openai',
    request: new TextModelRequest(
        messages: [Message::user('Explain gravity in one paragraph.')],
    ),
    stream: true,
);

// $body['stream']         === true
// $body['stream_options'] === ['include_usage' => true]

Parsing the stream

ChatStreamParser::parse() accepts an iterable of raw SSE events and returns a Generator of StreamPart objects. Each event is a two-key array with event (nullable string) and data (the raw SSE data line string).
use AiSdk\OpenAICompatible\ChatStreamParser;
use AiSdk\Streaming\StreamState;

$events = $this->runner()->streamJson($url, $body, $headers, 'openai');

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

echo $state->text();
The [DONE] sentinel that OpenAI-compatible providers send at the end of the stream is handled automatically. The parser skips it silently — you never need to check for it in your foreach loop.

StreamPart types

The generator emits parts in a defined order. Understanding each type lets you build rich streaming UIs, accumulate tool-call arguments progressively, and surface reasoning traces.
Emitted as soon as the first event that contains response-level fields (id, object, created, model, system_fingerprint, service_tier) is parsed. This part arrives before any text or tool-call parts.A second ProviderMetadataPart is emitted when the choice-level finish_reason is encountered, carrying choice_index and choice_finish_reason.
use AiSdk\Streaming\ProviderMetadataPart;

if ($part instanceof ProviderMetadataPart) {
    echo $part->providerName;    // 'openai'
    print_r($part->metadata);   // ['id' => 'chatcmpl_stream', 'model' => 'gpt-4o', ...]
}
Emitted for every SSE event that contains a non-empty delta.content string. Stream these to the user as they arrive for the smoothest experience.
use AiSdk\Streaming\TextDeltaPart;

if ($part instanceof TextDeltaPart) {
    echo $part->text; // e.g. 'Grav', 'ity is', ' a fundamental...'
}
Emitted when the delta contains a reasoning_content or reasoning key (the parser checks both, preferring reasoning_content). Useful for models that expose chain-of-thought tokens separately from the final response.
use AiSdk\Streaming\ReasoningDeltaPart;

if ($part instanceof ReasoningDeltaPart) {
    echo $part->reasoning; // incremental reasoning trace token
}
Emitted exactly once per tool-call slot (identified by index) when the first event for that slot arrives and includes a call id. Gives you the slot index, the call ID, and the function name up front.
use AiSdk\Streaming\ToolCallStartPart;

if ($part instanceof ToolCallStartPart) {
    echo $part->index;  // 0
    echo $part->id;     // 'call_abc123'
    echo $part->name;   // 'get_weather'
}
Emitted for every event that delivers a non-empty function.arguments fragment for a given slot. The argsJson field is a raw JSON string fragment — the full JSON is only valid once all deltas for the slot are concatenated (which StreamState does for you).
use AiSdk\Streaming\ToolCallDeltaPart;

if ($part instanceof ToolCallDeltaPart) {
    echo $part->index;    // slot index
    echo $part->argsJson; // e.g. '{"ci', 'ty":"Oslo"}'
}
Always emitted last, after all events have been consumed. Carries the normalized finish reason and the final usage figures (populated from the last usage event, which stream_options: {include_usage: true} guarantees).
use AiSdk\Streaming\FinishPart;

if ($part instanceof FinishPart) {
    echo $part->finishReason;           // e.g. 'stop', 'tool_calls', 'length'
    echo $part->usage->inputTokens;
    echo $part->usage->outputTokens;
}

Accumulating state with StreamState

Rather than matching each part type manually, pass every part to StreamState::record(). After the loop, the state object exposes the fully assembled text, tool calls, usage, and metadata.
use AiSdk\Streaming\StreamState;

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

echo $state->text();

foreach ($state->toolCalls() as $call) {
    echo $call->name;           // 'weather'
    print_r($call->arguments);  // ['city' => 'Oslo']
}

echo $state->providerMetadata()['openai']['id'];

Full streaming tool-call example

The following example is drawn directly from ChatWireFormatTest.php and shows the complete SSE event sequence for a streamed tool call, including fragmented JSON arguments:
use AiSdk\OpenAICompatible\ChatStreamParser;
use AiSdk\Streaming\StreamState;

// Raw SSE events as the HTTP client delivers them
$events = [
    // First chunk: response metadata + tool call slot open
    ['event' => null, 'data' => json_encode([
        'id'    => 'chatcmpl_stream',
        'model' => 'gpt-4o',
        'choices' => [[
            'delta' => [
                'tool_calls' => [[
                    'index'    => 0,
                    'id'       => 'call_1',
                    'function' => ['name' => 'weather', 'arguments' => ''],
                ]],
            ],
        ]],
    ])],

    // Arguments arrive in fragments
    ['event' => null, 'data' => json_encode([
        'choices' => [[
            'delta' => [
                'tool_calls' => [[
                    'index'    => 0,
                    'function' => ['arguments' => '{"ci'],
                ]],
            ],
        ]],
    ])],
    ['event' => null, 'data' => json_encode([
        'choices' => [[
            'delta' => [
                'tool_calls' => [[
                    'index'    => 0,
                    'function' => ['arguments' => 'ty":"Oslo"}'],
                ]],
            ],
        ]],
    ])],

    // Finish event
    ['event' => null, 'data' => json_encode([
        'choices' => [[
            'index'        => 0,
            'delta'        => [],
            'finish_reason' => 'tool_calls',
        ]],
    ])],

    // Sentinel — handled automatically
    ['event' => null, 'data' => '[DONE]'],
];

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

$call = $state->toolCalls()[0];
echo $call->name;             // 'weather'
print_r($call->arguments);    // ['city' => 'Oslo']

echo $state->providerMetadata()['openai']['id'];
// 'chatcmpl_stream'

echo $state->providerMetadata()['openai']['choice_finish_reason'];
// 'tool_calls'
For non-streaming chat completions, see Chat Completions. For tool definitions and multi-turn flows, see Tool Calling.

Build docs developers (and LLMs) love