Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/core/llms.txt

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

Streaming lets you pipe generated tokens to the client — or to any output sink — as soon as they arrive, without waiting for the model to finish the entire response. In aisdk/core every streaming request returns a Stream object that exposes two iteration modes, four lifecycle hooks, and a ->run() helper for when you only need the final TextResult.
Streaming requires the model to advertise Capability::Streaming. Calling ->stream() automatically adds this capability requirement; the SDK raises a CapabilityNotSupportedException before making any HTTP call if the model does not support it.

Basic streaming

Call ->stream() instead of ->run(). Iterate ->chunks() to receive raw text deltas as strings:
use AiSdk\Generate;
use AiSdk\OpenAI;

$stream = Generate::text('Tell me a story about a PHP developer.')
    ->model(OpenAI::model('gpt-4o'))
    ->stream();

foreach ($stream->chunks() as $chunk) {
    echo $chunk;
    flush();
}
->chunks() is a Generator<int, string> that only yields TextDeltaPart text values. Non-text stream events (tool call fragments, finish metadata, etc.) are still recorded internally but not surfaced here.

Collecting the final result

After iterating chunks

After the foreach loop completes, call ->result() to retrieve the assembled TextResult:
foreach ($stream->chunks() as $chunk) {
    echo $chunk;
}

$result = $stream->result();

echo $result->usage->totalTokens;
echo $result->finishReason->value;

Draining with ->run()

If you don’t need to print tokens incrementally — for example, you are recording the full response — call ->run() to drain the stream and return the TextResult directly:
$result = Generate::text('Summarise the PHP 8.3 release notes.')
    ->model(OpenAI::model('gpt-4o'))
    ->stream()
    ->run();

echo $result->text;

Typed stream parts

For fine-grained control — interleaving reasoning tokens, responding to tool call fragments mid-stream, or detecting errors without exceptions — iterate ->parts() instead of ->chunks(). Each yielded value is a StreamPart subclass:
use AiSdk\Streaming\FinishPart;
use AiSdk\Streaming\ReasoningDeltaPart;
use AiSdk\Streaming\TextDeltaPart;
use AiSdk\Streaming\ToolCallDeltaPart;
use AiSdk\Streaming\ToolCallStartPart;

foreach ($stream->parts() as $part) {
    if ($part instanceof TextDeltaPart) {
        echo $part->text;
    } elseif ($part instanceof ReasoningDeltaPart) {
        // Inner monologue from a reasoning model (e.g. o3)
        error_log('[thinking] ' . $part->text);
    } elseif ($part instanceof ToolCallStartPart) {
        // A new tool call slot has opened; arguments arrive next as deltas
        echo "\n[tool] {$part->name} (id={$part->id}, index={$part->index})\n";
    } elseif ($part instanceof ToolCallDeltaPart) {
        // Incremental JSON fragment for the tool call at $part->index
        echo $part->argsJson;
    } elseif ($part instanceof FinishPart) {
        printf(
            "\n[done] reason=%s  tokens=%d\n",
            $part->reason->value,
            $part->usage->totalTokens,
        );
    }
}

StreamPart reference

ClassKey propertiesDescription
TextDeltaPart$text: stringOne fragment of generated text.
ReasoningDeltaPart$text: stringFragment of an inner reasoning trace (reasoning models only).
ToolCallStartPart$index: int, $id: string, $name: stringSignals the start of a new tool call. Argument JSON follows as ToolCallDeltaParts.
ToolCallDeltaPart$index: int, $argsJson: string, $id: ?string, $name: ?stringPartial argument JSON for the tool call at $index.
FinishPart$reason: FinishReason, $usage: UsageEmitted once when the model stops generating.
ProviderMetadataPart$provider: string, $metadata: arrayProvider-specific metadata emitted during the stream. Accumulated by StreamState and available via $result->providerMetadata after the stream ends.
ErrorPart$error: ThrowableSignals a stream error. Never yielded to the caller — when an ->onError() hook is registered, the callback receives the Throwable and iteration continues; otherwise the Throwable is thrown immediately.
Stream also implements IteratorAggregate, so foreach ($stream as $part) is equivalent to foreach ($stream->parts() as $part).

Lifecycle hooks

Hooks let you react to stream events without managing loop boilerplate. They are registered before iteration begins and fire during the same ->parts() / ->chunks() / ->run() pass:
use AiSdk\Results\TextResult;
use AiSdk\ToolCall;

$stream = Generate::text('What is the weather in Lahore?')
    ->model(OpenAI::model('gpt-4o'))
    ->stream();

$stream
    ->onChunk(function (string $text): void {
        echo $text;
        flush();
    })
    ->onToolCall(function (ToolCall $call): void {
        // Fires after ALL fragments for a tool call have been assembled,
        // so $call->arguments is always the complete decoded array.
        logger()->info('Tool called', [
            'name'      => $call->name,
            'arguments' => $call->arguments,
        ]);
    })
    ->onFinish(function (TextResult $result): void {
        logger()->info('Stream finished', [
            'tokens' => $result->usage->totalTokens,
            'reason' => $result->finishReason->value,
        ]);
    })
    ->onError(function (\Throwable $e): void {
        logger()->error('Stream error', ['error' => $e->getMessage()]);
        // Registering this hook suppresses the exception; the ErrorPart
        // is not yielded — the callback receives the Throwable directly.
    });

$result = $stream->run();

Hook summary

MethodCallback signatureFires
->onChunk(callable)fn(string $text): voidOnce per TextDeltaPart, with the raw text fragment.
->onToolCall(callable)fn(ToolCall $call): voidOnce per assembled tool call, after all argument fragments have been received.
->onFinish(callable)fn(TextResult $result): voidOnce, when the stream ends and the final TextResult is ready.
->onError(callable)fn(Throwable $e): voidOn stream error. Registering this hook prevents the exception from propagating.
Hooks are only fired while the stream is being iterated. If you register hooks but never call ->run(), ->chunks(), or ->parts(), no hook fires.

Streaming in an HTTP response

aisdk/core is transport-agnostic. The following skeleton shows how to wrap a stream in a PSR-7 / Laravel-style streaming HTTP response:
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

$stream = Generate::text($userPrompt)
    ->model(OpenAI::model('gpt-4o'))
    ->stream();

foreach ($stream->chunks() as $chunk) {
    echo "data: " . json_encode(['text' => $chunk]) . "\n\n";
    ob_flush();
    flush();
}

echo "data: [DONE]\n\n";

Build docs developers (and LLMs) love