1use std::collections::BTreeMap;
11
12use serde_json::Value;
13
14use crate::errors::Error;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct NoulAnswer {
21 pub noul: f64,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct ChoiceAnswer {
28 pub choice: String,
30 pub probabilities: BTreeMap<String, f64>,
32 pub confidence: f64,
34}
35
36impl ChoiceAnswer {
37 pub fn ranked(&self) -> Vec<(&str, f64)> {
40 let mut ranked: Vec<(&str, f64)> = self
41 .probabilities
42 .iter()
43 .map(|(option, probability)| (option.as_str(), *probability))
44 .collect();
45 ranked.sort_by(|a, b| {
46 b.1.partial_cmp(&a.1)
47 .unwrap_or(std::cmp::Ordering::Equal)
48 .then_with(|| a.0.cmp(b.0))
49 });
50 ranked
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct ScoreAnswer {
58 pub score: f64,
60 pub confidence: f64,
62 pub legend: BTreeMap<u32, Value>,
65 pub probabilities: BTreeMap<u32, f64>,
67}
68
69#[derive(Debug, Clone, PartialEq)]
73#[non_exhaustive]
74pub enum Answer {
75 Noul(NoulAnswer),
77 Choice(ChoiceAnswer),
79 Score(ScoreAnswer),
81 Unknown {
84 kind: String,
86 raw: Value,
88 },
89}
90
91impl From<NoulAnswer> for Answer {
92 fn from(answer: NoulAnswer) -> Self {
93 Self::Noul(answer)
94 }
95}
96
97impl From<ChoiceAnswer> for Answer {
98 fn from(answer: ChoiceAnswer) -> Self {
99 Self::Choice(answer)
100 }
101}
102
103impl From<ScoreAnswer> for Answer {
104 fn from(answer: ScoreAnswer) -> Self {
105 Self::Score(answer)
106 }
107}
108
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
112pub struct Usage {
113 pub input_tokens: u64,
115 pub output_tokens: u64,
117}
118
119#[derive(Debug, Clone, PartialEq)]
121pub struct SystemOneResponse {
122 pub model: String,
125 pub answers: BTreeMap<String, Answer>,
127 pub usage: Usage,
129 pub request_id: Option<String>,
131}
132
133impl SystemOneResponse {
134 pub fn noul(&self, id: &str) -> Option<&NoulAnswer> {
137 match self.answers.get(id)? {
138 Answer::Noul(answer) => Some(answer),
139 _ => None,
140 }
141 }
142
143 pub fn choice(&self, id: &str) -> Option<&ChoiceAnswer> {
146 match self.answers.get(id)? {
147 Answer::Choice(answer) => Some(answer),
148 _ => None,
149 }
150 }
151
152 pub fn score(&self, id: &str) -> Option<&ScoreAnswer> {
155 match self.answers.get(id)? {
156 Answer::Score(answer) => Some(answer),
157 _ => None,
158 }
159 }
160
161 pub fn nouls(&self) -> impl Iterator<Item = (&str, &NoulAnswer)> {
163 self.answers.iter().filter_map(|(id, answer)| match answer {
164 Answer::Noul(answer) => Some((id.as_str(), answer)),
165 _ => None,
166 })
167 }
168
169 pub fn choices(&self) -> impl Iterator<Item = (&str, &ChoiceAnswer)> {
171 self.answers.iter().filter_map(|(id, answer)| match answer {
172 Answer::Choice(answer) => Some((id.as_str(), answer)),
173 _ => None,
174 })
175 }
176
177 pub fn scores(&self) -> impl Iterator<Item = (&str, &ScoreAnswer)> {
179 self.answers.iter().filter_map(|(id, answer)| match answer {
180 Answer::Score(answer) => Some((id.as_str(), answer)),
181 _ => None,
182 })
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct ModelMetadata {
189 pub name: String,
191 pub description: String,
193 pub release_date: String,
195}
196
197fn validation_error(
199 status: u16,
200 field_path: &str,
201 request_id: Option<String>,
202 endpoint: &str,
203) -> Error {
204 Error::ResponseValidation {
205 status,
206 field_path: field_path.to_owned(),
207 request_id,
208 endpoint: endpoint.to_owned(),
209 }
210}
211
212fn decode_error(response: &crate::client::RawResponse, field_path: &str) -> Error {
215 validation_error(
216 response.status,
217 field_path,
218 response.request_id.clone(),
219 &response.endpoint,
220 )
221}
222
223fn number(value: &Value, path: &str) -> Result<f64, String> {
225 match value {
226 Value::Number(number) => number.as_f64().ok_or_else(|| path.to_owned()),
227 _ => Err(path.to_owned()),
228 }
229}
230
231fn string(value: &Value, path: &str) -> Result<String, String> {
233 match value {
234 Value::String(string) => Ok(string.clone()),
235 _ => Err(path.to_owned()),
236 }
237}
238
239fn number_map(value: &Value, path: &str) -> Result<BTreeMap<String, f64>, String> {
241 let Value::Object(map) = value else {
242 return Err(path.to_owned());
243 };
244 let mut result = BTreeMap::new();
245 for (key, value) in map {
246 let key_path = format!("{path}.{key}");
247 result.insert(key.clone(), number(value, &key_path)?);
248 }
249 Ok(result)
250}
251
252fn level_number_map(value: &Value, path: &str) -> Result<BTreeMap<u32, f64>, String> {
255 let Value::Object(map) = value else {
256 return Err(path.to_owned());
257 };
258 let mut result = BTreeMap::new();
259 for (key, value) in map {
260 let key_path = format!("{path}.{key}");
261 let level: u32 = key.parse().map_err(|_| key_path.clone())?;
262 result.insert(level, number(value, &key_path)?);
263 }
264 Ok(result)
265}
266
267fn level_value_map(value: &Value, path: &str) -> Result<BTreeMap<u32, Value>, String> {
269 let Value::Object(map) = value else {
270 return Err(path.to_owned());
271 };
272 let mut result = BTreeMap::new();
273 for (key, value) in map {
274 let key_path = format!("{path}.{key}");
275 if !matches!(value, Value::String(_) | Value::Object(_) | Value::Array(_)) {
278 return Err(key_path);
279 }
280 let level: u32 = key.parse().map_err(|_| key_path.clone())?;
281 result.insert(level, value.clone());
282 }
283 Ok(result)
284}
285
286fn decode_answer(id: &str, value: &Value) -> Result<Answer, String> {
288 let Value::Object(answer) = value else {
289 return Err(format!("answers.{id}"));
290 };
291 let answer_type = match answer.get("type") {
292 Some(Value::String(answer_type)) => answer_type.as_str(),
293 _ => return Err(format!("answers.{id}.type")),
294 };
295 let field = |name: &str| answer.get(name);
296 match answer_type {
297 "noul" => {
298 let noul = field("noul").ok_or_else(|| format!("answers.{id}.noul"))?;
299 Ok(Answer::Noul(NoulAnswer {
300 noul: number(noul, &format!("answers.{id}.noul"))?,
301 }))
302 }
303 "choice" => {
304 let choice = field("choice").ok_or_else(|| format!("answers.{id}.choice"))?;
305 let probabilities =
306 field("probabilities").ok_or_else(|| format!("answers.{id}.probabilities"))?;
307 let confidence =
308 field("confidence").ok_or_else(|| format!("answers.{id}.confidence"))?;
309 Ok(Answer::Choice(ChoiceAnswer {
310 choice: string(choice, &format!("answers.{id}.choice"))?,
311 probabilities: number_map(probabilities, &format!("answers.{id}.probabilities"))?,
312 confidence: number(confidence, &format!("answers.{id}.confidence"))?,
313 }))
314 }
315 "score" => {
316 let score = field("score").ok_or_else(|| format!("answers.{id}.score"))?;
317 let confidence =
318 field("confidence").ok_or_else(|| format!("answers.{id}.confidence"))?;
319 let legend = field("legend").ok_or_else(|| format!("answers.{id}.legend"))?;
320 let probabilities =
321 field("probabilities").ok_or_else(|| format!("answers.{id}.probabilities"))?;
322 Ok(Answer::Score(ScoreAnswer {
323 score: number(score, &format!("answers.{id}.score"))?,
324 confidence: number(confidence, &format!("answers.{id}.confidence"))?,
325 legend: level_value_map(legend, &format!("answers.{id}.legend"))?,
326 probabilities: level_number_map(
327 probabilities,
328 &format!("answers.{id}.probabilities"),
329 )?,
330 }))
331 }
332 other => {
333 tracing::warn!(
334 answer_id = %id,
335 answer_type = %other,
336 "keeping answer with unrecognized type as Unknown"
337 );
338 Ok(Answer::Unknown {
339 kind: other.to_owned(),
340 raw: value.clone(),
341 })
342 }
343 }
344}
345
346use serde::{Deserialize, Serialize};
347
348impl SystemOneResponse {
349 pub(super) fn decode(response: &crate::client::RawResponse) -> Result<Self, Error> {
357 let body: Value =
358 serde_json::from_str(&response.body).map_err(|_| decode_error(response, ""))?;
359 let Value::Object(object) = &body else {
360 return Err(decode_error(response, ""));
361 };
362 let model = string(
363 object
364 .get("model")
365 .ok_or_else(|| decode_error(response, "model"))?,
366 "model",
367 )
368 .map_err(|_| decode_error(response, "model"))?;
369 let answers_value = object
370 .get("answers")
371 .ok_or_else(|| decode_error(response, "answers"))?;
372 let answers_object = match answers_value {
373 Value::Object(map) => map,
374 _ => return Err(decode_error(response, "answers")),
375 };
376 let mut answers: BTreeMap<String, Answer> = BTreeMap::new();
377 for (id, answer_value) in answers_object {
378 let decoded = decode_answer(id.as_str(), answer_value)
379 .map_err(|field_path| decode_error(response, &field_path))?;
380 answers.insert(id.clone(), decoded);
381 }
382 let usage = match object.get("usage") {
386 None | Some(Value::Null) => Usage::default(),
387 Some(Value::Object(usage)) => {
388 let tokens = |key: &str| -> Result<u64, Error> {
389 match usage.get(key) {
390 None | Some(Value::Null) => Ok(0),
391 Some(value) => value
392 .as_u64()
393 .ok_or_else(|| decode_error(response, &format!("usage.{key}"))),
394 }
395 };
396 Usage {
397 input_tokens: tokens("input_tokens")?,
398 output_tokens: tokens("output_tokens")?,
399 }
400 }
401 Some(_) => return Err(decode_error(response, "usage")),
402 };
403 Ok(Self {
404 model,
405 answers,
406 usage,
407 request_id: response.request_id.clone(),
408 })
409 }
410}
411
412pub(super) fn check_complete(
418 response: &crate::client::RawResponse,
419 answers: &BTreeMap<String, Answer>,
420 questions: &BTreeMap<String, crate::questions::Question>,
421) -> Result<(), Error> {
422 use crate::questions::Question;
423 for (id, question) in questions {
426 let Some(answer) = answers.get(id) else {
427 return Err(decode_error(response, &format!("answers.{id}")));
428 };
429 let matches = matches!(
430 (question, answer),
431 (Question::Noul(_), Answer::Noul(_))
432 | (Question::Choice(_), Answer::Choice(_))
433 | (Question::Score(_), Answer::Score(_))
434 | (_, Answer::Unknown { .. })
435 );
436 if !matches {
437 return Err(decode_error(response, &format!("answers.{id}.type")));
438 }
439 }
440 Ok(())
441}
442
443pub(super) fn decode_models(
444 response: &crate::client::RawResponse,
445) -> Result<Vec<ModelMetadata>, Error> {
446 let body: Value =
447 serde_json::from_str(&response.body).map_err(|_| decode_error(response, ""))?;
448 let Value::Object(object) = &body else {
449 return Err(decode_error(response, ""));
450 };
451 let Value::Array(models) = object
452 .get("models")
453 .ok_or_else(|| decode_error(response, "models"))?
454 else {
455 return Err(decode_error(response, "models"));
456 };
457 let mut result = Vec::with_capacity(models.len());
458 for (index, model) in models.iter().enumerate() {
459 let path = format!("models[{index}]");
462 let Value::Object(model) = model else {
463 return Err(decode_error(response, &path));
464 };
465 let field = |key: &str| -> Result<String, Error> {
466 let field_path = format!("{path}.{key}");
467 let value = model
468 .get(key)
469 .ok_or_else(|| decode_error(response, &field_path))?;
470 string(value, &field_path).map_err(|p| decode_error(response, &p))
471 };
472 result.push(ModelMetadata {
473 name: field("name")?,
474 description: field("description")?,
475 release_date: field("release_date")?,
476 });
477 }
478 Ok(result)
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 fn raw(body: &str) -> crate::client::RawResponse {
486 crate::client::RawResponse {
487 status: 200,
488 body: body.to_owned(),
489 request_id: None,
490 endpoint: "POST https://api.typesafe.ai/v1/systemone".into(),
491 }
492 }
493
494 #[test]
495 fn decodes_all_three_answer_kinds() {
496 let body = r#"{
497 "model": "jev-1.13.0",
498 "answers": {
499 "spam": {"type": "noul", "noul": 0.98},
500 "tone": {"type": "choice", "choice": "angry", "probabilities": {"angry": 0.8, "calm": 0.1, "excited": 0.1}, "confidence": 0.9},
501 "urgency": {"type": "score", "score": 1.7, "confidence": 0.9, "legend": {"0": "Can wait", "1": "This week", "2": "Today"}, "probabilities": {"0": 0.1, "1": 0.1, "2": 0.8}}
502 },
503 "usage": {"input_tokens": 120, "output_tokens": 12}
504 }"#;
505 let response = SystemOneResponse::decode(&raw(body)).unwrap();
506 assert_eq!(response.model, "jev-1.13.0");
507 assert_eq!(response.noul("spam").unwrap().noul, 0.98);
508 let tone = response.choice("tone").unwrap();
509 assert_eq!(tone.choice, "angry");
510 assert_eq!(tone.confidence, 0.9);
511 assert_eq!(tone.probabilities["angry"], 0.8);
512 let urgency = response.score("urgency").unwrap();
513 assert_eq!(urgency.score, 1.7);
514 assert_eq!(urgency.legend[&2], "Today");
515 assert_eq!(urgency.probabilities[&2], 0.8);
516 assert_eq!(response.usage.input_tokens, 120);
517 assert_eq!(response.usage.output_tokens, 12);
518 assert_eq!(response.nouls().count(), 1);
519 assert_eq!(response.choices().count(), 1);
520 assert_eq!(response.scores().count(), 1);
521 assert!(response.noul("tone").is_none());
522 }
523
524 #[test]
525 fn ranked_sorts_descending() {
526 let tone = ChoiceAnswer {
527 choice: "angry".into(),
528 probabilities: [("calm", 0.1), ("angry", 0.8), ("excited", 0.1)]
529 .into_iter()
530 .map(|(key, value)| (key.to_owned(), value))
531 .collect(),
532 confidence: 0.9,
533 };
534 let ranked = tone.ranked();
535 assert_eq!(ranked[0], ("angry", 0.8));
536 assert_eq!(ranked[1].0, "calm");
538 assert_eq!(ranked[2].0, "excited");
539 }
540
541 #[test]
542 fn unknown_answer_type_is_kept() {
543 let body = r#"{
544 "model": "jev-1.13.0",
545 "answers": {
546 "spam": {"type": "noul", "noul": 0.98},
547 "future": {"type": "vibes", "vibe": "immaculate", "confidence": 0.5}
548 },
549 "usage": {"input_tokens": 1, "output_tokens": 2}
550 }"#;
551 let response = SystemOneResponse::decode(&raw(body)).unwrap();
552 match response.answers.get("future").unwrap() {
553 Answer::Unknown { kind, raw } => {
554 assert_eq!(kind, "vibes");
555 assert_eq!(raw["vibe"], "immaculate");
556 }
557 other => panic!("expected Unknown, got {other:?}"),
558 }
559 assert!(response.noul("future").is_none());
561 assert_eq!(response.nouls().count(), 1);
562 }
563
564 #[test]
565 fn missing_usage_defaults_to_zero() {
566 let body = r#"{"model": "jev-1.13.0", "answers": {"a": {"type": "noul", "noul": 1.0}}}"#;
567 let response = SystemOneResponse::decode(&raw(body)).unwrap();
568 assert_eq!(response.usage, Usage::default());
569 }
570
571 #[test]
572 fn validation_error_field_paths() {
573 let body = r#"{"model": "m", "answers": {"tone": {"type": "choice", "choice": "calm", "probabilities": {"calm": 1.0}}}, "usage": {}}"#;
575 let error = SystemOneResponse::decode(&raw(body)).unwrap_err();
576 match error {
577 Error::ResponseValidation {
578 field_path, status, ..
579 } => {
580 assert_eq!(field_path, "answers.tone.confidence");
581 assert_eq!(status, 200);
582 }
583 other => panic!("expected ResponseValidation, got {other:?}"),
584 }
585 let error = SystemOneResponse::decode(&raw("nope")).unwrap_err();
587 match error {
588 Error::ResponseValidation { field_path, .. } => {
589 assert_eq!(field_path, "");
590 }
591 other => panic!("expected ResponseValidation, got {other:?}"),
592 }
593 let error = SystemOneResponse::decode(&raw(r#"{"answers": {}}"#)).unwrap_err();
595 match error {
596 Error::ResponseValidation { field_path, .. } => {
597 assert_eq!(field_path, "model");
598 }
599 other => panic!("expected ResponseValidation, got {other:?}"),
600 }
601 let error = SystemOneResponse::decode(&raw(r#"{"model": "m"}"#)).unwrap_err();
603 match error {
604 Error::ResponseValidation { field_path, .. } => {
605 assert_eq!(field_path, "answers");
606 }
607 other => panic!("expected ResponseValidation, got {other:?}"),
608 }
609 let body = r#"{"model": "m", "answers": {"a": {"noul": 1.0}}}"#;
611 let error = SystemOneResponse::decode(&raw(body)).unwrap_err();
612 match error {
613 Error::ResponseValidation { field_path, .. } => {
614 assert_eq!(field_path, "answers.a.type");
615 }
616 other => panic!("expected ResponseValidation, got {other:?}"),
617 }
618 let body = r#"{"model": "m", "answers": {"a": {"type": "noul", "noul": "yes"}}}"#;
620 let error = SystemOneResponse::decode(&raw(body)).unwrap_err();
621 match error {
622 Error::ResponseValidation { field_path, .. } => {
623 assert_eq!(field_path, "answers.a.noul");
624 }
625 other => panic!("expected ResponseValidation, got {other:?}"),
626 }
627 }
628
629 #[test]
630 fn decodes_models() {
631 let body = r#"{"models": [
632 {"name": "jev-latest", "description": "General-purpose system one model.", "release_date": "2026-09-15"}
633 ]}"#;
634 let models = decode_models(&raw(body)).unwrap();
635 assert_eq!(models.len(), 1);
636 assert_eq!(models[0].name, "jev-latest");
637 assert_eq!(models[0].release_date, "2026-09-15");
638 let error = decode_models(&raw(r#"{}"#)).unwrap_err();
639 match error {
640 Error::ResponseValidation { field_path, .. } => {
641 assert_eq!(field_path, "models");
642 }
643 other => panic!("expected ResponseValidation, got {other:?}"),
644 }
645 }
646}