llm-stream-parser
v1.1.0
Published
A TypeScript library for parsing and processing structured data from LLM streaming responses with custom tag definitions and event-driven architecture
Maintainers
Readme
🚀 LLM Stream Parser
🎯 Professional-grade XML parser optimized for Large Language Model streaming responses
Real-time parsing • Type-safe • Event-driven • Production-ready
🌟 Overview
LLM Stream Parser is a high-performance, TypeScript-first library designed specifically for parsing structured XML tags from streaming Large Language Model responses. Whether you're building AI chatbots, processing OpenAI streams, or handling Claude responses, this library provides the robust, real-time parsing capabilities you need.
🎯 Why LLM Stream Parser?
- 🔄 Stream-First Design: Parse content as it arrives, no waiting for completion
- 🧠 LLM-Optimized: Built specifically for AI model output patterns
- ⚡ High Performance: >10,000 tags/second with intelligent buffering
- 🛡️ Type-Safe: Full TypeScript support with strict typing
- 🏗️ Dual Parsing Modes: Simple flat parsing or complex nested hierarchies
- 🎨 Flexible Configuration: Extensive customization and validation options
- 📊 Event-Driven: Real-time monitoring with comprehensive event system
- 🧪 Production Ready: Comprehensive testing, error handling, and documentation
📋 Table of Contents
- 🌟 Overview
- ⚡ Quick Start
- 📦 Installation
- 🎯 Real-World Example
- 📖 API Reference
- 🔧 Configuration
- 🎨 Advanced Usage
- 🚀 Performance
- 🧪 Testing
- 🤝 Contributing
✨ Key Features
🎯 Core Capabilities
- ✅ Stream Processing - Real-time parsing as data arrives
- ✅ Dual Modes - Flat parsing or nested hierarchies
- ✅ Type Safety - Full TypeScript support
- ✅ Event System - Real-time monitoring and callbacks
- ✅ Error Recovery - Robust error handling and recovery
🔧 Advanced Features
- ✅ Validation - Content and attribute validation
- ✅ Transformation - Built-in content transformers
- ✅ Buffer Management - Intelligent memory management
- ✅ Performance - Optimized for high throughput
- ✅ Extensibility - Modular, customizable architecture
📦 Installation
npm install llm-stream-parseryarn add llm-stream-parserpnpm add llm-stream-parser⚡ Quick Start
Get up and running in under 2 minutes:
🚀 Basic Usage
import { LLMStreamParser } from 'llm-stream-parser';
// 1. Create parser
const parser = new LLMStreamParser();
// 2. Define tags
parser.addSimpleTags(['thinking', 'response', 'code']);
// 3. Handle events
parser.on('tag_completed', tag => {
console.log(`✅ ${tag.tagName}: ${tag.content}`);
});
// 4. Parse streaming content
parser.parse('<thinking>Analyzing the problem...</thinking>');
parser.parse("<response>Here's the solution:</response>");
parser.finalize();🧠 LLM Integration Example
// Perfect for Gemini, OpenAI, Claude, or any streaming LLM
import * as dotenv from 'dotenv';
import { GoogleGenAI } from '@google/genai';
import { LLMStreamParser } from 'llm-stream-parser';
dotenv.config();
const systemPrompt = `
You are an instructor creating technical training content.
You must respond in the following XML format:
<lesson>
<title>Title here</title>
<step number="1" difficulty="easy">
<explanation>exp here</explanation>
<code>code here</code>
</step>
<summary>summary_here</summary>
</lesson>
`;
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
async function main() {
const question = 'Create a lesson on developing a REST API with Express.js';
const parser = new LLMStreamParser();
parser.on('tag_completed', tag => {
console.log(`${JSON.stringify(tag)}\n`);
});
parser.addSimpleTags([
'lesson',
'title',
'step',
'explanation',
'code',
'summary',
]);
const response = await ai.models.generateContentStream({
model: 'gemini-2.0-flash',
contents: `${systemPrompt}\n\n${question}`,
});
for await (const chunk of response) {
if (chunk.text) {
parser.parse(chunk.text);
}
}
}
main().catch(console.error);🎯 Real-World Example
See the parser in action with our live streaming demo that simulates actual LLM responses:
🌊 Chunk Streaming Demo
# Run the real-world streaming demo
node demo-streaming.jsThis demo showcases:
- Realistic chunk streaming (25-character chunks with delays)
- Complex nested XML parsing in real-time
- Event-driven processing with live updates
- Performance monitoring and statistics
- Error handling and recovery
Sample Output:
🚀 LLM Stream Parser - Real World Demo
🧪 Starting Chat Conversation Demo
📋 Registered 10 tag types
🌊 Starting streaming simulation...
📦 Chunk 1/47: "<conversation id=\"support"
🔓 Tag opened: <conversation> (depth: 1)
📦 Chunk 2/47: "-chat\" timestamp=\"2024-0"
📦 Chunk 3/47: "1-15T14:30:00Z\">\n <me"
🔓 Tag opened: <message> (depth: 2)
📦 Chunk 4/47: "ssage role=\"user\" id=\"m"
...
✅ Tag completed: <thinking> with 108 chars
📝 Content preview: "The user is experiencing login issues. I should ga..."
✅ Tag completed: <message> with 891 chars
🎉 Parsing complete! Total tags: 15
📊 DEMO RESULTS
──────────────────────────────────────────────────────
⏱️ Processing time: 2485ms
📦 Total chunks: 47
📝 Content length: 1174 characters
🏷️ Tags completed: 15
🚨 Errors: 0
✅ Success rate: 100.0%🎨 Advanced Demo Features
The demo includes sophisticated examples like:
- Chat conversations with nested thinking blocks
- AI reasoning with evidence and analysis
- Code generation with syntax highlighting
- Real-time monitoring with performance metrics
- Error simulation and recovery testing
Perfect for testing integration with OpenAI, Claude, Gemini, or any LLM that uses XML-structured responses.
📖 API Reference
🏗️ Core Classes
The primary interface for all parsing operations.
class LLMStreamParser<T extends BaseTag = BaseTag> {
constructor(config?: ParserConfig);
}🔧 Tag Management
// Define custom tags with validation and transformation
defineTag(definition: TagDefinition<T>): this;
defineTags(definitions: TagDefinition<T>[]): this;
// Quick setup for simple tags
addSimpleTag(tagName: string, options?: SimpleTagOptions): this;
addSimpleTags(tagNames: string[]): this;
// Tag registry management
removeTag(tagName: string): boolean;
hasTag(tagName: string): boolean;
getRegisteredTags(): readonly string[];⚡ Parsing Operations
// Stream processing
parse(chunk: string): void; // Process individual chunks
parseComplete(content: string): void; // Parse complete content at once
finalize(): void; // Complete parsing session
reset(): void; // Reset parser state📊 State & Monitoring
getState(): ParserState; // Current parser state
getStats(): ParserStats; // Performance statistics
getCurrentDepth(): number; // Current nesting depth
getCurrentPath(): string; // Current tag path
getBufferSize(): number; // Current buffer usage🎯 Event System
// Event listeners
on<K extends keyof ParserEvents<T>>(event: K, listener: ParserEvents<T>[K]): this;
off<K extends keyof ParserEvents<T>>(event: K, listener: ParserEvents<T>[K]): this;
once<K extends keyof ParserEvents<T>>(event: K, listener: ParserEvents<T>[K]): this;
// Utility methods
clone(): LLMStreamParser<T>; // Create parser copy🎯 Key Types & Interfaces
interface ParserConfig {
// 🔧 Basic Configuration
maxBufferSize?: number; // Default: 1MB - Buffer size limit
trimWhitespace?: boolean; // Default: true - Auto-trim content
caseSensitive?: boolean; // Default: true - Case-sensitive tags
maxDepth?: number; // Default: 10 - Max nesting depth
autoCloseUnclosed?: boolean; // Default: false - Auto-close tags
// ⚡ Performance Configuration
batchSize?: number; // Default: 1000 - Processing batch size
enableStatistics?: boolean; // Default: true - Track performance
}🎯 Common Configurations
// 🚀 High Performance Setup
const performanceConfig: ParserConfig = {
maxBufferSize: 2 * 1024 * 1024, // 2MB buffer
batchSize: 2000, // Large batches
enableStatistics: false, // Skip stats for speed
trimWhitespace: false, // Skip trimming
};
// 🔒 Strict Validation Setup
const strictConfig: ParserConfig = {
caseSensitive: true, // Exact case matching
autoCloseUnclosed: false, // Require proper closing
enableStatistics: true, // Monitor for errors
maxDepth: 5, // Limit nesting
};
// 🧠 LLM Optimized Setup
const llmConfig: ParserConfig = {
maxDepth: 15, // Deep nesting for AI
autoCloseUnclosed: true, // Forgive LLM errors
trimWhitespace: true, // Clean AI output
};interface TagDefinition<T extends BaseTag = BaseTag> {
readonly tagName: T['tagName'];
// ✅ Validation
validateContent?: (content: string) => ValidationResult;
validateAttributes?: (attributes?: T['attributes']) => ValidationResult;
validateChildren?: (children: NestedTag[]) => ValidationResult;
// 🔄 Transformation
transformContent?: (content: string) => string;
transformAttributes?: (
attributes?: Record<string, unknown>
) => T['attributes'];
// ⚙️ Configuration
allowChildren?: boolean; // Default: true
allowSelfClosing?: boolean; // Default: true
// 🎯 Event Handlers
onStart?: (tagName: T['tagName'], attributes?: T['attributes']) => void;
onContentUpdate?: (partialContent: string, tag: Partial<T>) => void;
onComplete?: (tag: T) => void;
onChildAdded?: (child: NestedTag, parent: T) => void;
}🔧 Configuration
⚙️ Default Configuration
const defaultConfig: ParserConfig = {
maxBufferSize: 1024 * 1024, // 1MB buffer
trimWhitespace: true, // Auto-trim content
caseSensitive: true, // Case-sensitive tags
maxDepth: 10, // Max nesting depth
autoCloseUnclosed: false, // Strict tag closing
};🎯 Configuration Examples
import { LLMStreamParser } from 'llm-stream-parser';
const chatGPTParser = new LLMStreamParser({
maxDepth: 8, // Reasonable nesting limit
autoCloseUnclosed: true, // Forgive incomplete tags
trimWhitespace: true, // Clean output
caseSensitive: false, // Flexible tag matching
});
// Define common ChatGPT tags
chatGPTParser.addSimpleTags([
'thinking',
'analysis',
'answer',
'code',
'explanation',
'summary',
]);
// Handle streaming response
chatGPTParser.on('tag_completed', tag => {
console.log(`ChatGPT ${tag.tagName}:`, tag.content);
});const claudeParser = new LLMStreamParser({
maxDepth: 12, // Claude can be deeply nested
autoCloseUnclosed: true, // Handle incomplete responses
maxBufferSize: 2 * 1024 * 1024, // Larger buffer for long responses
});
// Claude-specific tags
claudeParser.addSimpleTags([
'thinking',
'reasoning',
'analysis',
'conclusion',
'example',
'code',
'explanation',
'clarification',
]);const performanceParser = new LLMStreamParser({
maxBufferSize: 4 * 1024 * 1024, // 4MB buffer
batchSize: 5000, // Large batch processing
enableStatistics: false, // Disable overhead
trimWhitespace: false, // Skip trimming
});
// Minimal event handling for maximum speed
performanceParser.on('tag_completed', tag => {
// Process tags with minimal overhead
processTag(tag.tagName, tag.content);
});🎨 Advanced Usage
🤖 LLM Integration Patterns
import { LLMStreamParser } from 'llm-stream-parser';
class AIChatHandler {
private parser = new LLMStreamParser({
autoCloseUnclosed: true,
});
constructor() {
this.setupParser();
}
private setupParser() {
// Define chat-specific tags
this.parser.addSimpleTags([
'thinking',
'response',
'code',
'explanation',
'question',
]);
// Real-time UI updates
this.parser.on('tag_started', tagName => {
this.showTypingIndicator(tagName);
});
this.parser.on('tag_content_update', (tagName, content) => {
this.updateChatMessage(tagName, content);
});
this.parser.on('tag_completed', tag => {
this.finalizeChatMessage(tag);
});
}
async handleStreamingResponse(stream: ReadableStream) {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
this.parser.parse(chunk);
}
} finally {
this.parser.finalize();
reader.releaseLock();
}
}
private showTypingIndicator(tagName: string) {
// Update UI to show AI is generating specific content type
console.log(`🤖 AI is generating ${tagName}...`);
}
private updateChatMessage(tagName: string, content: string) {
// Real-time content updates in chat interface
document.getElementById(`${tagName}-content`).textContent = content;
}
private finalizeChatMessage(tag: any) {
// Apply final formatting and save to chat history
console.log(`✅ Completed ${tag.tagName}:`, tag.content);
}
}import {
LLMStreamParser,
ContentValidators,
ContentTransformers,
} from 'llm-stream-parser';
class ReasoningAnalyzer {
private parser = new LLMStreamParser({
maxDepth: 15,
});
constructor() {
this.setupAdvancedTags();
}
private setupAdvancedTags() {
// Evidence tag with validation
this.parser.defineTag({
tagName: 'evidence',
validateAttributes: attrs => {
if (!attrs?.source) return 'Evidence must have a source';
if (
!attrs?.strength ||
!['weak', 'medium', 'strong'].includes(attrs.strength as string)
) {
return 'Evidence strength must be weak, medium, or strong';
}
return true;
},
transformContent: ContentTransformers.trim(),
onComplete: tag => {
this.analyzeEvidence(tag);
},
});
// Analysis tag with confidence scoring
this.parser.defineTag({
tagName: 'analysis',
validateAttributes: attrs => {
const confidence = attrs?.confidence as number;
if (confidence && (confidence < 0 || confidence > 1)) {
return 'Confidence must be between 0 and 1';
}
return true;
},
onComplete: tag => {
this.trackAnalysisQuality(tag);
},
});
// Conclusion with summary generation
this.parser.defineTag({
tagName: 'conclusion',
transformContent: ContentTransformers.chain(
ContentTransformers.trim(),
ContentTransformers.normalizeWhitespace()
),
onComplete: tag => {
this.generateSummary(tag);
},
});
}
private analyzeEvidence(tag: any) {
const strength = tag.attributes?.strength;
const source = tag.attributes?.source;
console.log(`📊 Evidence Analysis:
Source: ${source}
Strength: ${strength}
Content: ${tag.content.substring(0, 100)}...`);
}
private trackAnalysisQuality(tag: any) {
const confidence = tag.attributes?.confidence || 0.5;
console.log(`🎯 Analysis Quality Score: ${(confidence * 100).toFixed(1)}%`);
}
private generateSummary(tag: any) {
console.log(`📝 Conclusion Summary: ${tag.content}`);
}
}🔧 Advanced Configuration
Custom Validators
import { ContentValidators, AttributeValidators } from 'llm-stream-parser';
const parser = createParser();
parser.defineTag({
tagName: 'email',
validateContent: ContentValidators.pattern(
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
'Invalid email format'
),
validateAttributes: AttributeValidators.required(['type']),
transformContent: ContentTransformers.toLowerCase(),
});
parser.defineTag({
tagName: 'phone',
validateContent: ContentValidators.custom(content => {
const cleaned = content.replace(/\D/g, '');
return cleaned.length >= 10 || 'Phone number must have at least 10 digits';
}),
transformContent: content => content.replace(/\D/g, ''),
});Custom Transformers
import { ContentTransformers } from 'llm-stream-parser';
// Create custom transformer
const sanitizeHtml = ContentTransformers.custom(
content => content.replace(/<script[^>]*>.*?<\/script>/gi, ''),
'Failed to sanitize HTML'
);
parser.defineTag({
tagName: 'content',
transformContent: ContentTransformers.chain(
ContentTransformers.stripHtml(),
sanitizeHtml,
ContentTransformers.trim(),
ContentTransformers.normalizeWhitespace()
),
});Buffer Management
const parser = createParser({
maxBufferSize: 512 * 1024, // 512KB limit
batchSize: 500, // Process in smaller batches
});
// Monitor buffer usage
parser.on('stats_updated', stats => {
const usage = (stats.bufferSize / (512 * 1024)) * 100;
if (usage > 80) {
console.warn(`Buffer usage high: ${usage.toFixed(1)}%`);
}
});🏗️ Architecture
Modular Design
The library follows a clean modular architecture with focused responsibilities:
src/
├── types/ # Type definitions (6 modules)
│ ├── base.ts # Core types (BaseTag, NestedTag, etc.)
│ ├── config.ts # Configuration interfaces
│ ├── errors.ts # Error handling types
│ ├── events.ts # Event system types
│ ├── schema.ts # Schema definition types
│ └── index.ts # Type exports
├── core/ # Core functionality (5 modules)
│ ├── buffer-manager.ts # Stream buffer management
│ ├── tag-matcher.ts # XML pattern matching
│ ├── validator.ts # Content validation
│ ├── transformer.ts # Content transformation
│ └── stream-parser.ts # Main parsing engine
├── llm-stream-parser.ts # User-facing API
└── index.ts # Main exportsDesign Principles
- 🔨 Single Responsibility: Each module has one focused purpose
- 📦 Modular: Files kept under 300 lines for maintainability
- 🛡️ Type Safety: Full TypeScript coverage with strict typing
- ⚡ Performance: Optimized for high-throughput streaming
- 🔄 Extensibility: Easy to extend with new functionality
- 🧪 Testability: Isolated modules for comprehensive testing
🚀 Performance
📊 Benchmarks
🎯 Processing Speed
- Throughput: >10,000 tags/second
- Latency: <1ms average completion
- Scalability: Linear with content size
- Memory: O(1) with buffer limits
⚡ Real-World Performance
- OpenAI Streams: 15-25ms processing
- Large Documents: 500MB+ supported
- Concurrent Parsers: 100+ instances
- Memory Usage: <10MB typical
🔧 Optimization Strategies
// For maximum performance (>50,000 tags/sec)
const speedParser = new LLMStreamParser({
maxBufferSize: 8 * 1024 * 1024, // 8MB buffer
batchSize: 10000, // Large batches
enableStatistics: false, // Disable overhead
trimWhitespace: false, // Skip processing
caseSensitive: false, // Faster matching
});
// Minimal event handling
speedParser.on('tag_completed', tag => {
// Direct processing without logging
processTagDirectly(tag.tagName, tag.content);
});// For memory-constrained environments (<1MB usage)
const memoryParser = new LLMStreamParser({
maxBufferSize: 32 * 1024, // 32KB buffer
batchSize: 50, // Small batches
enableStatistics: true, // Monitor usage
trimWhitespace: true, // Clean up immediately
});
// Monitor memory usage
memoryParser.on('stats_updated', stats => {
if (stats.bufferSize > 24 * 1024) {
console.warn('High memory usage detected');
}
});// Production-ready configuration
const productionParser = new LLMStreamParser({
maxBufferSize: 2 * 1024 * 1024, // 2MB buffer
batchSize: 1000, // Balanced batches
enableStatistics: true, // Monitor performance
autoCloseUnclosed: true, // Handle malformed input
maxDepth: 20, // Support complex nesting
});
// Comprehensive error handling
productionParser.on('parse_error', (error, context) => {
logger.error('Parser error', { error, context });
metrics.increment('parser.errors');
});
// Performance monitoring
productionParser.on('stats_updated', stats => {
metrics.gauge('parser.buffer_size', stats.bufferSize);
metrics.gauge('parser.tags_processed', stats.totalTagsParsed);
});🧪 Testing
Unit Tests
import { createParser } from 'llm-stream-parser';
describe('LLMStreamParser', () => {
let parser: LLMStreamParser;
beforeEach(() => {
parser = createParser();
});
test('should parse simple tags', () => {
parser.addSimpleTag('test');
const results: any[] = [];
parser.on('tag_completed', tag => results.push(tag));
parser.parseComplete('<test>Hello World</test>');
expect(results).toHaveLength(1);
expect(results[0].tagName).toBe('test');
expect(results[0].content).toBe('Hello World');
});
test('should handle streaming content', () => {
parser.addSimpleTag('stream');
const results: any[] = [];
parser.on('tag_completed', tag => results.push(tag));
parser.parse('<stream>Part 1 ');
parser.parse('Part 2 ');
parser.parse('Part 3</stream>');
parser.finalize();
expect(results[0].content).toBe('Part 1 Part 2 Part 3');
});
});Integration Tests
describe('Integration Tests', () => {
test('should handle complex nested structure', () => {
const parser = createParser();
parser.addSimpleTags(['doc', 'section', 'para']);
const structure: any[] = [];
parser.on('tag_closed', (tag, depth, path) => {
structure.push({ tag: tag.tagName, depth, path });
});
parser.parseComplete(`
<doc>
<section>
<para>Content</para>
</section>
</doc>
`);
expect(structure).toHaveLength(3);
expect(structure[0].path).toBe('doc/section/para');
});
});🤝 Contributing
🛠️ Development Setup
# Clone the repository
git clone https://github.com/brankrts/llm-stream-parser.git
cd llm-stream-parser
# Install dependencies
npm install
# Run tests
npm test
# Build the library
npm run build
# Run linting
npm run lint
# Watch mode for development
npm run dev📋 Code Standards
- ✅ TypeScript - Strict mode enabled
- ✅ ESLint - Follow configuration rules
- ✅ File Size - Keep modules under 300 lines
- ✅ Test Coverage - Write comprehensive tests
- ✅ Documentation - Document public APIs
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- 🧠 Inspired by the need for real-time LLM response parsing
- 🏗️ Built with modern TypeScript best practices
- 🤖 Designed for the era of AI-powered applications
📞 Support & Resources
| Resource | Link | | -------------------- | -------------------------------------------------------------- | | 📖 Documentation | Wiki | | 🐛 Bug Reports | Issues | | 📧 Email | [email protected] |
🚀 Ready to parse your LLM streams?
npm install llm-stream-parser⭐ Star this repository if you find it useful!
Built with ❤️ for the AI community
