stynx-code-provider 3.6.1

Anthropic API provider with SSE streaming
Documentation
use stynx_code_types::{StopReason, StreamEvent, UsageStats};
use serde_json::Value;

pub fn parse_sse_block(block: &str) -> Option<(String, String)> {
    let mut event_type = String::new();
    let mut data = String::new();
    for line in block.lines() {
        if let Some(val) = line.strip_prefix("event: ") {
            event_type = val.to_string();
        } else if let Some(val) = line.strip_prefix("data: ") {
            data = val.to_string();
        }
    }
    if data.is_empty() { None } else { Some((event_type, data)) }
}

pub fn parse_sse_event(event_type: &str, data: &str) -> Vec<StreamEvent> {
    let v: Value = match serde_json::from_str(data) {
        Ok(v) => v,
        Err(_) => return vec![],
    };

    match event_type {
        "message_start" => {
            if let Some(usage) = v.get("message").and_then(|m| m.get("usage")) {
                let input_tokens = usage
                    .get("input_tokens")
                    .and_then(|t| t.as_u64())
                    .unwrap_or(0);
                vec![StreamEvent::Usage {
                    stats: UsageStats {
                        input_tokens,
                        output_tokens: 0,
                    },
                }]
            } else {
                vec![]
            }
        }

        "content_block_start" => {
            let Some(cb) = v.get("content_block") else {
                return vec![];
            };
            match cb.get("type").and_then(|t| t.as_str()) {
                Some("tool_use") => {
                    let id = cb.get("id").and_then(|v| v.as_str()).unwrap_or_default();
                    let name = cb.get("name").and_then(|v| v.as_str()).unwrap_or_default();
                    vec![StreamEvent::ToolUseStart {
                        id: id.to_string(),
                        name: name.to_string(),
                    }]
                }
                Some("text") => {
                    let text = cb.get("text").and_then(|v| v.as_str()).unwrap_or_default();
                    if text.is_empty() {
                        vec![]
                    } else {
                        vec![StreamEvent::ContentDelta {
                            text: text.to_string(),
                        }]
                    }
                }
                Some("thinking") => {

                    vec![]
                }
                _ => vec![],
            }
        }

        "content_block_delta" => {
            let Some(delta) = v.get("delta") else {
                return vec![];
            };
            match delta.get("type").and_then(|t| t.as_str()) {
                Some("text_delta") => {
                    if let Some(text) = delta.get("text").and_then(|t| t.as_str()) {
                        vec![StreamEvent::ContentDelta {
                            text: text.to_string(),
                        }]
                    } else {
                        vec![]
                    }
                }
                Some("input_json_delta") => {
                    if let Some(json) = delta.get("partial_json").and_then(|j| j.as_str()) {
                        vec![StreamEvent::ToolUseDelta {
                            json_chunk: json.to_string(),
                        }]
                    } else {
                        vec![]
                    }
                }
                Some("thinking_delta") => {
                    if let Some(text) = delta.get("thinking").and_then(|t| t.as_str()) {
                        vec![StreamEvent::ThinkingDelta {
                            text: text.to_string(),
                        }]
                    } else {
                        vec![]
                    }
                }
                _ => vec![],
            }
        }

        "content_block_stop" => vec![],

        "message_delta" => {
            let mut events = Vec::new();

            let output_tokens = v
                .get("usage")
                .and_then(|u| u.get("output_tokens"))
                .and_then(|t| t.as_u64())
                .unwrap_or(0);
            if output_tokens > 0 {
                events.push(StreamEvent::Usage {
                    stats: UsageStats {
                        input_tokens: 0,
                        output_tokens,
                    },
                });
            }

            if let Some(reason_str) = v
                .get("delta")
                .and_then(|d| d.get("stop_reason"))
                .and_then(|r| r.as_str())
            {
                let reason = match reason_str {
                    "end_turn" => StopReason::EndTurn,
                    "tool_use" => StopReason::ToolUse,
                    "max_tokens" => StopReason::MaxTokens,
                    _ => StopReason::EndTurn,
                };
                events.push(StreamEvent::Stop { reason });
            }

            events
        }

        "error" => {
            let msg = v
                .get("error")
                .and_then(|e| e.get("message"))
                .and_then(|m| m.as_str())
                .unwrap_or("unknown API error");
            vec![StreamEvent::Error {
                message: msg.to_string(),
            }]
        }

        _ => vec![],
    }
}

pub fn parse_sse_lines(text: &str) -> Vec<(String, String)> {
    let mut events = Vec::new();
    let mut current_event = String::new();
    let mut current_data = String::new();

    for line in text.lines() {
        if line.is_empty() {
            if !current_data.is_empty() {
                events.push((current_event.clone(), current_data.clone()));
                current_event.clear();
                current_data.clear();
            }
            continue;
        }
        if let Some(val) = line.strip_prefix("event: ") {
            current_event = val.to_string();
        } else if let Some(val) = line.strip_prefix("data: ") {
            current_data = val.to_string();
        }
    }

    if !current_data.is_empty() {
        events.push((current_event, current_data));
    }

    events
}