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")
    }
}