Streaming Responses
Parse and extract structured content from LLM streaming responses in real time using PromptStreamParser and StreamChunk.
Overview
When working with streaming chat completions, responses arrive as incremental text chunks. PromptStreamParser processes these chunks as they arrive and extracts structured content — code blocks, JSON objects, lists, tables, headings, and key-value pairs — without waiting for the full response.
This is useful for:
- Real-time UIs — render code blocks or tables as soon as they're complete
- Pipeline processing — feed extracted JSON into downstream handlers immediately
- Progress feedback — show users structured content as it streams in
StreamChunk
Each streaming chunk is represented by StreamChunk:
var chunk = new StreamChunk
{
Delta = "Hello", // Incremental text in this chunk
FullText = "Hello", // Accumulated text so far
IsComplete = false, // True only on the final chunk
FinishReason = null, // "stop", "length", etc. (final chunk only)
TokensUsed = 1 // Approximate running token count
};
| Property | Type | Description |
|---|---|---|
Delta |
string |
Incremental text received in this chunk |
FullText |
string |
Full accumulated text from all chunks so far |
IsComplete |
bool |
Whether this is the final chunk |
FinishReason |
string? |
Stop reason ("stop", "length") — final chunk only |
TokensUsed |
int |
Approximate running token count |
Basic Usage
using Prompt;
var parser = new PromptStreamParser();
// Subscribe to content extraction events
parser.OnContent += (sender, e) =>
{
Console.WriteLine($"[{e.Content.Type}] {e.Content.Content}");
};
// Feed chunks as they arrive from the API
foreach (var chunk in streamingResponse)
{
parser.Feed(chunk);
}
// Finalize and get summary
StreamParserSummary summary = parser.Complete();
Console.WriteLine($"Extracted {summary.Items.Count} items from {summary.TotalChunks} chunks");
You can also feed raw strings instead of StreamChunk objects:
parser.Feed("Here is some ```csharp\nConsole.WriteLine(\"hi\");\n```");
Content Types
The parser recognizes these content types (StreamContentType):
| Type | Detection | Example |
|---|---|---|
CodeBlock |
Fenced with triple backticks | ```csharp ... ``` |
JsonObject |
Balanced { ... } with valid JSON |
{"key": "value"} |
JsonArray |
Balanced [ ... ] with valid JSON |
[1, 2, 3] |
List |
Lines starting with - , * , + , or 1. |
- item one |
Table |
Lines starting with \| (2+ rows) |
\| col1 \| col2 \| |
KeyValue |
Key: Value pattern |
Status: Active |
Heading |
Lines starting with # |
## Section Title |
Text |
Everything not matched above | Plain paragraphs |
Each extracted item is a StreamContent object:
public class StreamContent
{
public StreamContentType Type { get; init; } // What was extracted
public string Content { get; init; } // The raw text
public string? Tag { get; init; } // Language for code, level for headings, key for KV
public object? Parsed { get; init; } // Parsed JSON (JsonElement) for JSON types
public int StartOffset { get; init; } // Start position in full response
public int EndOffset { get; init; } // End position in full response
public bool IsPartial { get; init; } // True if content is still streaming
}
Parser Options
Configure the parser with StreamParserOptions:
var parser = new PromptStreamParser(new StreamParserOptions
{
// Only extract code blocks and JSON
EnabledTypes = new HashSet<StreamContentType>
{
StreamContentType.CodeBlock,
StreamContentType.JsonObject
},
ParseJson = true, // Attempt to deserialize JSON (default: true)
EmitPartial = false, // Emit incomplete items? (default: false)
MaxContentLength = 5000, // Truncate long content (0 = no limit)
TrimContent = true // Trim whitespace (default: true)
});
Partial Content
When EmitPartial = true, the parser fires OnPartialContent events for items that haven't closed yet (e.g., a code block whose closing ``` hasn't arrived). Partial items have IsPartial = true.
parser.OnPartialContent += (sender, e) =>
{
Console.WriteLine($"[partial {e.Content.Type}] {e.Content.Content.Length} chars so far...");
};
Working with the Summary
After calling Complete(), you get a StreamParserSummary with convenient accessors:
StreamParserSummary summary = parser.Complete();
// All extracted items
List<StreamContent> items = summary.Items;
// Filtered by type
List<StreamContent> codeBlocks = summary.CodeBlocks;
List<StreamContent> jsonObjects = summary.JsonObjects;
List<StreamContent> tables = summary.Tables;
List<StreamContent> headings = summary.Headings;
// Key-value pairs as a dictionary
Dictionary<string, string> kvPairs = summary.KeyValues;
// Counts by type
Dictionary<StreamContentType, int> counts = summary.TypeCounts;
// Stats
int totalChars = summary.TotalCharacters;
int totalChunks = summary.TotalChunks;
Real-time Content Extraction Example
var parser = new PromptStreamParser();
parser.OnContent += (sender, e) =>
{
switch (e.Content.Type)
{
case StreamContentType.CodeBlock:
var lang = e.Content.Tag ?? "text";
RenderCodeBlock(lang, e.Content.Content);
break;
case StreamContentType.JsonObject:
var json = (JsonElement)e.Content.Parsed!;
ProcessStructuredData(json);
break;
case StreamContentType.Heading:
UpdateTableOfContents(e.Content.Tag!, e.Content.Content);
break;
}
};
// Feed chunks from Azure OpenAI streaming response
await foreach (var update in client.GetChatCompletionsStreamingAsync(options))
{
if (update.ContentUpdate is { } delta)
{
parser.Feed(new StreamChunk
{
Delta = delta,
FullText = accumulatedText += delta,
IsComplete = false
});
}
}
var summary = parser.Complete();
Peeking During Streaming
Use CurrentContent to inspect extracted items without finalizing the parser:
// Check what's been extracted so far (mid-stream)
IReadOnlyList<StreamContent> current = parser.CurrentContent;
if (current.Any(c => c.Type == StreamContentType.JsonObject))
{
// Already got structured data, can start processing
}
Resetting the Parser
Reuse a parser instance for multiple streams:
parser.Reset(); // Clears all state, ready for a new stream
See Also
- Error Handling — handling stream interruptions
- Options — configuring streaming parameters
- Advanced Features — combining streaming with other features