1use std::collections::BTreeMap;
4
5use serde_json::Value;
6
7use crate::answer::{self, Answer, ChoiceAnswer, NoulAnswer, ScoreAnswer};
8use crate::question::Questions;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct Usage {
13 pub input_tokens: u64,
14 pub output_tokens: u64,
15}
16
17#[derive(Clone, Debug, PartialEq)]
27pub struct Response {
28 pub model: String,
29 pub answers: BTreeMap<String, Answer>,
30 pub usage: Usage,
31 pub request_id: Option<String>,
32 pub body: Value,
33}
34
35impl Response {
36 pub fn answer(&self, id: &str) -> Option<&Answer> {
37 self.answers.get(id)
38 }
39
40 pub fn noul(&self, id: &str) -> Option<&NoulAnswer> {
42 self.answer(id).and_then(Answer::as_noul)
43 }
44
45 pub fn choice(&self, id: &str) -> Option<&ChoiceAnswer> {
47 self.answer(id).and_then(Answer::as_choice)
48 }
49
50 pub fn score(&self, id: &str) -> Option<&ScoreAnswer> {
52 self.answer(id).and_then(Answer::as_score)
53 }
54
55 pub fn raw(&self, id: &str) -> Option<&Value> {
57 self.answer(id).and_then(Answer::as_raw)
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Model {
64 pub name: String,
65 pub description: Option<String>,
66 pub release_date: Option<String>,
68}
69
70pub(crate) fn decode(
71 body: &Value,
72 questions: &Questions,
73 request_id: Option<String>,
74) -> Result<Response, String> {
75 let model = body
76 .get("model")
77 .and_then(Value::as_str)
78 .ok_or_else(|| "model".to_string())?
79 .to_string();
80 let usage = body.get("usage").ok_or_else(|| "usage".to_string())?;
81 let tokens = |field: &str| {
82 usage
83 .get(field)
84 .and_then(Value::as_u64)
85 .ok_or_else(|| format!("usage.{field}"))
86 };
87 let usage = Usage {
88 input_tokens: tokens("input_tokens")?,
89 output_tokens: tokens("output_tokens")?,
90 };
91 let answers = answer::decode_all(questions, body.get("answers"))?;
92 Ok(Response {
93 model,
94 answers,
95 usage,
96 request_id,
97 body: body.clone(),
98 })
99}
100
101pub(crate) fn decode_models(body: &Value) -> Result<Vec<Model>, String> {
102 let models = body
103 .get("models")
104 .and_then(Value::as_array)
105 .ok_or_else(|| "models".to_string())?;
106 models
107 .iter()
108 .enumerate()
109 .map(|(i, raw)| {
110 let name = raw
111 .get("name")
112 .and_then(Value::as_str)
113 .ok_or_else(|| format!("models[{i}].name"))?;
114 let text = |field: &str| raw.get(field).and_then(Value::as_str).map(str::to_string);
115 Ok(Model {
116 name: name.to_string(),
117 description: text("description"),
118 release_date: text("release_date"),
119 })
120 })
121 .collect()
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::Noul;
128 use serde_json::json;
129
130 #[test]
131 fn decodes_a_response() {
132 let questions = Questions::new().ask("billing", Noul::new("Billing?"));
133 let body = json!({
134 "model": "jev-1.13.0",
135 "answers": {"billing": {"type": "noul", "noul": 0.97}},
136 "usage": {"input_tokens": 100, "output_tokens": 10},
137 "later": "a field this client doesn't know"
138 });
139 let response = decode(&body, &questions, Some("req_1".into())).unwrap();
140 assert_eq!(response.model, "jev-1.13.0");
141 assert_eq!(response.noul("billing").unwrap().noul, 0.97);
142 assert!(response.choice("billing").is_none());
143 assert_eq!(response.usage.input_tokens, 100);
144 assert_eq!(response.request_id.as_deref(), Some("req_1"));
145 assert_eq!(response.body["later"], "a field this client doesn't know");
146 }
147
148 #[test]
149 fn names_missing_top_level_fields() {
150 let questions = Questions::new().ask("q", Noul::new("Is it?"));
151 let answers = json!({"q": {"type": "noul", "noul": 0.5}});
152 assert_eq!(
153 decode(
154 &json!({"answers": answers, "usage": {"input_tokens": 1, "output_tokens": 1}}),
155 &questions,
156 None
157 )
158 .unwrap_err(),
159 "model"
160 );
161 assert_eq!(
162 decode(
163 &json!({"model": "m", "answers": answers, "usage": {"input_tokens": 1}}),
164 &questions,
165 None
166 )
167 .unwrap_err(),
168 "usage.output_tokens"
169 );
170 }
171
172 #[test]
173 fn decodes_models() {
174 let models = decode_models(&json!({"models": [
175 {"name": "jev-latest", "description": "Alias", "release_date": "2026-01-01T00:00:00Z"},
176 {"name": "jev-1.13.0"}
177 ]}))
178 .unwrap();
179 assert_eq!(models[1].name, "jev-1.13.0");
180 assert_eq!(models[1].description, None);
181 assert_eq!(
182 decode_models(&json!({"models": [{}]})).unwrap_err(),
183 "models[0].name"
184 );
185 }
186}