vtcode_llm/provider/
decisions.rs1use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use vtcode_commons::tool_types::CompactStr;
6
7use super::{LLMError, Usage};
8
9pub const DECISIONS_MODEL: &str = "gpt-6-luna";
11
12#[derive(Debug, Clone, Serialize)]
14pub struct DecisionChoiceOption {
15 pub value: CompactStr,
16 pub description: CompactStr,
17}
18
19#[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#[derive(Debug, Clone, Deserialize)]
52pub struct DecisionProbability {
53 pub value: CompactStr,
54 pub probability: f64,
55}
56
57#[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#[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;