claude-rust-provider 2.0.0

Anthropic API provider with SSE streaming
Documentation
use claude_rust_auth::Credential;
use claude_rust_types::{ContentBlock, Conversation, Role};
use serde_json::{Value, json};

use super::anthropic_provider::BILLING_HEADER_LINE;

pub fn build_request_body(
    credential: &Credential,
    model: &str,
    conversation: &Conversation,
    tools: &[Value],
    thinking: bool,
    max_tokens: u32,
) -> Value {
    let total = conversation.messages.len();
    let messages: Vec<Value> = conversation.messages.iter().enumerate().map(|(mi, msg)| {
        let last_mi = total.saturating_sub(2);
        let last_bi = msg.content.len().saturating_sub(1);
        let content: Vec<Value> = msg.content.iter().enumerate().filter_map(|(bi, block)| {
            let cache = matches!(msg.role, Role::User) && mi == last_mi && bi == last_bi && total >= 3;
            match block {
                ContentBlock::Text { text } => {
                    let mut v = json!({"type": "text", "text": text});
                    if cache { v["cache_control"] = json!({"type": "ephemeral"}); }
                    Some(v)
                }
                ContentBlock::ToolUse { id, name, input } => Some(json!({
                    "type": "tool_use", "id": id, "name": name, "input": input,
                })),
                ContentBlock::ToolResult { tool_use_id, content, is_error } => {
                    const MAX_RESULT: usize = 8_000;
                    let content = if content.len() > MAX_RESULT {
                        let boundary = content.char_indices()
                            .map(|(i, _)| i)
                            .take_while(|&i| i < MAX_RESULT)
                            .last()
                            .unwrap_or(0);
                        format!("{}\n[truncated {} chars]", &content[..boundary], content.len() - boundary)
                    } else {
                        content.clone()
                    };
                    let mut v = json!({
                        "type": "tool_result",
                        "tool_use_id": tool_use_id,
                        "content": content,
                    });
                    if let Some(true) = is_error { v["is_error"] = json!(true); }
                    if cache { v["cache_control"] = json!({"type": "ephemeral"}); }
                    Some(v)
                }
                ContentBlock::Image { media_type, data } => Some(json!({
                    "type": "image",
                    "source": {"type": "base64", "media_type": media_type, "data": data}
                })),
                ContentBlock::Thinking { .. } => None,
            }
        }).collect();
        json!({"role": serde_json::to_value(&msg.role).unwrap_or(json!("user")), "content": content})
    }).collect();

    let messages = sanitize_messages(messages);

    let mut body = json!({
        "model": model,
        "max_tokens": max_tokens,
        "messages": messages,
        "stream": true,
    });

    if credential.is_oauth() {
        let mut sys: Vec<Value> = vec![json!({"type": "text", "text": BILLING_HEADER_LINE})];
        if let Some(s) = &conversation.system {
            sys.push(json!({"type": "text", "text": s}));
        }
        if let Some(last) = sys.last_mut() {
            last["cache_control"] = json!({"type": "ephemeral"});
        }
        body["system"] = json!(sys);
    } else if let Some(s) = &conversation.system {
        body["system"] = json!([{"type": "text", "text": s, "cache_control": {"type": "ephemeral"}}]);
    }

    if thinking {
        let budget = 5000u32;
        if max_tokens <= budget { body["max_tokens"] = json!(budget + 4096); }
        body["thinking"] = json!({"type": "enabled", "budget_tokens": budget});
    }

    if !tools.is_empty() {
        let mut tools_arr = tools.to_vec();
        if let Some(last) = tools_arr.last_mut() {
            last["cache_control"] = json!({"type": "ephemeral"});
        }
        body["tools"] = json!(tools_arr);
    }

    body
}

fn sanitize_messages(mut messages: Vec<Value>) -> Vec<Value> {
    let mut i = 0;
    while i < messages.len() {
        if messages[i].get("role").and_then(|r| r.as_str()) != Some("assistant") {
            i += 1;
            continue;
        }
        let tool_use_ids: Vec<String> = messages[i]["content"]
            .as_array()
            .map(|arr| arr.iter()
                .filter_map(|b| if b["type"] == "tool_use" {
                    b["id"].as_str().map(String::from)
                } else { None })
                .collect())
            .unwrap_or_default();
        if tool_use_ids.is_empty() { i += 1; continue; }

        let next_result_ids: Vec<String> = messages.get(i + 1)
            .and_then(|m| m["content"].as_array())
            .map(|arr| arr.iter()
                .filter_map(|b| if b["type"] == "tool_result" {
                    b["tool_use_id"].as_str().map(String::from)
                } else { None })
                .collect())
            .unwrap_or_default();

        let orphaned: Vec<&str> = tool_use_ids.iter()
            .filter(|id| !next_result_ids.contains(id))
            .map(String::as_str)
            .collect();

        if orphaned.is_empty() { i += 1; continue; }

        if let Some(arr) = messages[i]["content"].as_array_mut() {
            arr.retain(|b| !(b["type"] == "tool_use"
                && b["id"].as_str().map(|id| orphaned.contains(&id)).unwrap_or(false)));
        }

        if messages[i]["content"].as_array().map(|a| a.is_empty()).unwrap_or(true) {
            messages.remove(i);
        } else {
            i += 1;
        }
    }
    messages
}