oy-cli 0.10.1

Local AI coding CLI for inspecting, editing, running commands, and auditing repositories
Documentation
use super::*;
use crate::llm::{CacheHint, GenerationOptions, ModelRoute, Protocol, RouteAuth};

fn request() -> LlmRequest {
    LlmRequest {
        route: ModelRoute {
            protocol: Protocol::AnthropicMessages,
            model: "claude-sonnet-4-5".to_string(),
            base_url: Some("https://api.anthropic.com/v1".to_string()),
            auth: RouteAuth::Header {
                name: "x-api-key".to_string(),
                value: "secret".to_string(),
            },
            query_params: None,
            additional_params: None,
        },
        system_prompt: "You are terse".to_string(),
        system_cache: Some(CacheHint::Ephemeral {
            ttl_seconds: Some(3600),
        }),
        messages: vec![Message::user_text("hello")],
        tools: vec![ToolSpec {
            name: "read".to_string(),
            description: "Read a file".to_string(),
            parameters: json!({"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}),
            cache: None,
        }],
        max_turns: 4,
        tool_choice: Some(ToolChoice::Required),
        generation: Some(GenerationOptions {
            max_tokens: Some(123),
            temperature: Some(0.2),
            ..Default::default()
        }),
        cache: None,
    }
}

#[test]
fn request_body_lowers_messages_tools_cache_and_generation() {
    let body = request_body(&request()).unwrap();

    assert_eq!(body["model"], "claude-sonnet-4-5");
    assert_eq!(body["stream"], true);
    assert_eq!(body["max_tokens"], 123);
    assert_eq!(body["temperature"], 0.2);
    assert_eq!(body["tool_choice"], json!({"type": "any"}));
    assert_eq!(
        body["system"][0]["cache_control"],
        json!({"type": "ephemeral", "ttl": "1h"})
    );
    assert_eq!(body["tools"][0]["name"], "read");
    assert_eq!(body["messages"][0]["role"], "user");
    assert_eq!(
        body["messages"][0]["content"][0],
        json!({"type":"text","text":"hello"})
    );
}

#[test]
fn stream_parser_maps_text_tool_usage_and_finish() {
    let mut state = StreamState::default();
    let mut events = Vec::new();
    events.extend(parse_stream_event(&mut state, &json!({"type":"message_start","message":{"usage":{"input_tokens":2,"cache_read_input_tokens":3}}})).unwrap());
    events.extend(
        parse_stream_event(
            &mut state,
            &json!({"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"}}),
        )
        .unwrap(),
    );
    events.extend(parse_stream_event(&mut state, &json!({"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"read"}})).unwrap());
    events.extend(parse_stream_event(&mut state, &json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":"}})).unwrap());
    events.extend(parse_stream_event(&mut state, &json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"README.md\"}"}})).unwrap());
    events.extend(
        parse_stream_event(&mut state, &json!({"type":"content_block_stop","index":1})).unwrap(),
    );
    events.extend(parse_stream_event(&mut state, &json!({"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":4}})).unwrap());
    events.extend(finish_stream(&mut state).unwrap());

    assert!(matches!(events[0], LlmEvent::TextDelta { ref text } if text == "hi"));
    let call = events
        .iter()
        .find_map(|event| match event {
            LlmEvent::ToolCall { call, .. } => Some(call),
            _ => None,
        })
        .unwrap();
    assert_eq!(call.call_id, "toolu_1");
    assert_eq!(
        call.arguments_value().unwrap(),
        json!({"path": "README.md"})
    );
    assert!(events.iter().any(|event| matches!(
        event,
        LlmEvent::StepFinish {
            reason: FinishReason::ToolCalls,
            ..
        }
    )));
}