canact 0.1.1

Probe an LLM and return host policy: max tools, edit format, XML fallback, JSON repair
Documentation
//! Overlay exporters must only emit keys the host will accept.

use canact::{
    CapabilityLevel, CapabilityProfile, ClineModelInfo, HostOverlay, ProbeResult,
    overlay_model_name,
};

fn sample(
    search: CapabilityLevel,
    unified: CapabilityLevel,
    vision: CapabilityLevel,
) -> CapabilityProfile {
    let pr = |name: &str, level: CapabilityLevel| ProbeResult {
        name: name.to_owned(),
        score: match level {
            CapabilityLevel::Strong => 1.0,
            CapabilityLevel::Medium => 0.5,
            CapabilityLevel::Weak => 0.1,
        },
        max_score: 1.0,
        level,
        details: "test".to_owned(),
    };
    CapabilityProfile {
        model_id: "qwen2.5-coder".to_owned(),
        provider: "ollama".to_owned(),
        tool_calling: pr("tool_calling", CapabilityLevel::Strong),
        json_output: pr("json_output", CapabilityLevel::Strong),
        instruction_following: pr("instruction_following", CapabilityLevel::Strong),
        search_replace: pr("search_replace", search),
        unified_diff: pr("unified_diff", unified),
        xml_tool_calling: pr("xml_tool_calling", CapabilityLevel::Medium),
        complex_tool_calling: pr("complex_tool_calling", CapabilityLevel::Strong),
        nested_arguments: pr("nested_arguments", CapabilityLevel::Strong),
        vision: pr("vision", vision),
        tool_selection: pr("tool_selection", CapabilityLevel::Medium),
        streaming_tool_calls: pr("streaming_tool_calls", CapabilityLevel::Strong),
        one_shot_tool_plan: pr("one_shot_tool_plan", CapabilityLevel::Strong),
        multi_turn_task_sequencing: pr("multi_turn_task_sequencing", CapabilityLevel::Strong),
        context_faithfulness: pr("context_faithfulness", CapabilityLevel::Strong),
        code_syntax: pr("code_syntax", CapabilityLevel::Strong),
        max_tokens_compliance: pr("max_tokens_compliance", CapabilityLevel::Strong),
        multi_turn_memory: pr("multi_turn_memory", CapabilityLevel::Strong),
        system_message_adherence: pr("system_message_adherence", CapabilityLevel::Strong),
        token_efficiency: pr("token_efficiency", CapabilityLevel::Strong),
        parallel_tool_scale: pr("parallel_tool_scale", CapabilityLevel::Strong),
        probed_at: 1_700_000_000,
        effective_context_tokens: Some(8192),
        probed_context_floor: Some(8192),
    }
}

#[test]
fn host_overlay_write_aider_pair() {
    let dir = tempfile::tempdir().expect("temp dir");
    let overlay = HostOverlay::aider(
        &sample(
            CapabilityLevel::Strong,
            CapabilityLevel::Medium,
            CapabilityLevel::Weak,
        ),
        Some(40960),
    );
    let paths = overlay.write_to(dir.path()).expect("write");
    assert_eq!(paths.len(), 2);
    let settings =
        std::fs::read_to_string(dir.path().join(".aider.model.settings.yml")).expect("settings");
    let metadata =
        std::fs::read_to_string(dir.path().join(".aider.model.metadata.json")).expect("metadata");
    assert!(settings.contains("edit_format: diff"), "{settings}");
    let value: serde_json::Value = serde_json::from_str(&metadata).expect("json");
    assert_eq!(
        value["ollama/qwen2.5-coder"]["max_input_tokens"], 8192,
        "{value}"
    );
}

#[test]
fn host_overlay_write_cline_json() {
    let dir = tempfile::tempdir().expect("temp dir");
    let overlay = HostOverlay::cline(
        &sample(
            CapabilityLevel::Weak,
            CapabilityLevel::Weak,
            CapabilityLevel::Strong,
        ),
        None,
    );
    overlay.write_to(dir.path()).expect("write");
    let body = std::fs::read_to_string(dir.path().join("cline.modelinfo.json")).expect("json");
    let info: ClineModelInfo = serde_json::from_str(&body).expect("parse");
    assert_eq!(info.context_window, Some(8192));
    assert!(info.supports_images);
}

#[test]
fn overlay_model_name_stable() {
    let p = sample(
        CapabilityLevel::Strong,
        CapabilityLevel::Medium,
        CapabilityLevel::Weak,
    );
    assert_eq!(overlay_model_name(&p), "ollama/qwen2.5-coder");
}

#[test]
fn overlay_localhost_port_still_maps_to_ollama() {
    let mut p = sample(
        CapabilityLevel::Strong,
        CapabilityLevel::Medium,
        CapabilityLevel::Weak,
    );
    p.provider = "127.0.0.1:1234".into();
    assert_eq!(
        overlay_model_name(&p),
        "ollama/qwen2.5-coder",
        "export still maps loopback host:port to ollama"
    );
    p.provider = "localhost:11434".into();
    assert_eq!(overlay_model_name(&p), "ollama/qwen2.5-coder");
}

#[test]
fn overlay_xai_host_maps_to_xai_family() {
    let mut p = sample(
        CapabilityLevel::Strong,
        CapabilityLevel::Medium,
        CapabilityLevel::Weak,
    );
    p.model_id = "grok-4".into();
    p.provider = "api.x.ai".into();
    assert_eq!(overlay_model_name(&p), "xai/grok-4");
    p.provider = "grok".into();
    assert_eq!(overlay_model_name(&p), "xai/grok-4");
}

#[test]
fn overlay_anthropic_host_maps_to_anthropic_family() {
    let mut p = sample(
        CapabilityLevel::Strong,
        CapabilityLevel::Medium,
        CapabilityLevel::Weak,
    );
    p.model_id = "claude-haiku-4-5-20251001".into();
    p.provider = "api.anthropic.com".into();
    assert_eq!(
        overlay_model_name(&p),
        "anthropic/claude-haiku-4-5-20251001"
    );
    p.provider = "claude".into();
    assert_eq!(
        overlay_model_name(&p),
        "anthropic/claude-haiku-4-5-20251001"
    );
}