File size: 6,397 Bytes
19886eb ab0f9c6 d2d4a42 ab0f9c6 ddda5be ab0f9c6 ddda5be 19886eb ddda5be ab0f9c6 19886eb ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 ddda5be ab0f9c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | //! 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<String>,
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<Value>,
/// 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<Value>,
/// insertion order matters (probabilities are reported per criteria key; serde_json preserve_order keeps it)
pub questions: serde_json::Map<String, Value>,
/// reasoning controls: {"mode": "auto"|"on"|"off", "budget_ms": ms, "trace": bool}
#[serde(default)]
pub reasoning: Option<ReasoningReq>,
}
#[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<String>,
/// 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<f64>,
/// return each thought's decoded text in `answers[q].reasoning.trace`
#[serde(default)]
pub trace: Option<bool>,
}
/// 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<V>(pub Vec<(String, V)>);
impl<V> OrderedMap<V> {
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<V: Ser> Ser for OrderedMap<V> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
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<f32> },
Noul { noul: f32 },
/// TypeSafe `score`: ordinal levels 0..n-1 (criteria array order); legend echoes each level as sent
Score { score: f32, legend: OrderedMap<Value>, probabilities: OrderedMap<f32>, 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<f64>, 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<ReasoningOut>,
}
#[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<String>,
}
/// 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<Answer>,
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<usize>,
}
/// 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<String>) -> 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<String>) -> Self {
ApiError::new(400, msg).kind("max_tokens_exceeded")
}
/// a rejected asset (TypeSafe: 400)
pub fn asset(msg: impl Into<String>) -> Self {
ApiError::new(400, msg).kind("invalid_asset")
}
}
|