llmoxide 0.1.0

Provider-agnostic Rust SDK for OpenAI, Anthropic, Gemini, and Ollama (streaming + tools)
Documentation
use crate::types::{Event, ToolCall};

fn collect_openai_tool_events(frames: &[(&str, &str)]) -> Vec<Event> {
    // Minimal reproduction of the OpenAI stream loop's tool-call assembly behavior.
    use std::collections::HashMap;
    let mut tool_calls: HashMap<String, (Option<String>, String)> = HashMap::new();
    let mut out = Vec::new();

    for (event, data) in frames {
        match *event {
            "response.output_item.added" => {
                let v: serde_json::Value = serde_json::from_str(data).unwrap();
                let item = v.get("item").unwrap();
                if item.get("type").and_then(|x| x.as_str()) == Some("function_call") {
                    let id = item.get("id").and_then(|x| x.as_str()).unwrap();
                    let name = item
                        .get("name")
                        .and_then(|x| x.as_str())
                        .map(|s| s.to_string());
                    let args = item
                        .get("arguments")
                        .and_then(|x| x.as_str())
                        .unwrap_or("")
                        .to_string();
                    tool_calls.insert(id.to_string(), (name, args));
                }
            }
            "response.function_call_arguments.delta" => {
                let v: serde_json::Value = serde_json::from_str(data).unwrap();
                let call_id = v.get("call_id").and_then(|x| x.as_str()).unwrap();
                let delta = v.get("delta").and_then(|x| x.as_str()).unwrap();
                let entry = tool_calls
                    .entry(call_id.to_string())
                    .or_insert((None, String::new()));
                entry.1.push_str(delta);
            }
            "response.function_call_arguments.done" => {
                let v: serde_json::Value = serde_json::from_str(data).unwrap();
                let call_id = v.get("call_id").and_then(|x| x.as_str()).unwrap();
                if let Some((name_opt, raw_args)) = tool_calls.remove(call_id) {
                    let name = name_opt.unwrap_or_else(|| "tool".to_string());
                    let args: serde_json::Value = serde_json::from_str(&raw_args).unwrap();
                    out.push(Event::ToolCall(ToolCall {
                        id: Some(call_id.to_string()),
                        name,
                        arguments: args,
                    }));
                }
            }
            _ => {}
        }
    }

    out
}

#[test]
fn openai_stream_assembles_tool_call_args() {
    let frames = vec![
        (
            "response.output_item.added",
            r#"{"item":{"type":"function_call","id":"call_1","name":"get_weather","arguments":""}}"#,
        ),
        (
            "response.function_call_arguments.delta",
            r#"{"call_id":"call_1","delta":"{\"city\":\"Ber"}"#,
        ),
        (
            "response.function_call_arguments.delta",
            r#"{"call_id":"call_1","delta":"lin\",\"units\":\"c\"}"}"#,
        ),
        (
            "response.function_call_arguments.done",
            r#"{"call_id":"call_1"}"#,
        ),
    ];

    let events = collect_openai_tool_events(&frames);
    assert_eq!(events.len(), 1);
    let Event::ToolCall(call) = &events[0] else {
        panic!("expected ToolCall event");
    };
    assert_eq!(call.id.as_deref(), Some("call_1"));
    assert_eq!(call.name, "get_weather");
    assert_eq!(call.arguments["city"].as_str(), Some("Berlin"));
    assert_eq!(call.arguments["units"].as_str(), Some("c"));
}