Set stream: true and the answer arrives as server-sent events. The format is the standard chat.completion.chunk shape, so any OpenAI-compatible client parses it without changes.
stream = client.chat.completions.create(model="upfyn-yuva",messages=[{"role": "user", "content": "Count to five."}],stream=True,stream_options={"include_usage": True},)for chunk in stream:if chunk.choices and chunk.choices[0].delta.content:print(chunk.choices[0].delta.content, end="", flush=True)elif chunk.usage: # the final chunk carries no choicesprint(f"\n\ncost: {chunk.usage.total_tokens} tokens")
What the stream looks like#
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"upfyn-yuva","choices":[{"index":0,"delta":{"role":"assistant","content":""}}]}data: {"id":"chatcmpl-...","choices":[{"index":0,"delta":{"content":"One"}}]}data: {"id":"chatcmpl-...","choices":[{"index":0,"delta":{"content":", two"}}]}data: {"id":"chatcmpl-...","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}data: {"id":"chatcmpl-...","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":8,"total_tokens":20},"credits_used":0.09,"credits_remaining":4821.35}data: [DONE]
- Each frame is
data:followed by one JSON object. - Text arrives in
choices[0].delta.content. finish_reasonappears on the last content-bearing frame.- The stream always ends with
data: [DONE].
Getting the cost#
By default a stream tells you nothing about what it cost. Ask for it with stream_options: { include_usage: true } and one extra frame is sent before [DONE], carrying usage, credits_used and credits_remaining. That frame has an empty choices array.
The header is a reservation, not the price
A streaming response setsX-Credits-Reserved before the body — that is the amount held up front, not what you were charged. The settled figure only ever appears in the final usage frame. If you bill your own users from the header, you will bill the wrong number.Keepalive comments#
When a model takes a while to produce its first token, we flush headers early and send SSE comment frames so proxies and load balancers do not close the connection.
: upfyn-thinking: upfyn-thinkingdata: {"id":"chatcmpl-...","choices":[{"index":0,"delta":{"content":"Here"}}]}
Lines starting with : are comments in the SSE specification, and every conformant parser — including both OpenAI SDKs — ignores them. You only need to care if you wrote your own parser: skip any line that does not begin with data: .
When a stream fails#
The status code is already 200 by the time the first byte leaves, so a mid-stream failure arrives as a frame with an error key, then [DONE]. Check every frame for it — see Errors.
Resuming a dropped stream#
If a client disconnects, the gateway keeps reading from the model to completion, so the turn finishes and bills correctly rather than being abandoned half-done. That also means you can reconnect and collect what you missed.
| Step | How |
|---|---|
| Note the stream id | Read the X-Upfyn-Stream-Id response header when the stream opens. |
| Count what you received | Keep a running index of frames consumed. |
| Reconnect | GET /v1/chat/streams/{id}?cursor={n} resumes after frame n. |
| Check status instead | GET /v1/chat/streams/{id}/status if you only need to know whether it finished. |
Disable buffering in your own proxy
We sendX-Accel-Buffering: no, which nginx honours. If you put your own reverse proxy in front, make sure it is not buffering the response — the symptom is a stream that arrives all at once at the end.