feat(runtime): add streaming support to LlmDriver trait

- Add StreamChunk and StreamEvent types for Tauri event emission
- Add stream() method to LlmDriver trait with async-stream
- Implement Anthropic streaming with SSE parsing
- Implement OpenAI streaming with SSE parsing
- Add placeholder stream() for Gemini and Local drivers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-24 01:44:40 +08:00
parent 4ba0a531aa
commit 820e3a1ffe
8 changed files with 409 additions and 13 deletions

View File

@@ -1,11 +1,58 @@
//! Streaming utilities
//! Streaming response types
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use zclaw_types::Result;
/// Stream event for LLM responses
/// Stream chunk emitted during streaming
/// This is the serializable type sent via Tauri events
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamChunk {
/// Text delta
TextDelta { delta: String },
/// Thinking delta (for extended thinking models)
ThinkingDelta { delta: String },
/// Tool use started
ToolUseStart { id: String, name: String },
/// Tool use input delta
ToolUseDelta { id: String, delta: String },
/// Tool use completed
ToolUseEnd { id: String, input: serde_json::Value },
/// Stream completed
Complete {
input_tokens: u32,
output_tokens: u32,
stop_reason: String,
},
/// Error occurred
Error { message: String },
}
/// Streaming event for Tauri emission
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
/// Session ID for routing
pub session_id: String,
/// Agent ID for routing
pub agent_id: String,
/// The chunk content
pub chunk: StreamChunk,
}
impl StreamEvent {
pub fn new(session_id: impl Into<String>, agent_id: impl Into<String>, chunk: StreamChunk) -> Self {
Self {
session_id: session_id.into(),
agent_id: agent_id.into(),
chunk,
}
}
}
/// Legacy stream event for internal use with mpsc channels
#[derive(Debug, Clone)]
pub enum StreamEvent {
pub enum InternalStreamEvent {
/// Text delta received
TextDelta(String),
/// Thinking delta received
@@ -24,31 +71,31 @@ pub enum StreamEvent {
/// Stream sender wrapper
pub struct StreamSender {
tx: mpsc::Sender<StreamEvent>,
tx: mpsc::Sender<InternalStreamEvent>,
}
impl StreamSender {
pub fn new(tx: mpsc::Sender<StreamEvent>) -> Self {
pub fn new(tx: mpsc::Sender<InternalStreamEvent>) -> Self {
Self { tx }
}
pub async fn send_text(&self, delta: impl Into<String>) -> Result<()> {
self.tx.send(StreamEvent::TextDelta(delta.into())).await.ok();
self.tx.send(InternalStreamEvent::TextDelta(delta.into())).await.ok();
Ok(())
}
pub async fn send_thinking(&self, delta: impl Into<String>) -> Result<()> {
self.tx.send(StreamEvent::ThinkingDelta(delta.into())).await.ok();
self.tx.send(InternalStreamEvent::ThinkingDelta(delta.into())).await.ok();
Ok(())
}
pub async fn send_complete(&self, input_tokens: u32, output_tokens: u32) -> Result<()> {
self.tx.send(StreamEvent::Complete { input_tokens, output_tokens }).await.ok();
self.tx.send(InternalStreamEvent::Complete { input_tokens, output_tokens }).await.ok();
Ok(())
}
pub async fn send_error(&self, error: impl Into<String>) -> Result<()> {
self.tx.send(StreamEvent::Error(error.into())).await.ok();
self.tx.send(InternalStreamEvent::Error(error.into())).await.ok();
Ok(())
}
}