vtcode-llm 0.181.2

LLM provider abstraction, client implementations, and streaming for VT Code
Documentation
use crate::provider::FinishReason;
use crate::providers::anthropic::response_parser::*;
use serde_json::json;

#[test]
fn test_parse_finish_reason() {
    assert!(matches!(parse_finish_reason("end_turn"), FinishReason::Stop));
    assert!(matches!(parse_finish_reason("max_tokens"), FinishReason::Length));
    assert!(matches!(parse_finish_reason("tool_use"), FinishReason::ToolCalls));
    assert!(matches!(parse_finish_reason("compaction"), FinishReason::Pause));
    assert!(matches!(parse_finish_reason("pause_turn"), FinishReason::Pause));
    assert!(matches!(parse_finish_reason("refusal"), FinishReason::Refusal));
    assert!(matches!(parse_finish_reason("model_context_window_exceeded"), FinishReason::Length));
}

#[test]
fn test_parse_response_basic() {
    let response_json = json!({
        "content": [
            {"type": "text", "text": "Hello, world!"}
        ],
        "stop_reason": "end_turn",
        "usage": {
            "input_tokens": 10,
            "output_tokens": 5
        }
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    assert_eq!(response.content.as_deref(), Some("Hello, world!"));
    assert!(matches!(response.finish_reason, FinishReason::Stop));
}

#[test]
fn test_parse_response_carries_refusal_stop_details() {
    let response_json = json!({
        "content": [],
        "stop_reason": "refusal",
        "stop_details": {
            "type": "refusal",
            "category": "cyber",
            "explanation": "declined",
            "fallback_credit_token": "credit-1",
            "fallback_has_prefill_claim": false
        }
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    assert!(matches!(response.finish_reason, FinishReason::Refusal));
    let details = response.reasoning_details.expect("stop_details detail");
    assert_eq!(details.len(), 1);
    let detail: serde_json::Value = serde_json::from_str(&details[0]).expect("serialized stop_details");
    assert_eq!(detail["type"], "stop_details");
    assert_eq!(detail["category"], "cyber");
    assert_eq!(detail["explanation"], "declined");
    assert_eq!(detail["fallback_credit_token"], "credit-1");
    assert_eq!(detail["fallback_has_prefill_claim"], false);
}

#[test]
fn test_parse_response_ignores_null_stop_details() {
    let response_json = json!({
        "content": [{"type": "text", "text": "done"}],
        "stop_reason": "end_turn",
        "stop_details": null
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    assert!(response.reasoning_details.is_none(), "details: {:?}", response.reasoning_details);
}

#[test]
fn test_parse_response_with_compaction() {
    let response_json = json!({
        "content": [
            {
                "type": "compaction",
                "content": "opaque summary",
                "signature": "signed-summary",
                "encrypted_content": "opaque-extension"
            }
        ],
        "stop_reason": "compaction"
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    assert!(matches!(response.finish_reason, FinishReason::Pause));
    assert_eq!(response.compaction.as_deref(), Some("opaque summary"));
    let details = response.reasoning_details.expect("compaction detail");
    assert_eq!(details.len(), 1);
    let detail: serde_json::Value = serde_json::from_str(&details[0]).expect("serialized compaction detail");
    assert_eq!(detail["signature"], "signed-summary");
    assert_eq!(detail["encrypted_content"], "opaque-extension");
}

#[test]
fn test_parse_response_keeps_compaction_block_when_content_is_null() {
    let response = parse_response(
        json!({
            "content": [{"type": "compaction", "content": null, "signature": "signed-summary"}],
            "stop_reason": "compaction"
        }),
        "claude-sonnet-5".to_string(),
    )
    .expect("parse response");

    assert!(response.compaction.is_none());
    let details = response.reasoning_details.expect("opaque compaction detail");
    assert_eq!(serde_json::from_str::<serde_json::Value>(&details[0]).expect("detail")["content"], json!(null));
}

#[test]
fn test_parse_response_with_thinking() {
    let response_json = json!({
        "content": [
            {"type": "thinking", "thinking": "Let me think...", "signature": "sig123"},
            {"type": "text", "text": "The answer is 42."}
        ],
        "stop_reason": "end_turn",
        "usage": {
            "input_tokens": 10,
            "output_tokens": 20
        }
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    let reasoning = response.reasoning.as_deref().expect("expected reasoning content");
    assert!(reasoning.contains("Let me think"));
    assert_eq!(
        response.reasoning_details,
        Some(vec![
            json!({
                "type": "thinking",
                "thinking": "Let me think...",
                "signature": "sig123"
            })
            .to_string()
        ])
    );
    assert_eq!(response.content.as_deref(), Some("The answer is 42."));
}

#[test]
fn test_parse_response_with_tool_use() {
    let response_json = json!({
        "content": [
            {
                "type": "tool_use",
                "id": "tool_123",
                "name": "get_weather",
                "input": {"location": "NYC"}
            }
        ],
        "stop_reason": "tool_use",
        "usage": {
            "input_tokens": 10,
            "output_tokens": 5
        }
    });

    let response = parse_response(response_json, "claude-sonnet-5".to_string()).expect("parse response");
    let tool_calls = response.tool_calls.as_ref().expect("expected tool calls");
    assert_eq!(tool_calls.len(), 1);
    let function = tool_calls[0].function.as_ref().expect("expected function call");
    assert_eq!(function.name, "get_weather");
}