harn-vm 0.10.125

Async bytecode virtual machine for the Harn programming language
Documentation
use crate::llm::usage::ProviderUsageReceipt;
use crate::value::{VmError, VmValue};

use super::{
    parse_openai_responses_response, test_support::parse_llm_response, ProviderResponseEnvelope,
};

fn assert_provider_response_envelope(
    error: &VmError,
    response_id: Option<&str>,
    stop_reason: Option<&str>,
    block_types: &[&str],
    input_tokens: i64,
    output_tokens: i64,
) {
    let response = ProviderResponseEnvelope::from_error(error)
        .expect("empty generation must retain its typed provider response");
    assert_eq!(response.response_id(), response_id);
    assert_eq!(response.stop_reason(), stop_reason);
    assert_eq!(response.content_block_count(), block_types.len());
    assert_eq!(
        response.content_block_types(),
        block_types
            .iter()
            .map(|value| (*value).to_string())
            .collect::<Vec<_>>()
    );
    let receipt = ProviderUsageReceipt::from_error(error)
        .expect("empty generation must retain the compatibility usage receipt");
    let VmValue::Dict(fields) = receipt.to_vm_value() else {
        panic!("receipt must lower to a dictionary");
    };
    assert_eq!(
        fields.get("input_tokens").and_then(VmValue::as_int),
        Some(input_tokens)
    );
    assert_eq!(
        fields.get("output_tokens").and_then(VmValue::as_int),
        Some(output_tokens)
    );
}

#[test]
fn anthropic_empty_completion_keeps_provider_response_envelope() {
    let response = serde_json::json!({
        "id": "msg-empty",
        "content": [{"type": "policy_outcome", "detail": ""}],
        "stop_reason": "refusal",
        "usage": {"input_tokens": 11, "output_tokens": 7}
    });

    let error = parse_llm_response(&response, "anthropic", "claude-opus-4-7", true, false)
        .expect_err("empty Anthropic completion must be rejected");

    assert_provider_response_envelope(
        &error,
        Some("msg-empty"),
        Some("refusal"),
        &["policy_outcome"],
        11,
        7,
    );
}

#[test]
fn openai_responses_empty_completion_keeps_provider_response_envelope() {
    let response = serde_json::json!({
        "id": "resp-empty",
        "status": "completed",
        "output": [{
            "type": "message",
            "content": [{"type": "policy_outcome", "detail": ""}]
        }],
        "usage": {"input_tokens": 19, "output_tokens": 3}
    });

    let error = parse_openai_responses_response(&response, "openai", "gpt-5.4-preview")
        .expect_err("empty Responses API completion must be rejected");

    assert_provider_response_envelope(
        &error,
        Some("resp-empty"),
        Some("completed"),
        &["policy_outcome"],
        19,
        3,
    );
}

#[test]
fn openai_responses_incomplete_reason_outranks_generic_status() {
    let response = serde_json::json!({
        "id": "resp-incomplete",
        "status": "incomplete",
        "incomplete_details": {"reason": "max_output_tokens"},
        "output": [],
        "usage": {"input_tokens": 23, "output_tokens": 5}
    });

    let error = parse_openai_responses_response(&response, "openai", "gpt-5.4-preview")
        .expect_err("incomplete empty response must be rejected");

    assert_provider_response_envelope(
        &error,
        Some("resp-incomplete"),
        Some("max_output_tokens"),
        &[],
        23,
        5,
    );
}

#[test]
fn openai_chat_empty_completion_keeps_provider_response_envelope() {
    let response = serde_json::json!({
        "id": "chatcmpl-empty",
        "choices": [{
            "message": {"content": ""},
            "finish_reason": "stop"
        }],
        "usage": {"prompt_tokens": 1, "completion_tokens": 0}
    });

    let error = parse_llm_response(&response, "openai", "gpt-5.4-preview", false, false)
        .expect_err("empty provider message must be rejected");

    let VmError::Thrown(VmValue::Dict(fields)) = &error else {
        panic!("empty generation must be structured: {error:?}");
    };
    assert!(fields
        .get("code")
        .is_some_and(|value| value.display() == "empty_generation"));
    assert!(matches!(fields.get("output_tokens"), Some(VmValue::Int(0))));
    assert_provider_response_envelope(
        &error,
        Some("chatcmpl-empty"),
        Some("stop"),
        &["text"],
        1,
        0,
    );
    assert!(error.to_string().contains("delivered no content"));
}

#[test]
fn openai_chat_distinguishes_empty_reasoning_from_absent_reasoning() {
    let absent = serde_json::json!({
        "id": "chatcmpl-absent-reasoning",
        "choices": [{"message": {"content": null}, "finish_reason": "stop"}],
        "usage": {"prompt_tokens": 2, "completion_tokens": 0}
    });
    let present = serde_json::json!({
        "id": "chatcmpl-empty-reasoning",
        "choices": [{
            "message": {"content": null, "reasoning_content": ""},
            "finish_reason": "stop"
        }],
        "usage": {"prompt_tokens": 2, "completion_tokens": 0}
    });

    let absent_error = parse_llm_response(&absent, "openai", "reasoning-route", false, false)
        .expect_err("absent reasoning remains an empty generation");
    let present_error = parse_llm_response(&present, "openai", "reasoning-route", false, false)
        .expect_err("empty reasoning remains an empty generation");

    let absent_envelope = ProviderResponseEnvelope::from_error(&absent_error)
        .expect("absent reasoning must still carry an envelope");
    let present_envelope = ProviderResponseEnvelope::from_error(&present_error)
        .expect("empty reasoning must still carry an envelope");
    assert!(absent_envelope.content_block_types().is_empty());
    assert_eq!(
        present_envelope.content_block_types(),
        &["reasoning".to_string()]
    );
}