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
12pub fn is_standard_decisions_endpoint(base_url: &str) -> bool {
19 let Ok(url) = url::Url::parse(base_url) else {
20 return false;
21 };
22 url.scheme() == "https"
23 && url.host_str() == Some("api.openai.com")
24 && url.port_or_known_default() == Some(443)
25 && url.username().is_empty()
26 && url.password().is_none()
27 && matches!(url.path(), "/v1" | "/v1/")
28 && url.query().is_none()
29 && url.fragment().is_none()
30}
31
32#[derive(Debug, Clone, Serialize)]
34pub struct DecisionChoiceOption {
35 pub value: CompactStr,
36 pub description: CompactStr,
37}
38
39#[derive(Clone)]
42pub struct ChoiceDecisionRequest {
43 pub input: String,
44 pub name: CompactStr,
45 pub instructions: String,
46 pub choices: Vec<DecisionChoiceOption>,
47}
48
49impl ChoiceDecisionRequest {
50 pub(crate) fn payload(&self) -> Result<Value, LLMError> {
51 if self.name.is_empty()
52 || self.choices.len() < 2
53 || self.choices.iter().enumerate().any(|(index, choice)| {
54 choice.value.is_empty() || self.choices[..index].iter().any(|previous| previous.value == choice.value)
55 })
56 {
57 return Err(LLMError::Provider {
58 message: "Invalid Decisions choice question".to_owned(),
59 metadata: None,
60 });
61 }
62 Ok(json!({
63 "model": DECISIONS_MODEL,
64 "input": self.input,
65 "questions": [{"type": "choice", "name": self.name, "instructions": self.instructions, "choices": self.choices}]
66 }))
67 }
68}
69
70#[derive(Debug, Clone, Deserialize)]
72pub struct DecisionProbability {
73 pub value: CompactStr,
74 pub probability: f64,
75}
76
77#[derive(Debug, Clone, Deserialize)]
79pub struct ChoiceDecisionAnswer {
80 pub name: CompactStr,
81 pub choice: CompactStr,
82 pub confidence: f64,
83 pub probabilities: Vec<DecisionProbability>,
84}
85
86#[derive(Debug, Clone)]
88pub struct ChoiceDecisionResponse {
89 pub answer: Option<ChoiceDecisionAnswer>,
90 pub usage: Option<Usage>,
91}
92
93impl ChoiceDecisionResponse {
94 pub(crate) fn from_payload(payload: &Value, request: &ChoiceDecisionRequest) -> Self {
95 let usage = parse_usage(payload.get("usage"));
96 let answer = (|| {
97 let answers = payload.get("answers")?.as_array()?;
98 if answers.len() != 1 || answers.first()?.get("type")?.as_str()? != "choice" {
99 return None;
100 }
101 let answer: ChoiceDecisionAnswer = serde_json::from_value(answers.first()?.clone()).ok()?;
102 let allowed = |value: &str| request.choices.iter().any(|option| option.value == value);
103 if answer.name != request.name
104 || !allowed(&answer.choice)
105 || !unit_interval(answer.confidence)
106 || answer.probabilities.len() != request.choices.len()
107 || answer.probabilities.iter().enumerate().any(|(index, probability)| {
108 !allowed(&probability.value)
109 || !unit_interval(probability.probability)
110 || answer.probabilities[..index]
111 .iter()
112 .any(|previous| previous.value == probability.value)
113 })
114 {
115 return None;
116 }
117 Some(answer)
118 })();
119 Self { answer, usage }
120 }
121}
122
123fn unit_interval(value: f64) -> bool {
124 value.is_finite() && (0.0..=1.0).contains(&value)
125}
126
127fn parse_usage(value: Option<&Value>) -> Option<Usage> {
128 let value = value?;
129 let input = u32::try_from(value.get("input_tokens")?.as_u64()?).ok()?;
130 let output = u32::try_from(value.get("output_tokens")?.as_u64()?).ok()?;
131 let total = u32::try_from(value.get("total_tokens")?.as_u64()?).ok()?;
132 if input.checked_add(output)? != total {
133 return None;
134 }
135 Some(Usage {
136 prompt_tokens: input,
137 completion_tokens: output,
138 total_tokens: total,
139 cached_prompt_tokens: None,
140 cache_creation_tokens: None,
141 cache_read_tokens: None,
142 iterations: None,
143 })
144}
145
146#[cfg(test)]
147mod tests;