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-alivecomments) are skipped. data: [DONE]ends the stream.- A chunk with an
errorobject means the stream broke after it started; the text so far may be incomplete, so retry the whole request. - The last chunk before
[DONE]carriesusagewhen you sendstream_options.include_usage.
See Stream format for every field.
Was this page helpful?