LiteLLM-RS Streaming Architecture. Covers UnifiedSSEParser, SSETransformer trait, VecDeque buffering, provider-specific transformers, and real-time event handling.
Provider streaming lives in src/core/providers/base/sse.rs plus per-provider
transformers under src/core/providers/base/sse/ (openai.rs, anthropic.rs,
gemini.rs, cohere.rs, databricks.rs). The layer consumes a provider's raw
SSE byte stream and yields Result<ChatChunk, ProviderError> items in an
OpenAI-compatible shape, so the server routes never see provider-specific
formats.
┌─────────────────────────────────────────────────────────────────┐
│ Provider SSE byte stream │
│ reqwest::Response::bytes_stream() │
│ (OpenAI, Anthropic, Google, ...) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ UnifiedSSEStream<S, T> │
│ - polls upstream bytes, feeds UnifiedSSEParser │
│ - chunk_buffer: VecDeque<ChatChunk>, capped at 10_000 │
│ - Item = Result<ChatChunk, ProviderError> │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ UnifiedSSEParser<T> │
│ - String line buffer (incomplete tail retained across reads) │
│ - SSEEvent field parsing, multi-line data joining │
│ - end-marker / finish_stream dispatch │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SSETransformer (per provider) │
│ - transform_chunk / transform_stream_chunk │
│ - normalizes wire format to ChatChunk │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Server route re-serialization │
│ ChatChunk -> SSE frames ("data: {...}\n\n") + final [DONE] │
└─────────────────────────────────────────────────────────────────┘
The parser owns its transformer: UnifiedSSEParser<T: SSETransformer> calls
back into T while parsing, so there is no separate processing stage between
parser and transformer.
// src/core/providers/base/sse.rs
#[derive(Debug, Clone)]
pub struct SSEEvent {
pub event_type: Option<String>,
pub data: String,
pub id: Option<String>,
pub retry: Option<u64>,
}
SSEEvent::from_line(&str) -> Option<SSEEvent> parses one SSE field line:
: comment lines return None.data, event, id, and retry set the matching field; whitespace after
the colon is trimmed.retry must parse as u64, otherwise None; unknown fields return None.The parser accumulates multiple data lines of one event, joining them with
\n, and dispatches on the blank line that terminates the event.
// src/core/providers/base/sse.rs
pub trait SSETransformer: Send + Sync {
fn provider_name(&self) -> &'static str;
fn is_end_marker(&self, data: &str) -> bool {
data.trim() == "[DONE]"
}
fn transform_chunk(&self, data: &str) -> Result<Option<ChatChunk>, ProviderError>;
fn transform_stream_chunk(&self, data: &str) -> Result<Option<ChatChunk>, ProviderError> {
self.transform_chunk(data)
}
fn finish_stream(&self) -> Result<Option<ChatChunk>, ProviderError> {
Ok(None)
}
fn parse_finish_reason(&self, reason: &str) -> Option<FinishReason> { ... }
}
ProviderError
(crate::core::providers::unified_provider::ProviderError). There is no
dedicated StreamError enum.parse_finish_reason maps case-insensitively:
stop|end_turn -> Stop, length|max_tokens -> Length,
tool_calls|function_call|tool_use -> ToolCalls,
content_filter|safety|recitation -> ContentFilter,
stop_sequence -> StopSequence, refusal -> Refusal,
pause_turn -> PauseTurn; unknown strings yield None.OpenAICompatibleTransformer,
AnthropicTransformer, GeminiTransformer, CohereTransformer,
DatabricksTransformer (see
reference/provider-transformers.md).// src/core/providers/base/sse.rs
pub struct UnifiedSSEParser<T: SSETransformer> {
transformer: T,
buffer: String,
current_event: Option<SSEEvent>,
}
impl<T: SSETransformer> UnifiedSSEParser<T> {
pub fn new(transformer: T) -> Self;
pub fn process_bytes(&mut self, bytes: &[u8]) -> Result<Vec<ChatChunk>, ProviderError>;
}
String, not a byte deque. Each incoming read is decoded
independently with String::from_utf8_lossy and appended; only text up to
the last \n is processed and the incomplete tail stays buffered for the
next call. Line/event splits are retained, but a read boundary inside a
multibyte UTF-8 code point is lossy because the undecoded bytes are not
retained.process_bytes runs non-stream mode: an end marker produces nothing and
events go through transform_chunk.UnifiedSSEStream drives the private process_stream_bytes path (stream
mode): an end marker triggers transformer.finish_stream() instead, and data
goes through transform_stream_chunk.finish_stream flushes any leftover partial line and pending
event, then appends transformer.finish_stream() output.// src/core/providers/base/sse.rs
const MAX_CHUNK_BUFFER_SIZE: usize = 10_000;
pub struct UnifiedSSEStream<S, T>
where
S: Stream<Item = Result<Bytes, reqwest::Error>> + Send + Unpin,
T: SSETransformer + Clone,
{
inner: S,
parser: UnifiedSSEParser<T>,
chunk_buffer: VecDeque<ChatChunk>,
pending_error: Option<ProviderError>,
finished: bool,
}
poll_next order: pop chunk_buffer, then take pending_error, then return
None once finished, otherwise poll inner and feed bytes through
process_stream_bytes.
Pending after cx.waker().wake_by_ref().MAX_CHUNK_BUFFER_SIZE (10_000),
it yields Err(ProviderError::network(...)) instead of growing unboundedly.ProviderError::network(provider, format!("Stream error: {error}")); chunks
drained from parser.finish_stream() are emitted before the error item.finished and drains parser.finish_stream()
before returning None.Helper create_provider_sse_stream(response, provider_name) boxes
response.bytes_stream() behind an OpenAICompatibleTransformer.
server.stream_idle_timeout and fixed buffering constantsnpx skills add majiayu000/streaming-architecture下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer