starweaver-model 0.0.5

Provider-neutral model protocol and wire adapters for Starweaver
Documentation
//! Anthropic settings and cache-control mapping.

use serde_json::{json, Value};

use crate::{
    adapter::ToolDefinition,
    providers::{insert_nonempty_description, provider_tool_schema_without_meta},
    ModelSettings,
};

pub(super) fn apply_anthropic_settings(
    request: &mut serde_json::Map<String, Value>,
    settings: Option<&ModelSettings>,
) {
    let Some(settings) = settings else {
        return;
    };
    if let Some(thinking) = &settings.thinking {
        let thinking_mode = thinking.mode.as_deref().unwrap_or("enabled");
        let mut payload = serde_json::Map::new();
        payload.insert("type".to_string(), json!(thinking_mode));
        if thinking_mode == "enabled" {
            payload.insert(
                "budget_tokens".to_string(),
                json!(thinking.budget_tokens.unwrap_or(1024)),
            );
        }
        if thinking_mode == "adaptive" {
            payload.insert("display".to_string(), json!("summarized"));
            if !thinking.effort.is_empty() {
                let output_config = request
                    .entry("output_config".to_string())
                    .or_insert_with(|| Value::Object(serde_json::Map::new()));
                if let Some(output_config) = output_config.as_object_mut() {
                    output_config.insert("effort".to_string(), json!(thinking.effort));
                }
            }
        }
        request.insert("thinking".to_string(), Value::Object(payload));
    }
    if let Some(temperature) = settings.temperature {
        request.insert("temperature".to_string(), json!(temperature));
    }
    if let Some(top_p) = settings.top_p {
        request.insert("top_p".to_string(), json!(top_p));
    }
    if let Some(top_k) = settings.top_k {
        request.insert("top_k".to_string(), json!(top_k));
    }
    if !settings.stop_sequences.is_empty() {
        request.insert("stop_sequences".to_string(), json!(settings.stop_sequences));
    }
    if let Some(anthropic) = &settings.provider_settings.anthropic {
        if let Some(metadata) = &anthropic.metadata {
            request.insert("metadata".to_string(), metadata.clone());
        }
        if let Some(context_management) = &anthropic.context_management {
            request.insert("context_management".to_string(), context_management.clone());
        }
        if let Some(container) = &anthropic.container {
            request.insert("container".to_string(), json!(container));
        }
        if let Some(service_tier) = &anthropic.service_tier {
            request.insert("service_tier".to_string(), json!(service_tier));
        }
    }
    if let Some(tool_choice) = settings.tool_choice.as_ref() {
        let mut choice = anthropic_tool_choice(tool_choice);
        if settings.parallel_tool_calls == Some(false)
            && !matches!(tool_choice, crate::settings::ToolChoice::None)
        {
            if let Some(choice) = choice.as_object_mut() {
                choice.insert("disable_parallel_tool_use".to_string(), json!(true));
            }
        }
        request.insert("tool_choice".to_string(), choice);
    } else if settings.parallel_tool_calls == Some(false) {
        request.insert(
            "tool_choice".to_string(),
            json!({"type": "auto", "disable_parallel_tool_use": true}),
        );
    }
    if let Some(options) = settings
        .provider_options
        .as_ref()
        .and_then(Value::as_object)
    {
        request.extend(
            options
                .iter()
                .filter(|(key, _)| !is_internal_anthropic_option(key))
                .map(|(key, value)| (key.clone(), value.clone())),
        );
    }
}

pub(super) fn append_anthropic_tools(
    request: &mut serde_json::Map<String, Value>,
    tools: &[ToolDefinition],
    settings: Option<&ModelSettings>,
) {
    if tools.is_empty() {
        return;
    }
    let mut definitions = tools
        .iter()
        .map(|tool| {
            let mut definition = serde_json::Map::new();
            definition.insert("name".to_string(), json!(tool.name));
            insert_nonempty_description(&mut definition, tool.description.as_ref());
            definition.insert(
                "input_schema".to_string(),
                provider_tool_schema_without_meta(&tool.parameters),
            );
            Value::Object(definition)
        })
        .collect::<Vec<_>>();
    if let Some(ttl) = anthropic_cache_ttl(settings, "anthropic_cache_tool_definitions") {
        if let Some(last) = definitions.last_mut() {
            last["cache_control"] = anthropic_cache_control(ttl);
        }
    }
    request.insert("tools".to_string(), Value::Array(definitions));
}

fn anthropic_tool_choice(choice: &crate::settings::ToolChoice) -> Value {
    match choice {
        crate::settings::ToolChoice::Auto | crate::settings::ToolChoice::ToolOrOutput { .. } => {
            json!({"type": "auto"})
        }
        crate::settings::ToolChoice::None => json!({"type": "none"}),
        crate::settings::ToolChoice::Required | crate::settings::ToolChoice::Tools { .. } => {
            json!({"type": "any"})
        }
        crate::settings::ToolChoice::Tool { name } => json!({"type": "tool", "name": name}),
    }
}

pub(super) fn anthropic_cache_ttl(
    settings: Option<&ModelSettings>,
    key: &str,
) -> Option<&'static str> {
    let value = settings
        .and_then(|settings| settings.provider_options.as_ref())
        .and_then(|options| options.get(key));
    match value {
        Some(Value::Bool(true)) => Some("5m"),
        Some(Value::String(value)) if value == "5m" => Some("5m"),
        Some(Value::String(value)) if value == "1h" => Some("1h"),
        _ => None,
    }
}

pub(super) fn anthropic_cache_control(ttl: &str) -> Value {
    json!({"type": "ephemeral", "ttl": ttl})
}

fn is_internal_anthropic_option(key: &str) -> bool {
    matches!(
        key,
        "anthropic_cache"
            | "anthropic_cache_instructions"
            | "anthropic_cache_tool_definitions"
            | "anthropic_cache_response"
            | "anthropic_cache_messages"
            | "anthropic_effort"
    )
}