Streaming
Both APIs support real-time streaming of generated tokens, so you can display partial results as they arrive.
Chat Completions Streaming
Set stream=true and provide a callback:
chat = Chat(model="gpt-5.4-mini", stream=true)
push!(chat, Message(Val(:system), "You are a poet."))
push!(chat, Message(Val(:user), "Write a very short 2-line poem about coding."))
task = chatrequest!(chat, callback=function(chunk, close)
if chunk isa String
print(chunk)
elseif chunk isa Message
println("\n--- done ---")
end
end)
result = fetch(task)
if result isa LLMSuccess
println(result.message.content)
else
println("Request failed — see result for details")
endRequest failed — see result for detailsThe Chat Completions callback fires in a fixed sequence: with a String argument as text arrives — each argument is newly-generated text forwarded verbatim (several wire deltas may be coalesced into one callback), so multibyte characters are never split across chunk boundaries — and then exactly once at end-of-stream with the fully assembled Message, whose content equals the concatenation of every forwarded String.
Stopping a Stream Early
The callback receives a Ref{Bool} that you can set to true to stop streaming:
task = chatrequest!(chat, callback=function(chunk, close)
if chunk isa String
print(chunk)
if contains(chunk, "bad word")
close[] = true # stop the stream
end
end
end)Streamed Tool Calls
When the model streams tool calls, pass on_tool_call to be notified as each call completes. It fires exactly once per tool call, in call order, receiving a fully assembled ToolCall whose arguments are already parsed (a zero-argument call arrives as an empty Dict). The text callback and on_tool_call are independent, so a single request can stream assistant text and surface tool calls as they finish:
# weather_tool defined as in the Tool Calling guide
chat = Chat(model="gpt-5.2", tools=[weather_tool], stream=true)
push!(chat, Message(Val(:system), "Use the tools you are given."))
push!(chat, Message(Val(:user), "What's the weather in Paris and Tokyo?"))
task = chatrequest!(chat;
callback = (chunk, close) -> chunk isa String && print(chunk),
on_tool_call = tc -> println("\ntool call: ", tc.func.name, " ", tc.func.arguments),
)
result = fetch(task)on_tool_call is supported on the chatrequest! streaming path for every chat provider (OpenAI-wire, native Anthropic, native Gemini); the Responses-API respond path does not surface it. It is a notification hook — the final assembled Message still carries every tool call (alongside any assistant text), so code that does not set on_tool_call loses nothing and can read result.message.tool_calls after fetch.
Responses API Streaming
The Responses API provides an even cleaner streaming interface using Julia's do-block syntax:
task = respond("Write a haiku about Julia programming.", model="gpt-5.4-mini") do chunk, close
if chunk isa String
print(chunk)
elseif chunk isa UniLM.ResponseObject
println("\nDone! Status: ", chunk.status)
end
end
result = fetch(task)
if result isa ResponseSuccess
println(output_text(result))
else
println("Request failed — ", output_text(result))
endRequest failed — Error: KeyError: key "OPENAI_API_KEY" not foundThe do-block form automatically sets stream=true.
With Explicit Configuration
r = Respond(
input="Explain quantum computing step by step",
model="gpt-5.2",
stream=true,
max_output_tokens=2000,
)
println("Stream enabled: ", r.stream)
println("Request preview:")
println(JSON.json(r))Stream enabled: true
Request preview:
{"input":"Explain quantum computing step by step","max_output_tokens":2000,"model":"gpt-5.2","stream":true}Streaming Across Providers
Streaming is not OpenAI-only. The native Anthropic (ANTHROPICServiceEndpoint) and native Gemini (GEMINIServiceEndpoint) backends stream with the same callback / do-block API shown above — only the service (and model) change:
# Native Anthropic streaming (Chat Completions)
chat = Chat(service=ANTHROPICServiceEndpoint, stream=true)
push!(chat, Message(Val(:system), "You are a poet."))
push!(chat, Message(Val(:user), "Two lines about the sea."))
task = chatrequest!(chat, callback=(chunk, close) -> chunk isa String && print(chunk))
fetch(task)
# Native Gemini streaming (Chat Completions)
chat = Chat(service=GEMINIServiceEndpoint, stream=true)
push!(chat, Message(Val(:system), "You are a poet."))
push!(chat, Message(Val(:user), "Two lines about the mountains."))
task = chatrequest!(chat, callback=(chunk, close) -> chunk isa String && print(chunk))
fetch(task)Streaming preserves provider-native fidelity too: for Anthropic streams, the content blocks (including thinking blocks and their signatures) are re-assembled verbatim from the SSE deltas and attached to the final Message.provider_content, so a streamed tool-calling turn round-trips exactly like a non-streamed one.
Providers on the OpenAI-compatible Chat Completions standard (DeepSeek, Ollama, vLLM, LM Studio, …) stream through the same stream=true + callback path.
Dropped SSE Payloads
An SSE data: payload the parser cannot read is dropped rather than allowed to abort the turn, and each drop is counted for that stream. The count rides the result as sse_dropped — on LLMSuccess, LLMFailure, ResponseSuccess and ResponseFailure alike — and a single warning naming the count, model and surface fires once the stream finalizes, not per line. sse_dropped == 0 means a clean stream, and is what a non-streamed call always reports. A non-zero count means the turn was assembled from an incomplete wire: a provider-side or transport anomaly, never routine operation. On a truncated stream it is often the reason no message could be built at all, so it is worth reading on a failure result as well as on a success.
Notes
- Streaming runs on a separate Julia thread via
Threads.@spawn. Make sure Julia is started with multiple threads (julia -t auto). - The returned
Taskcan befetched to get the final result. - The
closeRef{Bool}can be set totruefrom the callback to terminate the stream early. - On completion, the Chat Completions callback receives a
Message; the Responses API callback receives aResponseObject. - Streamed usage: set
stream_options=Dict("include_usage" => true)to capture token usage — it lands on the result's.usageonce the stream completes. Empty-choiceschunks,:keep-alive comment lines, and provider preambles (e.g. Azure content-filter results) are all tolerated without affecting the stream. - A provider error mid-stream on an otherwise-
200response (e.g. an Anthropicoverloaded_error) surfaces as anLLMFailure/LLMCallError, never a truncatedLLMSuccess— theelsebranch in the examples above catches it. - One
Chatper in-flight call. AChatis unsynchronized mutable state, so do not share one across concurrent streams, and do notpush!to it while its stream task is still running. Useforkto fan out — see Concurrency.
See Also
- Timeouts & Retries — the stream idle bound, what a completed-then-torn-down turn resolves to, and the concurrency contracts.