CookbookStream in PHP

Stream in PHP

stream_options.include_usage is Preview (not available yet). Until it ships, leave it out; the code below simply prints no usage line.

Print a reply as it is generated with the curl extension’s write callback.

<?php
$buffer = '';
$ch = curl_init('https://app.syntic.ai/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SYNTIC_API_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'model' => 'Amara S1.2',
        'stream' => true,
        'stream_options' => ['include_usage' => true],
        'messages' => [['role' => 'user', 'content' => 'Explain idempotency keys in three sentences.']],
    ]),
    CURLOPT_WRITEFUNCTION => function ($ch, string $fragment) use (&$buffer): int {
        $buffer .= $fragment;
        while (($pos = strpos($buffer, "\n")) !== false) {
            $line = trim(substr($buffer, 0, $pos));
            $buffer = substr($buffer, $pos + 1);
            if (!str_starts_with($line, 'data: ')) continue;
            $payload = substr($line, 6);
            if ($payload === '[DONE]') continue;
            $chunk = json_decode($payload, true);
            if (isset($chunk['error'])) {
                fwrite(STDERR, "stream interrupted: {$chunk['error']['message']}\n");
                return 0; // abort the transfer
            }
            echo $chunk['choices'][0]['delta']['content'] ?? '';
            if (!empty($chunk['usage'])) {
                printf("\n[%d in / %d out tokens]\n", $chunk['usage']['prompt_tokens'], $chunk['usage']['completion_tokens']);
            }
            flush();
        }
        return strlen($fragment);
    },
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
    fwrite(STDERR, "HTTP $status: $buffer\n");
}

Returning a number different from the fragment length from the write callback stops the transfer, which is also how you cancel a stream early. When serving a web page, turn off output buffering (ob_implicit_flush(true)) so text reaches the browser as it arrives.

What the code handles

  • Lines that start with data: carry JSON chunks; other lines (blank lines, : keep-alive comments) are skipped.
  • data: [DONE] ends the stream.
  • A chunk with an error object means the stream broke after it started; the text so far may be incomplete, so retry the whole request.
  • The last chunk before [DONE] carries usage when you send stream_options.include_usage.

See Stream format for every field.

Was this page helpful?