Streaming
SSE (Server-Sent Events) streaming delivers tokens in real-time as the LLM generates them, instead of waiting for the complete response.
Basic Usage
Provide Stream = True and an OnDelta callback:
type
TMyHandler = class
procedure HandleDelta(const Delta: RawUtf8);
end;
procedure TMyHandler.HandleDelta(const Delta: RawUtf8);
begin
Write(Delta); // print each token as it arrives
end;
var
Handler: TMyHandler;
Response: TLLMResponse;
begin
Handler := TMyHandler.Create;
try
Response := LLM.Chat('openai/gpt-4o',
Messages, // TLLMMessageDynArray
Tools, // TLLMToolDynArray (can be empty)
'', // ApiKey (from .env)
'', // ApiBase (default)
4096, // MaxTokens
1.0, // Temperature
True, // Stream
Handler.HandleDelta); // OnDelta callback
// Response is complete here — Content, ToolCalls, FinishReason, Usage populated
// Note: Created and RawResponse are NOT populated via the streaming path
WriteLn;
WriteLn('Finish: ', Response.FinishReason);
WriteLn('Usage: ', Response.Usage.ToString);
finally
Handler.Free;
end;
end;
The OnDelta callback is TOnLLMStreamDelta = procedure(const Delta: RawUtf8) of object — it must be a method, not a standalone procedure.
How It Works
Architecture
TLiteLLMHTTPHandler.ChatStream
|
v
THttpClientSocket.Request(... OutStream := SSE)
| |
| HTTP chunks arrive ------> TLiteLLMSSEStream.Write()
| |
| v
| ProcessBuffer() — parse SSE lines
| |
| v
| HandleLine() — accumulate event/data
| |
| v
| DispatchEvent() — on empty line
| |
| v
| ILiteLLMProviderConfig.ProcessSSEEvent()
| |
| +---> OnDelta(text fragment)
| +---> accumulate tool calls
| +---> accumulate usage
v
SSE.GetResponse() -> TLLMResponse (complete)
TLiteLLMSSEStream
TLiteLLMSSEStream (in mormot.ai.litellm.streaming.pas) is a TStream subclass that intercepts Write calls from THttpClientSocket's chunked transfer decoding. It:
- Buffers incoming bytes
- Splits on
\nboundaries (per W3C SSE spec) - Parses
event:anddata:fields - On empty line: dispatches the accumulated event to the provider's
ProcessSSEEvent - Handles the
[DONE]marker (OpenAI convention)
SSE Protocol
Per the W3C Server-Sent Events specification:
- Lines starting with
:are comments (ignored) event: <type>sets the event type for the next dispatchdata: <payload>sets the data (multipledata:lines are joined with\n)- An empty line dispatches the current event
[DONE]in data field signals stream end (OpenAI convention, handled by base stream)
Provider-Specific Processing
Each provider overrides ProcessSSEEvent(EventType, Data, Accumulator, OnDelta):
OpenAI: Ignores EventType. Parses choices[0].delta.content for text, choices[0].delta.tool_calls for tool call fragments. Usage comes from the final chunk when stream_options.include_usage is enabled.
Anthropic: Uses EventType as a state machine driver (message_start, content_block_start, content_block_delta, content_block_stop, message_delta, error). Text deltas arrive in content_block_delta with type: text_delta. Tool arguments arrive as input_json_delta fragments.
Gemini: Ignores EventType. Each chunk contains a complete candidates[0].content.parts[] array. Text parts fire OnDelta, function call parts create tool call entries. Usage metadata (usageMetadata) is updated from each chunk.
Streaming with Tool Calls
Tool calls are accumulated during streaming and available in the final response:
Response := LLM.Chat('anthropic/claude-sonnet-4-20250514',
Messages, Tools, '', '', 4096, 1.0, True, Handler.HandleDelta);
if Response.HasToolCalls then
begin
// Tool calls are fully parsed at this point
for i := 0 to High(Response.ToolCalls) do
WriteLn('Tool: ', Response.ToolCalls[i].Name);
end;
Tool call arguments arrive as JSON string fragments during streaming. The provider accumulates them in TLLMToolCall.RawData and parses the complete JSON when the tool call block finishes (content_block_stop for Anthropic, finish_reason for OpenAI).
Streaming via ChatRaw
For proxy scenarios where you want to pass raw parameters:
var
Params: variant;
begin
Params := _Obj([
'max_tokens', 2048,
'temperature', 0.5]);
Response := LLM.ChatRaw('gemini/gemini-2.5-flash',
Messages, Tools, Params,
'', '', // ApiKey, ApiBase
True, // Stream
Handler.HandleDelta);
end;
Callbacks During Streaming
If you've registered ILiteLLMCallback implementations, the handler fires:
OnStart— before the HTTP requestOnSuccess— after streaming completes (with the finalTLLMResponse)OnFailure— if an error occurs during streaming
Note: ILiteLLMCallback.OnStreamDelta is available on the callback interface but must be explicitly called by your application if needed — the handler does not fire it automatically during streaming (to avoid double-dispatching with OnDelta).
Error Handling During Streaming
If the HTTP response status is not 2xx, an ELiteLLM exception is raised after the request completes (the status is checked after Sock.Request returns, not before streaming begins). If the stream encounters an error event (Anthropic event: error), the provider raises ELiteLLM from within ProcessSSEEvent.
try
Response := LLM.Chat('anthropic/claude-sonnet-4-20250514',
Messages, Tools, '', '', 4096, 1.0, True, Handler.HandleDelta);
except
on E: ELiteLLMRateLimit do
WriteLn('Rate limited during stream: ', E.Message);
on E: ELiteLLM do
WriteLn('Stream error: ', E.StatusCode, ' ', E.Message);
end;
Timeouts
The default timeout for streaming is 600000ms (10 minutes), set in TLiteLLMHTTPHandler.ChatStream. This applies to both the initial connection and the ongoing receive. For long-running generations, this should be sufficient. The timeout is not currently configurable through TLiteLLM.Chat — for custom timeouts, use Handler.ChatStream directly.