vtcode-llm 0.175.4

LLM provider abstraction, client implementations, and streaming for VT Code
Documentation
//! Text-only, single-question Decisions choice contract.

use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use vtcode_commons::tool_types::CompactStr;

use super::{LLMError, Usage};

/// The model supported by the experimental Decisions endpoint.
pub const DECISIONS_MODEL: &str = "gpt-6-luna";

/// Whether `base_url` is the direct standard OpenAI endpoint Decisions requires.
///
/// Shared by the runtime `OpenAIProvider::decisions_eligible` gate and the
/// `/model` picker's non-I/O eligibility mirror, so the two cannot drift. Empty
/// input is not standard: callers that treat "no override" as eligible must
/// filter empties before calling this.
pub fn is_standard_decisions_endpoint(base_url: &str) -> bool {
    let Ok(url) = url::Url::parse(base_url) else {
        return false;
    };
    url.scheme() == "https"
        && url.host_str() == Some("api.openai.com")
        && url.port_or_known_default() == Some(443)
        && url.username().is_empty()
        && url.password().is_none()
        && matches!(url.path(), "/v1" | "/v1/")
        && url.query().is_none()
        && url.fragment().is_none()
}

/// One supplied text category and its meaning.
#[derive(Debug, Clone, Serialize)]
pub struct DecisionChoiceOption {
    pub value: CompactStr,
    pub description: CompactStr,
}

/// Evidence and instructions for exactly one named choice question.
/// Deliberately omits images, predicates, scores, and generation controls.
#[derive(Clone)]
pub struct ChoiceDecisionRequest {
    pub input: String,
    pub name: CompactStr,
    pub instructions: String,
    pub choices: Vec<DecisionChoiceOption>,
}

impl ChoiceDecisionRequest {
    pub(crate) fn payload(&self) -> Result<Value, LLMError> {
        if self.name.is_empty()
            || self.choices.len() < 2
            || self.choices.iter().enumerate().any(|(index, choice)| {
                choice.value.is_empty() || self.choices[..index].iter().any(|previous| previous.value == choice.value)
            })
        {
            return Err(LLMError::Provider {
                message: "Invalid Decisions choice question".to_owned(),
                metadata: None,
            });
        }
        Ok(json!({
            "model": DECISIONS_MODEL,
            "input": self.input,
            "questions": [{"type": "choice", "name": self.name, "instructions": self.instructions, "choices": self.choices}]
        }))
    }
}

/// A probability for one supplied category.
#[derive(Debug, Clone, Deserialize)]
pub struct DecisionProbability {
    pub value: CompactStr,
    pub probability: f64,
}

/// A validated choice answer. Confidence is retained without a threshold.
#[derive(Debug, Clone, Deserialize)]
pub struct ChoiceDecisionAnswer {
    pub name: CompactStr,
    pub choice: CompactStr,
    pub confidence: f64,
    pub probabilities: Vec<DecisionProbability>,
}

/// An inconclusive answer still retains independently parsed billed usage.
#[derive(Debug, Clone)]
pub struct ChoiceDecisionResponse {
    pub answer: Option<ChoiceDecisionAnswer>,
    pub usage: Option<Usage>,
}

impl ChoiceDecisionResponse {
    pub(crate) fn from_payload(payload: &Value, request: &ChoiceDecisionRequest) -> Self {
        let usage = parse_usage(payload.get("usage"));
        let answer = (|| {
            let answers = payload.get("answers")?.as_array()?;
            if answers.len() != 1 || answers.first()?.get("type")?.as_str()? != "choice" {
                return None;
            }
            let answer: ChoiceDecisionAnswer = serde_json::from_value(answers.first()?.clone()).ok()?;
            let allowed = |value: &str| request.choices.iter().any(|option| option.value == value);
            if answer.name != request.name
                || !allowed(&answer.choice)
                || !unit_interval(answer.confidence)
                || answer.probabilities.len() != request.choices.len()
                || answer.probabilities.iter().enumerate().any(|(index, probability)| {
                    !allowed(&probability.value)
                        || !unit_interval(probability.probability)
                        || answer.probabilities[..index]
                            .iter()
                            .any(|previous| previous.value == probability.value)
                })
            {
                return None;
            }
            Some(answer)
        })();
        Self { answer, usage }
    }
}

fn unit_interval(value: f64) -> bool {
    value.is_finite() && (0.0..=1.0).contains(&value)
}

fn parse_usage(value: Option<&Value>) -> Option<Usage> {
    let value = value?;
    let input = u32::try_from(value.get("input_tokens")?.as_u64()?).ok()?;
    let output = u32::try_from(value.get("output_tokens")?.as_u64()?).ok()?;
    let total = u32::try_from(value.get("total_tokens")?.as_u64()?).ok()?;
    if input.checked_add(output)? != total {
        return None;
    }
    Some(Usage {
        prompt_tokens: input,
        completion_tokens: output,
        total_tokens: total,
        cached_prompt_tokens: None,
        cache_creation_tokens: None,
        cache_read_tokens: None,
        iterations: None,
    })
}

#[cfg(test)]
mod tests;