Skip to main content

typesafe/
response.rs

1//! Responses from the API.
2
3use std::collections::BTreeMap;
4
5use serde_json::Value;
6
7use crate::answer::{self, Answer, ChoiceAnswer, NoulAnswer, ScoreAnswer};
8use crate::question::Questions;
9
10/// Tokens used by a call. Only input tokens are billed.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct Usage {
13    pub input_tokens: u64,
14    pub output_tokens: u64,
15}
16
17/// The result of a System One call.
18///
19/// - `answers`: one answer per question, under the id the question used
20/// - `model`: the versioned model that answered, such as `jev-1.13.0`, even
21///   when the call named an alias like `jev-latest`. Store it next to anything
22///   you keep, so a threshold can be traced to the model it was tuned on.
23/// - `usage`: tokens used
24/// - `request_id`: the `x-typesafe-request-id` header, for support requests
25/// - `body`: the decoded JSON body, for fields this client doesn't know yet
26#[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    /// The answer to the Noul asked under `id`.
41    pub fn noul(&self, id: &str) -> Option<&NoulAnswer> {
42        self.answer(id).and_then(Answer::as_noul)
43    }
44
45    /// The answer to the Choice asked under `id`.
46    pub fn choice(&self, id: &str) -> Option<&ChoiceAnswer> {
47        self.answer(id).and_then(Answer::as_choice)
48    }
49
50    /// The answer to the Score asked under `id`.
51    pub fn score(&self, id: &str) -> Option<&ScoreAnswer> {
52        self.answer(id).and_then(Answer::as_score)
53    }
54
55    /// The answer to the raw question asked under `id`.
56    pub fn raw(&self, id: &str) -> Option<&Value> {
57        self.answer(id).and_then(Answer::as_raw)
58    }
59}
60
61/// A model name the account can send, from `models()`.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Model {
64    pub name: String,
65    pub description: Option<String>,
66    /// As the server sent it, usually an ISO 8601 timestamp.
67    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}