starweaver-model 0.10.0

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

use serde_json::{Map, Value, json};
use starweaver_model::{
    NativeToolDefinition, ToolDefinition,
    providers::{
        anthropic::AnthropicMessagesAdapter, bedrock::BedrockConverseAdapter,
        gemini::GeminiGenerateContentAdapter, openai_chat::OpenAiChatAdapter,
        openai_responses::OpenAiResponsesAdapter,
    },
};

#[test]
fn openai_responses_maps_native_tool_kinds() {
    let native_tools = [
        native_tool(
            "web_search_preview",
            [("search_context_size", json!("medium"))],
        ),
        native_tool("code_interpreter", []),
        native_tool("image_generation", [("quality", json!("high"))]),
        native_tool("file_search", [("vector_store_ids", json!(["vs_123"]))]),
        native_tool("web_fetch", [("max_uses", json!(2))]),
        native_tool("memory", [("container", json!("default"))]),
    ];

    let request =
        OpenAiResponsesAdapter::build_request("gpt-4.1-mini", &[], None, &[], &native_tools)
            .unwrap();

    assert_tool(&request, 0, "web_search_preview");
    assert_eq!(request["tools"][0]["search_context_size"], "medium");
    assert_tool(&request, 1, "code_interpreter");
    assert_tool(&request, 2, "image_generation");
    assert_eq!(request["tools"][2]["quality"], "high");
    assert_tool(&request, 3, "file_search");
    assert_eq!(request["tools"][3]["vector_store_ids"][0], "vs_123");
    assert_tool(&request, 4, "web_fetch");
    assert_eq!(request["tools"][4]["max_uses"], 2);
    assert_tool(&request, 5, "memory");
    assert_eq!(request["tools"][5]["container"], "default");
}

#[test]
fn provider_function_tool_schemas_are_lowered_for_wire_requests() {
    let tool = nested_nullable_tool();
    let chat =
        OpenAiChatAdapter::build_request("gpt-4o-mini", &[], None, std::slice::from_ref(&tool))
            .unwrap();
    let responses = OpenAiResponsesAdapter::build_request(
        "gpt-4.1-mini",
        &[],
        None,
        std::slice::from_ref(&tool),
        &[],
    )
    .unwrap();
    let anthropic = AnthropicMessagesAdapter::build_request(
        "claude-3-5-sonnet",
        &[],
        None,
        std::slice::from_ref(&tool),
    )
    .unwrap();
    let gemini =
        GeminiGenerateContentAdapter::build_request(&[], None, std::slice::from_ref(&tool))
            .unwrap();
    let bedrock = BedrockConverseAdapter::build_request(
        "anthropic.claude",
        &[],
        None,
        std::slice::from_ref(&tool),
    )
    .unwrap();

    assert_lowered_schema(&chat["tools"][0]["function"]["parameters"]);
    assert_lowered_schema(&responses["tools"][0]["parameters"]);
    assert_lowered_schema(&anthropic["tools"][0]["input_schema"]);
    assert_lowered_schema(&gemini["tools"][0]["functionDeclarations"][0]["parameters"]);
    assert_lowered_schema(&bedrock["toolConfig"]["tools"][0]["toolSpec"]["inputSchema"]["json"]);
    assert_eq!(chat["tools"][0]["function"].get("description"), None);
    assert_eq!(responses["tools"][0].get("description"), None);
    assert_eq!(anthropic["tools"][0].get("description"), None);
    assert_eq!(
        gemini["tools"][0]["functionDeclarations"][0].get("description"),
        None
    );
    assert_eq!(
        bedrock["toolConfig"]["tools"][0]["toolSpec"].get("description"),
        None
    );
}

#[test]
fn gemini_maps_native_google_search_code_execution_and_generic_tools() {
    let native_tools = [
        NativeToolDefinition::new("google_search"),
        NativeToolDefinition::new("code_execution"),
        native_tool("url_context", [("maxUses", json!(1))]),
    ];

    let request = GeminiGenerateContentAdapter::build_request_with_native_tools(
        &[],
        None,
        &[],
        &native_tools,
    )
    .unwrap();

    assert!(
        request["tools"].as_array().unwrap()[0]
            .as_object()
            .unwrap()
            .contains_key("googleSearch")
    );
    assert!(
        request["tools"].as_array().unwrap()[1]
            .as_object()
            .unwrap()
            .contains_key("codeExecution")
    );
    assert_eq!(request["tools"][2]["url_context"]["maxUses"], 1);
}

fn nested_nullable_tool() -> ToolDefinition {
    ToolDefinition {
        name: "batch".to_string(),
        description: None,
        parameters: json!({
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "type": "object",
            "properties": {
                "items": {
                    "type": ["array", "null"],
                    "items": {"$ref": "#/$defs/Item"}
                }
            },
            "$defs": {
                "Item": {
                    "type": "object",
                    "properties": {
                        "name": {"type": ["string", "null"]}
                    }
                }
            }
        }),
        return_schema: None,
        strict: None,
        sequential: None,
        metadata: Map::new(),
    }
}

fn assert_lowered_schema(schema: &Value) {
    assert_eq!(schema.get("$schema"), None);
    assert_eq!(
        schema["properties"]["items"]["type"],
        json!(["array", "null"])
    );
    assert_eq!(
        schema["$defs"]["Item"]["properties"]["name"]["type"],
        json!(["string", "null"])
    );
}

fn native_tool<const N: usize>(
    tool_type: &str,
    entries: [(&str, Value); N],
) -> NativeToolDefinition {
    NativeToolDefinition::new(tool_type).with_config(config(entries))
}

fn config<const N: usize>(entries: [(&str, Value); N]) -> Map<String, Value> {
    entries
        .into_iter()
        .map(|(key, value)| (key.to_string(), value))
        .collect()
}

fn assert_tool(request: &Value, index: usize, tool_type: &str) {
    assert_eq!(request["tools"][index]["type"], tool_type);
}