vtcode-llm 0.177.0

LLM provider abstraction, client implementations, and streaming for VT Code
Documentation
use super::*;
use crate::provider::{LLMProvider, Message};
use vtcode_config::constants::models;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn provider() -> OpenAIProvider {
    let mut provider = OpenAIProvider::with_model(String::new(), models::openai::GPT_6_ASTRA.to_string());
    provider.prompt_cache_enabled = true;
    provider.prompt_cache_settings.surface_metrics = true;
    provider
}

fn request(key: &str) -> LLMRequest {
    LLMRequest {
        model: models::openai::GPT_6_ASTRA.to_string(),
        prompt_cache_key: Some(key.to_string()),
        messages: vec![Message::user("Continue.".to_string())].into(),
        ..Default::default()
    }
}

fn completed(id: &str) -> Value {
    json!({ "id": id, "status": "completed", "model": "gpt-6-astra",
        "output": [{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "OK"}]}],
        "prompt_cache_diagnostics": {"type": "cache_miss", "reason": "tools_changed", "cache_missed_tokens": 1536} })
}

#[test]
fn comparison_is_key_scoped_bounded_and_independent_of_replay_lineage() {
    let provider = provider();
    let mut request = request("session-a");
    assert!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id")
            .is_none()
    );
    provider
        .cache_comparison_tracker(&request)
        .unwrap()
        .record_completion(&LLMResponse {
            request_id: Some("resp_baseline_a".to_string()),
            ..Default::default()
        });
    request.previous_response_id = None;
    let after = provider.convert_to_openai_responses_format(&request).unwrap();
    assert_eq!(after.pointer("/prompt_cache_options/comparison_response_id"), Some(&json!("resp_baseline_a")));
    assert_eq!(after.pointer("/prompt_cache_options/ttl"), Some(&json!("30m")));
    assert!(after.get("previous_response_id").is_none());
    request.prompt_cache_key = Some("session-b".to_string());
    assert!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id")
            .is_none()
    );
    request.prompt_cache_key = Some("session-a".to_string());
    for id in [
        "chatcmpl_123".to_string(),
        "resp_\nforged".to_string(),
        format!("resp_{}", "x".repeat(257)),
    ] {
        provider
            .cache_comparison_tracker(&request)
            .unwrap()
            .record_completion(&LLMResponse { request_id: Some(id), ..Default::default() });
    }
    assert_eq!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id"),
        Some(&json!("resp_baseline_a"))
    );
}

#[test]
fn comparison_requires_native_supported_model_and_enabled_metrics() {
    let mut provider = provider();
    let mut request = request("session-a");
    request.model = models::openai::GPT_5.to_string();
    assert!(provider.cache_comparison_tracker(&request).is_none());
    request.model = models::openai::GPT_6_ASTRA.to_string();
    provider.prompt_cache_settings.surface_metrics = false;
    assert!(provider.cache_comparison_tracker(&request).is_none());
    provider.prompt_cache_settings.surface_metrics = true;
    provider.prompt_cache_enabled = false;
    assert!(provider.cache_comparison_tracker(&request).is_none());
    provider.prompt_cache_enabled = true;
    provider.provider_key_override = Some(Arc::from("custom"));
    assert!(provider.cache_comparison_tracker(&request).is_none());
}

#[test]
fn explicit_cache_opt_out_omits_native_hints_and_accounting_key() {
    for provider_disabled in [false, true] {
        let mut cfg = vtcode_config::core::PromptCachingConfig::default();
        cfg.enabled = provider_disabled;
        cfg.providers.openai.enabled = !provider_disabled;
        let provider = OpenAIProvider::from_config(
            Some(String::new()),
            None,
            Some(models::openai::GPT_6_ASTRA.to_string()),
            None,
            Some(cfg),
            None,
            None,
            None,
            None,
        );
        let mut request = request("session-off");
        request.system_prompt = Some(Arc::from("Stable instructions"));
        let payload = provider.convert_to_openai_responses_format(&request).unwrap();
        assert!(payload.get("prompt_cache_options").is_none());
        assert!(payload.get("prompt_cache_retention").is_none());
        assert!(payload.get("prompt_cache_key").is_none());
        assert!(payload.get("instructions").is_some());
        assert!(payload.pointer("/input/0/content/0/prompt_cache_breakpoint").is_none());
    }
}

#[tokio::test]
async fn foreground_http_completions_advance_comparison_auxiliary_and_errors_do_not() {
    let server = MockServer::start().await;
    let mut provider = provider();
    provider.responses_url = Arc::from(format!("{}/responses", server.uri()));
    let request = request("session-a");
    Mock::given(method("POST"))
        .and(path("/responses"))
        .respond_with(ResponseTemplate::new(200).set_body_json(completed("resp_foreground")))
        .mount(&server)
        .await;
    provider.generate(request.clone()).await.unwrap();
    server.reset().await;
    Mock::given(method("POST"))
        .and(path("/responses"))
        .respond_with(ResponseTemplate::new(200).set_body_json(completed("resp_auxiliary")))
        .mount(&server)
        .await;
    provider.generate_auxiliary(request.clone()).await.unwrap();
    let sent = server.received_requests().await.unwrap();
    assert_eq!(
        sent[0]
            .body_json::<Value>()
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id"),
        Some(&json!("resp_foreground"))
    );
    server.reset().await;
    Mock::given(method("POST"))
        .and(path("/responses"))
        .respond_with(ResponseTemplate::new(500).set_body_json(json!({"error": {"message": "Unavailable"}})))
        .mount(&server)
        .await;
    assert!(provider.generate(request.clone()).await.is_err());
    assert_eq!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id"),
        Some(&json!("resp_foreground"))
    );
}

#[tokio::test]
async fn both_http_stream_surfaces_advance_only_on_delivered_completion() {
    use futures::StreamExt;
    let server = MockServer::start().await;
    let mut provider = provider();
    provider.responses_url = Arc::from(format!("{}/responses", server.uri()));
    let request = request("session-stream");
    let body = format!(
        "data: {}\n\ndata: [DONE]\n\n",
        json!({"type": "response.completed", "response": completed("resp_stream")})
    );
    Mock::given(method("POST"))
        .and(path("/responses"))
        .respond_with(ResponseTemplate::new(200).set_body_raw(body, "text/event-stream"))
        .mount(&server)
        .await;
    drop(provider.stream(request.clone()).await.unwrap());
    assert!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id")
            .is_none()
    );
    let mut legacy = provider.stream(request.clone()).await.unwrap();
    while let Some(event) = legacy.next().await {
        event.unwrap();
    }
    assert_eq!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id"),
        Some(&json!("resp_stream"))
    );
    server.reset().await;
    let body = format!(
        "data: {}\n\ndata: [DONE]\n\n",
        json!({"type": "response.completed", "response": completed("resp_normalized")})
    );
    Mock::given(method("POST"))
        .and(path("/responses"))
        .respond_with(ResponseTemplate::new(200).set_body_raw(body, "text/event-stream"))
        .mount(&server)
        .await;
    let mut normalized = provider.stream_normalized(request.clone()).await.unwrap();
    while let Some(event) = normalized.next().await {
        event.unwrap();
    }
    assert_eq!(
        provider
            .convert_to_openai_responses_format(&request)
            .unwrap()
            .pointer("/prompt_cache_options/comparison_response_id"),
        Some(&json!("resp_normalized"))
    );
}