artem_ml3 Jun 2026 09:22

I want token-by-token streaming from a chat completions endpoint, the kind that returns text/event-stream with data: lines. I do not want to pull in a vendor SDK for this, the surface is small. The part I keep getting wrong is consuming the stream incrementally with curl instead of buffering the whole response, and parsing partial chunks where a single read can contain several data: lines or half of one.

Has anyone got a clean pattern for the curl write callback plus a line buffer that handles the [DONE] sentinel? Posting what I have so far.

Replies (5)
priyar3 Jun 2026 09:52

The key is CURLOPT_WRITEFUNCTION. curl calls it repeatedly with whatever bytes arrived, which do not respect line or event boundaries. So you append to a buffer, then split off complete lines on \n and leave any trailing partial line in the buffer for the next call. Return strlen of the received chunk from the callback or curl aborts the transfer.

0
sergey_web3 Jun 2026 11:02

Here is the shape I use. The buffer lives in a closure use by reference.

PHP
$buffer = '';
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$buffer, $onToken) {
$buffer .= $data;
while (($pos = strpos($buffer, "\n")) !== false) {
$line = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 1);
$line = trim($line);
if ($line === '' || !str_starts_with($line, 'data:')) {
continue;
}
$payload = trim(substr($line, 5));
if ($payload === '[DONE]') {
return strlen($data);
}
$json = json_decode($payload, true);
if (isset($json['choices'][0]['delta']['content'])) {
$onToken($json['choices'][0]['delta']['content']);
}
}
return strlen($data);
});
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
0
cgomez3 Jun 2026 12:22

Two gotchas with that. Make sure you do not set CURLOPT_RETURNTRANSFER when using a write callback, they fight. And set CURLOPT_BUFFERSIZE low, around 256, otherwise curl batches more before calling you and the streaming feels chunky instead of smooth. The parsing logic above is correct, the perceived latency is all in the buffer size.

0
artem_ml3 Jun 2026 13:52

RETURNTRANSFER was exactly my bug, the callback never fired because curl was buffering everything for the return value. Removed it and lowered BUFFERSIZE, now I get tokens as they arrive. For anyone in Swoole, do this inside a coroutine with the Swoole curl hook enabled so the wait does not block the worker.

0
nataS3 Jun 2026 15:42

If you also want to forward the stream to a browser as SSE, echo each token wrapped as data: ...\n\n and call flush from inside the same callback. Under Swoole you push to the response object instead of echo. Just remember the upstream [DONE] and your downstream [DONE] are separate concerns, do not blindly proxy the raw upstream lines or you leak the provider format to your client.

0
Write a reply
Markdown. ```php blocks are runnable.