Skip to main content

vtcode_llm/provider/
decisions.rs

1//! Text-only, single-question Decisions choice contract.
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use vtcode_commons::tool_types::CompactStr;
6
7use super::{LLMError, Usage};
8
9/// The model supported by the experimental Decisions endpoint.
10pub const DECISIONS_MODEL: &str = "gpt-6-luna";
11
12/// One supplied text category and its meaning.
13#[derive(Debug, Clone, Serialize)]
14pub struct DecisionChoiceOption {
15    pub value: CompactStr,
16    pub description: CompactStr,
17}
18
19/// Evidence and instructions for exactly one named choice question.
20/// Deliberately omits images, predicates, scores, and generation controls.
21#[derive(Clone)]
22pub struct ChoiceDecisionRequest {
23    pub input: String,
24    pub name: CompactStr,
25    pub instructions: String,
26    pub choices: Vec<DecisionChoiceOption>,
27}
28
29impl ChoiceDecisionRequest {
30    pub(crate) fn payload(&self) -> Result<Value, LLMError> {
31        if self.name.is_empty()
32            || self.choices.len() < 2
33            || self.choices.iter().enumerate().any(|(index, choice)| {
34                choice.value.is_empty() || self.choices[..index].iter().any(|previous| previous.value == choice.value)
35            })
36        {
37            return Err(LLMError::Provider {
38                message: "Invalid Decisions choice question".to_owned(),
39                metadata: None,
40            });
41        }
42        Ok(json!({
43            "model": DECISIONS_MODEL,
44            "input": self.input,
45            "questions": [{"type": "choice", "name": self.name, "instructions": self.instructions, "choices": self.choices}]
46        }))
47    }
48}
49
50/// A probability for one supplied category.
51#[derive(Debug, Clone, Deserialize)]
52pub struct DecisionProbability {
53    pub value: CompactStr,
54    pub probability: f64,
55}
56
57/// A validated choice answer. Confidence is retained without a threshold.
58#[derive(Debug, Clone, Deserialize)]
59pub struct ChoiceDecisionAnswer {
60    pub name: CompactStr,
61    pub choice: CompactStr,
62    pub confidence: f64,
63    pub probabilities: Vec<DecisionProbability>,
64}
65
66/// An inconclusive answer still retains independently parsed billed usage.
67#[derive(Debug, Clone)]
68pub struct ChoiceDecisionResponse {
69    pub answer: Option<ChoiceDecisionAnswer>,
70    pub usage: Option<Usage>,
71}
72
73impl ChoiceDecisionResponse {
74    pub(crate) fn from_payload(payload: &Value, request: &ChoiceDecisionRequest) -> Self {
75        let usage = parse_usage(payload.get("usage"));
76        let answer = (|| {
77            let answers = payload.get("answers")?.as_array()?;
78            if answers.len() != 1 || answers.first()?.get("type")?.as_str()? != "choice" {
79                return None;
80            }
81            let answer: ChoiceDecisionAnswer = serde_json::from_value(answers.first()?.clone()).ok()?;
82            let allowed = |value: &str| request.choices.iter().any(|option| option.value == value);
83            if answer.name != request.name
84                || !allowed(&answer.choice)
85                || !unit_interval(answer.confidence)
86                || answer.probabilities.len() != request.choices.len()
87                || answer.probabilities.iter().enumerate().any(|(index, probability)| {
88                    !allowed(&probability.value)
89                        || !unit_interval(probability.probability)
90                        || answer.probabilities[..index]
91                            .iter()
92                            .any(|previous| previous.value == probability.value)
93                })
94            {
95                return None;
96            }
97            Some(answer)
98        })();
99        Self { answer, usage }
100    }
101}
102
103fn unit_interval(value: f64) -> bool {
104    value.is_finite() && (0.0..=1.0).contains(&value)
105}
106
107fn parse_usage(value: Option<&Value>) -> Option<Usage> {
108    let value = value?;
109    let input = u32::try_from(value.get("input_tokens")?.as_u64()?).ok()?;
110    let output = u32::try_from(value.get("output_tokens")?.as_u64()?).ok()?;
111    let total = u32::try_from(value.get("total_tokens")?.as_u64()?).ok()?;
112    if input.checked_add(output)? != total {
113        return None;
114    }
115    Some(Usage {
116        prompt_tokens: input,
117        completion_tokens: output,
118        total_tokens: total,
119        cached_prompt_tokens: None,
120        cache_creation_tokens: None,
121        cache_read_tokens: None,
122        iterations: None,
123    })
124}
125
126#[cfg(test)]
127mod tests;