starweaver-model 0.8.1

Provider-neutral model protocol and wire adapters for Starweaver
Documentation
#![allow(clippy::unwrap_used)]

use serde_json::json;

use super::AnthropicMessagesAdapter;
use crate::message::{ModelMessage, ModelResponse, ModelResponsePart, ProviderPartInfo};

fn response_with_provider_thinking(provider_name: &str) -> ModelMessage {
    ModelMessage::Response(ModelResponse {
        parts: vec![ModelResponsePart::ProviderThinking {
            text: "inspect context".to_string(),
            signature: Some("provider-signature".to_string()),
            provider: ProviderPartInfo::new(provider_name).with_id("thinking_1"),
        }],
        usage: starweaver_usage::Usage::default(),
        model_name: None,
        provider: None,
        finish_reason: None,
        timestamp: None,
        run_id: None,
        conversation_id: None,
        metadata: serde_json::Map::new(),
    })
}

#[test]
fn build_request_replays_anthropic_provider_thinking_natively() {
    let request = AnthropicMessagesAdapter::build_request(
        "claude-test",
        &[response_with_provider_thinking("anthropic")],
        None,
        &[],
    )
    .unwrap();

    let content = request["messages"][0]["content"].as_array().unwrap();
    assert_eq!(content[0]["type"], "thinking");
    assert_eq!(content[0]["thinking"], "inspect context");
    assert_eq!(content[0]["signature"], "provider-signature");
}

#[test]
fn build_request_does_not_replay_foreign_thinking_signature() {
    let request = AnthropicMessagesAdapter::build_request(
        "claude-test",
        &[response_with_provider_thinking("openai")],
        None,
        &[],
    )
    .unwrap();

    let content = request["messages"][0]["content"].as_array().unwrap();
    assert_eq!(content[0]["type"], "text");
    assert_eq!(content[0]["text"], "<think>\ninspect context\n</think>");
    assert!(content[0].get("signature").is_none());
}

#[test]
fn build_request_does_not_replay_ambiguous_thinking_signature() {
    let response = ModelMessage::Response(ModelResponse {
        parts: vec![ModelResponsePart::Thinking {
            text: "inspect context".to_string(),
            signature: Some("ambiguous-signature".to_string()),
        }],
        usage: starweaver_usage::Usage::default(),
        model_name: None,
        provider: None,
        finish_reason: None,
        timestamp: None,
        run_id: None,
        conversation_id: None,
        metadata: serde_json::Map::new(),
    });

    let request =
        AnthropicMessagesAdapter::build_request("claude-test", &[response], None, &[]).unwrap();

    let content = request["messages"][0]["content"].as_array().unwrap();
    assert_eq!(content[0]["type"], "text");
    assert_eq!(content[0]["text"], "<think>\ninspect context\n</think>");
    assert!(content[0].get("signature").is_none());
    assert!(
        !serde_json::to_string(&request)
            .unwrap()
            .contains("ambiguous-signature")
    );
}

#[test]
fn build_request_replays_anthropic_redacted_thinking_natively() {
    let response = ModelMessage::Response(ModelResponse {
        parts: vec![ModelResponsePart::ProviderOpaque {
            item_type: "redacted_thinking".to_string(),
            payload: json!({
                "type": "redacted_thinking",
                "data": "encrypted-redacted-thinking"
            }),
            provider: ProviderPartInfo::new("anthropic"),
        }],
        usage: starweaver_usage::Usage::default(),
        model_name: None,
        provider: None,
        finish_reason: None,
        timestamp: None,
        run_id: None,
        conversation_id: None,
        metadata: serde_json::Map::new(),
    });

    let request =
        AnthropicMessagesAdapter::build_request("claude-test", &[response], None, &[]).unwrap();

    let content = request["messages"][0]["content"].as_array().unwrap();
    assert_eq!(content[0]["type"], "redacted_thinking");
    assert_eq!(content[0]["data"], "encrypted-redacted-thinking");
}

#[test]
fn parse_response_preserves_anthropic_provider_thinking() {
    let response = AnthropicMessagesAdapter::parse_response(&json!({
        "id": "msg_1",
        "model": "claude-test",
        "stop_reason": "end_turn",
        "content": [{
            "type": "thinking",
            "id": "thinking_1",
            "thinking": "inspect",
            "signature": "anthropic-signature"
        }],
        "usage": {"input_tokens": 1, "output_tokens": 2}
    }))
    .unwrap();

    assert!(matches!(
        &response.parts[0],
        ModelResponsePart::ProviderThinking { text, signature, provider }
            if text == "inspect"
                && signature.as_deref() == Some("anthropic-signature")
                && provider.provider_name.as_deref() == Some("anthropic")
                && provider.id.as_deref() == Some("thinking_1")
    ));
}

#[test]
fn parse_response_preserves_anthropic_redacted_thinking() {
    let response = AnthropicMessagesAdapter::parse_response(&json!({
        "id": "msg_1",
        "model": "claude-test",
        "stop_reason": "end_turn",
        "content": [{
            "type": "redacted_thinking",
            "data": "encrypted-redacted-thinking"
        }],
        "usage": {"input_tokens": 1, "output_tokens": 2}
    }))
    .unwrap();

    assert!(matches!(
        &response.parts[0],
        ModelResponsePart::ProviderOpaque { item_type, payload, provider }
            if item_type == "redacted_thinking"
                && payload["data"] == "encrypted-redacted-thinking"
                && provider.provider_name.as_deref() == Some("anthropic")
    ));
}