//! Wire format of POST /v1/systemone: the Decision Index / TypeSafe System One request and response (checked by decision_index.engines.base.validate). use serde::{Deserialize, Serialize}; use serde_json::Value; use serde::ser::{Serialize as Ser, SerializeMap, Serializer}; #[derive(Debug, Deserialize)] pub struct Request { #[serde(default)] pub model: Option, pub state: Value, /// Decision Index kit image input: up to 4 data URIs or bare base64 strings, shown to the model in order before the text #[serde(default)] pub images: Option, /// TypeSafe assets: {placeholder: {"type": "image", "format": "png"|"jpg"|"jpeg", "data_base64": ...} | {"part": multipart part name}}; /// every placeholder must occur in `state` or a question's instructions / criteria (string values, never object keys) #[serde(default)] pub assets: Option, /// insertion order matters (probabilities are reported per criteria key; serde_json preserve_order keeps it) pub questions: serde_json::Map, /// reasoning controls: {"mode": "auto"|"on"|"off", "budget_ms": ms, "trace": bool} #[serde(default)] pub reasoning: Option, } #[derive(Debug, Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ReasoningReq { /// "auto" (default: the server's gate decides per question) | "on" (every question thinks) | "off" (no question thinks) #[serde(default)] pub mode: Option, /// wall-clock thinking budget in ms, [0, 600000]; 0 = none (thoughts run to the end-of-thought token or the cap). /// Absent: the server's budget, except mode "on" which then has no wall-clock limit #[serde(default)] pub budget_ms: Option, /// return each thought's decoded text in `answers[q].reasoning.trace` #[serde(default)] pub trace: Option, } /// JSON object that keeps insertion order (Python dict semantics: answers in question order, probabilities in criteria order) #[derive(Debug, Clone, PartialEq, Default)] pub struct OrderedMap(pub Vec<(String, V)>); impl OrderedMap { pub fn new() -> Self { OrderedMap(vec![]) } pub fn insert(&mut self, k: String, v: V) { self.0.push((k, v)); } pub fn get(&self, k: &str) -> Option<&V> { self.0.iter().find(|(a, _)| a == k).map(|(_, v)| v) } } impl Ser for OrderedMap { fn serialize(&self, s: S) -> Result { let mut m = s.serialize_map(Some(self.0.len()))?; for (k, v) in &self.0 { m.serialize_entry(k, v)?; } m.end() } } #[derive(Debug, Serialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum AnswerOut { Choice { choice: String, confidence: f32, probabilities: OrderedMap }, Noul { noul: f32 }, /// TypeSafe `score`: ordinal levels 0..n-1 (criteria array order); legend echoes each level as sent Score { score: f32, legend: OrderedMap, probabilities: OrderedMap, confidence: f32 }, /// free-form number (extension): `number` = parsed value (null if `text` is not a number), `text` = the generated answer, /// `confidence` = exp(mean log-probability of the answer tokens) Number { number: Option, text: String, confidence: f32 }, } /// one answer: the typed fields, plus `reasoning` only when the question thought #[derive(Debug, Serialize, PartialEq)] pub struct Answer { #[serde(flatten)] pub answer: AnswerOut, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, } #[derive(Debug, Serialize, PartialEq)] pub struct ReasoningOut { /// thought tokens generated for this question pub tokens: usize, /// decoded thought text; only when the request asked for traces #[serde(skip_serializing_if = "Option::is_none")] pub trace: Option, } /// request totals #[derive(Debug, Serialize, Default)] pub struct Usage { /// prompt tokens summed over the questions (each question's prompt incl. the shared state, and every image's soft tokens + 2 delimiters) pub input_tokens: usize, /// thought tokens generated, summed over the questions pub reasoning_tokens: usize, /// answer tokens generated: the tokens of `number` answers (choice / noul / score answers are read from one forward's logits and generate none) pub output_tokens: usize, /// server-side time from request arrival to the response pub latency_ms: f64, } #[derive(Debug, Serialize)] pub struct Response { pub model: String, pub answers: OrderedMap, pub usage: Usage, /// total image soft tokens of the request (read by the DI vision runner into environment.json effective_image_budget.max_image_tokens); omitted without images #[serde(skip_serializing_if = "Option::is_none")] pub image_tokens: Option, } /// error body `{"error": {"type": kind, "message": msg}}` with HTTP status `code` #[derive(Debug)] pub struct ApiError { pub code: u16, pub kind: &'static str, pub msg: String, } impl ApiError { /// the default kind of each status: 400 invalid_request, 401 authentication_error, 413 request_too_large, 415 unsupported_encoding, /// 422 validation_error, 529 overloaded, otherwise server_error pub fn new(code: u16, msg: impl Into) -> Self { let kind = match code { 400 => "invalid_request", 401 => "authentication_error", 413 => "request_too_large", 415 => "unsupported_encoding", 422 => "validation_error", 529 => "overloaded", _ => "server_error", }; ApiError { code, kind, msg: msg.into() } } pub fn kind(mut self, kind: &'static str) -> Self { self.kind = kind; self } /// prompt (+ images + thinking reserve) over the context limit, or a request larger than the whole KV cache: TypeSafe's 400 /// `max_tokens_exceeded`; the message always contains "maximum context length" (the DI kit's capacity marker -> unsupported) pub fn context(msg: impl Into) -> Self { ApiError::new(400, msg).kind("max_tokens_exceeded") } /// a rejected asset (TypeSafe: 400) pub fn asset(msg: impl Into) -> Self { ApiError::new(400, msg).kind("invalid_asset") } }