Skip to main content

cortiq_engine/
d1.rs

1//! d1 — Liquid AI's System One decision models (d1-2B on LFM3-VL): named
2//! questions over a state (text, JSON, or pictures) answered from ONE
3//! non-generating forward pass, each as a calibrated distribution.
4//!
5//! This is a port of the checkpoint's own `prompt.py`, `runner.py`,
6//! `pointer.py`, `box.py`, `grid.py`, `calibration.py`, `parsing.py` and
7//! `api.py`; names follow them. In short:
8//!
9//! * **Prompt.** `[BOS]<|im_start|>user\n[media][state]\n\n\nQUESTION:\n`
10//!   then, per question, its block, `<|im_end|>\n<|im_start|>assistant\n`,
11//!   the lead `<think></think>` and, for a typed answer, named markers
12//!   (`start:`, `present:`, `top:`, ...) whose colon tokens are the slots.
13//! * **`set` attention.** Up to `d1_set_size` questions follow one state,
14//!   each after its own `QUESTION:\n`; the full-attention layers see both
15//!   ways (the state ↔ everything, a question ↔ the state and itself), the
16//!   sliding and convolution layers stay causal (`pipeline::masked`).
17//! * **Readout.** A code slot is a softmax over its options' token groups
18//!   (each group max-pooled over its spellings) of the log-probabilities, the
19//!   untyped questions tempered by the calibration's temperature; a read's
20//!   start/end slots point at state positions by the dot product of final
21//!   hidden states (position 0, the BOS, is "nothing").
22//! * **Answers.** Through the calibration's monotone tables: `found:*`,
23//!   `multiple`, `estimate` quantile levels, box confidences (Halton draws).
24
25use crate::Pipeline;
26use crate::pipeline::masked::SetMask;
27use crate::tokenizer::Tokenizer;
28use cortiq_core::CmfModel;
29use serde_json::{Map, Value, json};
30
31const IM_START: &str = "<|im_start|>";
32const IM_END: &str = "<|im_end|>";
33const LEAD: &str = "<think></think>";
34const ZWSP: char = '\u{200b}';
35const EDGES: [&str; 4] = ["top", "bottom", "left", "right"];
36const INSTANCES: usize = 16;
37const MAX_INSTANCES: usize = 64;
38const DRAWS: usize = 512;
39
40// ─────────────────────────────── spec ───────────────────────────────
41
42/// The calibration a release ships (`calibration.json`).
43#[derive(Clone, Debug, Default)]
44pub struct Calibration {
45    default: f64,
46    temperatures: Map<String, Value>,
47    tables: Map<String, Value>,
48}
49
50/// `knots` (increasing `[raw, calibrated]` pairs), linear between, flat
51/// past the ends.
52fn through_knots(value: f64, knots: &[(f64, f64)]) -> f64 {
53    if knots.is_empty() {
54        return value;
55    }
56    let index = knots.partition_point(|&(raw, _)| raw < value);
57    if index == 0 {
58        return knots[0].1;
59    }
60    if index == knots.len() {
61        return knots[knots.len() - 1].1;
62    }
63    let ((a, pa), (b, pb)) = (knots[index - 1], knots[index]);
64    pa + (value - a) * (pb - pa) / (b - a)
65}
66
67impl Calibration {
68    fn from_json(v: &Value) -> Self {
69        Self {
70            default: v.get("default").and_then(Value::as_f64).unwrap_or(1.0),
71            temperatures: v.get("temperatures").and_then(Value::as_object).cloned().unwrap_or_default(),
72            tables: v.get("tables").and_then(Value::as_object).cloned().unwrap_or_default(),
73        }
74    }
75
76    fn knots(&self, name: &str) -> Vec<(f64, f64)> {
77        self.tables
78            .get(name)
79            .and_then(Value::as_array)
80            .map(|rows| {
81                rows.iter()
82                    .filter_map(|r| Some((r.get(0)?.as_f64()?, r.get(1)?.as_f64()?)))
83                    .collect()
84            })
85            .unwrap_or_default()
86    }
87
88    fn temperature(&self, bucket: &str) -> f64 {
89        self.temperatures.get(bucket).and_then(Value::as_f64).unwrap_or(self.default)
90    }
91
92    fn through(&self, name: &str, value: f64) -> f64 {
93        through_knots(value, &self.knots(name))
94    }
95
96    /// The level whose observed share is `share`: the table read backwards.
97    fn level(&self, name: &str, share: f64) -> f64 {
98        let flipped: Vec<(f64, f64)> = self.knots(name).into_iter().map(|(x, y)| (y, x)).collect();
99        through_knots(share, &flipped)
100    }
101}
102
103/// Everything a d1 file records beside its weights (`provenance.d1`).
104#[derive(Clone, Debug)]
105pub struct D1Spec {
106    codes: u8,
107    set_size: usize,
108    numbers: bool,
109    vision_max_pixels: usize,
110    edges_interp: bool,
111    prompt: u8,
112    calibration: Calibration,
113}
114
115/// Whether `model` is a d1 decision model.
116pub fn is_d1(model: &CmfModel) -> bool {
117    model
118        .header
119        .provenance
120        .as_ref()
121        .is_some_and(|p| p.get("d1").is_some())
122}
123
124impl D1Spec {
125    pub fn from_model(model: &CmfModel) -> Result<Self, String> {
126        let p = model
127            .header
128            .provenance
129            .as_ref()
130            .and_then(|p| p.get("d1"))
131            .ok_or("not a d1 file (no provenance.d1)")?;
132        let c = &p["config"];
133        let attention = c.get("d1_attention").and_then(Value::as_str).unwrap_or("causal");
134        if attention != "set" {
135            return Err(format!("d1: attention '{attention}' is not supported (set only)"));
136        }
137        Ok(Self {
138            codes: c.get("d1_codes").and_then(Value::as_u64).unwrap_or(1) as u8,
139            set_size: c.get("d1_set_size").and_then(Value::as_u64).unwrap_or(16) as usize,
140            numbers: c.get("d1_numbers").and_then(Value::as_bool).unwrap_or(false),
141            vision_max_pixels: c
142                .get("d1_vision_max_pixels")
143                .and_then(Value::as_u64)
144                .unwrap_or(1_638_400) as usize,
145            edges_interp: c.get("d1_box_edges").and_then(Value::as_str) == Some("interp"),
146            prompt: c.get("d1_prompt").and_then(Value::as_u64).unwrap_or(1) as u8,
147            calibration: Calibration::from_json(&p["calibration"]),
148        })
149    }
150}
151
152// ───────────────────────────── questions ─────────────────────────────
153
154#[derive(Clone, Debug, PartialEq)]
155enum ReadKind {
156    Text,
157    Number,
158    Date,
159}
160
161impl ReadKind {
162    fn name(&self) -> &'static str {
163        match self {
164            ReadKind::Text => "text",
165            ReadKind::Number => "number",
166            ReadKind::Date => "date",
167        }
168    }
169    /// A read's longest span, in tokens (`pointer.SPAN_CAPS`).
170    fn cap(&self) -> usize {
171        match self {
172            ReadKind::Text => 512,
173            _ => 32,
174        }
175    }
176}
177
178#[derive(Clone, Debug)]
179enum Question {
180    Choice { said: String, criteria: Vec<(String, Option<String>)>, multiple: bool },
181    Noul { said: String, yes_no: Option<(String, String)> },
182    Score { said: String, levels: Vec<String> },
183    Read { said: String, kind: ReadKind, unit: Option<String> },
184    Estimate { said: String, grid: Grid, unit: Option<String> },
185    Box { said: String },
186    Boxes { said: String, max: Option<usize> },
187}
188
189impl Question {
190    fn said(&self) -> &str {
191        match self {
192            Question::Choice { said, .. }
193            | Question::Noul { said, .. }
194            | Question::Score { said, .. }
195            | Question::Read { said, .. }
196            | Question::Estimate { said, .. }
197            | Question::Box { said }
198            | Question::Boxes { said, .. } => said,
199        }
200    }
201
202    /// Typed answers are read at their own slots and never tempered.
203    fn typed(&self) -> bool {
204        matches!(
205            self,
206            Question::Read { .. }
207                | Question::Estimate { .. }
208                | Question::Box { .. }
209                | Question::Boxes { .. }
210                | Question::Choice { multiple: true, .. }
211        )
212    }
213}
214
215fn py_str(v: &Value) -> String {
216    match v {
217        Value::String(s) => s.clone(),
218        Value::Null => "None".into(),
219        other => other.to_string(),
220    }
221}
222
223/// A caller's question in the API's JSON, checked as `prompt.check_question`
224/// and built as `prompt.as_question`.
225fn parse_question(q: &Value, prompt: u8) -> Result<Question, String> {
226    let obj = q.as_object().ok_or(
227        "a question is an object such as {\"type\": \"noul\", \"instructions\": \"Is it urgent?\"}",
228    )?;
229    let kind = obj.get("type").and_then(Value::as_str).unwrap_or("");
230    const TYPES: [&str; 8] = ["noul", "choice", "score", "text", "number", "date", "box", "boxes"];
231    if !TYPES.contains(&kind) {
232        return Err(format!("\"type\" is one of {}, not {kind:?}", TYPES.join(", ")));
233    }
234    let said = match obj.get("instructions") {
235        Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
236        Some(Value::Object(_)) => return Err("spoken instructions need an audio model (d1-Omni)".into()),
237        _ => return Err("\"instructions\" is the question as text".into()),
238    };
239    let criteria = obj.get("criteria");
240    if obj.get("multiple").and_then(Value::as_bool).unwrap_or(false) && kind != "choice" {
241        return Err("\"multiple\" belongs to a choice".into());
242    }
243    if obj.contains_key("max") {
244        let most = if prompt == 1 { INSTANCES } else { MAX_INSTANCES };
245        let ok = kind == "boxes"
246            && obj["max"].as_u64().is_some_and(|m| m >= 1 && m as usize <= most);
247        if !ok {
248            return Err(format!("\"max\" belongs to boxes, a whole number from 1 to {most}"));
249        }
250    }
251    if obj.contains_key("step") && !obj.contains_key("range") {
252        return Err("\"step\" needs a \"range\"".into());
253    }
254    let unit = obj.get("unit").and_then(Value::as_str).map(str::to_string);
255    if let Some(range) = obj.get("range") {
256        let bounds: Vec<f64> = range
257            .as_array()
258            .filter(|a| a.len() == 2)
259            .map(|a| a.iter().filter_map(Value::as_f64).collect())
260            .unwrap_or_default();
261        if kind != "number" || bounds.len() != 2 {
262            return Err("an estimate is {\"type\": \"number\", \"range\": [low, high]}, with an optional \"step\"".into());
263        }
264        let (low, high) = (bounds[0], bounds[1]);
265        let step = obj.get("step").and_then(Value::as_f64);
266        let even = |s: f64| ((high - low) / s - ((high - low) / s).round()).abs() <= 1e-9 * ((high - low) / s).abs().max(1.0);
267        if low >= high || step.is_some_and(|s| !(s > 0.0 && even(s))) {
268            return Err("an estimate's range runs low to high and its step, above 0, divides it".into());
269        }
270        let size = match step {
271            None => 128,
272            Some(s) => 128.min(((high - low) / s).round() as usize + 1),
273        };
274        return Ok(Question::Estimate { said, grid: Grid { low, high, size }, unit });
275    }
276    Ok(match kind {
277        "text" => Question::Read { said, kind: ReadKind::Text, unit },
278        "number" => Question::Read { said, kind: ReadKind::Number, unit },
279        "date" => Question::Read { said, kind: ReadKind::Date, unit },
280        "box" => Question::Box { said },
281        "boxes" => Question::Boxes { said, max: obj.get("max").and_then(Value::as_u64).map(|m| m as usize) },
282        "noul" => {
283            let yes_no = match criteria {
284                None | Some(Value::Null) => None,
285                Some(Value::Object(m)) => Some((
286                    py_str(m.get("true").unwrap_or(&Value::Null)),
287                    py_str(m.get("false").unwrap_or(&Value::Null)),
288                )),
289                _ => return Err("a noul's \"criteria\", when given, is {\"true\": ..., \"false\": ...}".into()),
290            };
291            Question::Noul { said, yes_no }
292        }
293        "score" => {
294            let levels: Vec<String> = criteria
295                .and_then(Value::as_array)
296                .map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect())
297                .unwrap_or_default();
298            let n = criteria.and_then(Value::as_array).map_or(0, |a| a.len());
299            if !(2..=10).contains(&n) || levels.len() != n {
300                return Err("a score's \"criteria\" lists 2 to 10 levels, lowest first".into());
301            }
302            Question::Score { said, levels }
303        }
304        _ => {
305            let m = criteria.and_then(Value::as_object).filter(|m| !m.is_empty()).ok_or(
306                "a choice's \"criteria\" names the options: {\"billing\": \"Charges and refunds\"}",
307            )?;
308            Question::Choice {
309                said,
310                criteria: m
311                    .iter()
312                    .map(|(k, v)| (k.clone(), v.as_str().map(str::to_string)))
313                    .collect(),
314                multiple: obj.get("multiple").and_then(Value::as_bool).unwrap_or(false),
315            }
316        }
317    })
318}
319
320// ─────────────────────────────── grids ───────────────────────────────
321
322#[derive(Clone, Copy, Debug, PartialEq)]
323struct Grid {
324    low: f64,
325    high: f64,
326    size: usize,
327}
328
329/// An edge's values as numbers: 0-999 thousandths.
330const THOUSANDTHS: Grid = Grid { low: 0.0, high: 0.999, size: 1000 };
331/// An edge's values as codes, 128 of them.
332const EDGE_CODES: Grid = Grid { low: 0.0, high: 1.0, size: 128 };
333
334impl Grid {
335    fn step(&self) -> f64 {
336        (self.high - self.low) / (self.size - 1) as f64
337    }
338
339    fn values(&self) -> Vec<f64> {
340        let step = self.step();
341        let mut v: Vec<f64> = (0..self.size - 1).map(|i| self.low + i as f64 * step).collect();
342        v.push(self.high);
343        v
344    }
345
346    /// Mean, median and the p10/p90 quantiles; `level` maps a quantile to
347    /// the level it is read at.
348    fn read(&self, probs: &[f64], level: Option<&dyn Fn(f64) -> f64>) -> Map<String, Value> {
349        let total: f64 = probs.iter().sum();
350        let values = self.values();
351        let mut out = Map::new();
352        out.insert(
353            "mean".into(),
354            json!(values.iter().zip(probs).map(|(v, p)| v * (p / total)).sum::<f64>()),
355        );
356        let at: Vec<(&str, f64)> = [("p10", 0.1), ("median", 0.5), ("p90", 0.9)]
357            .iter()
358            .map(|&(k, q)| (k, level.map_or(q, |f| f(q))))
359            .collect();
360        let mut mass = 0.0;
361        for (value, p) in values.iter().zip(probs) {
362            mass += p / total;
363            for &(key, quantile) in &at {
364                if !out.contains_key(key) && mass >= quantile {
365                    out.insert(key.into(), json!(*value));
366                }
367            }
368        }
369        for &(key, _) in &at {
370            out.entry(key.to_string()).or_insert(json!(values[values.len() - 1]));
371        }
372        out
373    }
374}
375
376/// Python's `format(x, "g")`: 6 significant digits, trailing zeros dropped,
377/// exponent form below 1e-4 or from 1e6.
378fn py_g(x: f64) -> String {
379    if x == 0.0 {
380        return if x.is_sign_negative() { "-0".into() } else { "0".into() };
381    }
382    if !x.is_finite() {
383        return if x.is_nan() { "nan".into() } else if x > 0.0 { "inf".into() } else { "-inf".into() };
384    }
385    let sci = format!("{:.5e}", x);
386    let (mant, exp) = sci.split_once('e').unwrap();
387    let exp: i32 = exp.parse().unwrap();
388    if !(-4..6).contains(&exp) {
389        let mant = mant.trim_end_matches('0').trim_end_matches('.');
390        return format!("{mant}e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs());
391    }
392    let decimals = (5 - exp).max(0) as usize;
393    let s = format!("{:.*}", decimals, x);
394    if s.contains('.') {
395        s.trim_end_matches('0').trim_end_matches('.').to_string()
396    } else {
397        s
398    }
399}
400
401// ───────────────────────────── tokenizer ─────────────────────────────
402
403const LETTERS: [&str; 26] = [
404    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
405    "V", "W", "X", "Y", "Z",
406];
407
408fn option_codes(labels: &[String], scheme: u8) -> Vec<String> {
409    let labs: Vec<String> = labels.iter().map(|x| x.trim().to_string()).collect();
410    if !labs.is_empty() && labs.iter().all(|k| k.chars().count() == 1 && k.chars().all(char::is_alphabetic)) {
411        return labs;
412    }
413    if labs.len() <= 26 {
414        return LETTERS[..labs.len()].iter().map(|s| s.to_string()).collect();
415    }
416    if scheme == 2 {
417        let mut all: Vec<String> = LETTERS.iter().map(|s| s.to_string()).collect();
418        for a in LETTERS {
419            for b in LETTERS {
420                all.push(format!("{a}{b}"));
421            }
422        }
423        all.truncate(labs.len());
424        return all;
425    }
426    (0..labs.len()).map(|i| format!("{i:02}")).collect()
427}
428
429struct Tok<'a> {
430    tok: &'a Tokenizer,
431    specials: Vec<String>,
432}
433
434impl<'a> Tok<'a> {
435    fn new(tok: &'a Tokenizer) -> Self {
436        let mut specials: Vec<String> = tok
437            .special_texts()
438            .into_iter()
439            .filter(|s| s.chars().count() > 1 && !s.contains(ZWSP))
440            .collect();
441        specials.sort_by_key(|s| std::cmp::Reverse(s.len()));
442        Self { tok, specials }
443    }
444
445    fn encode(&self, text: &str) -> Vec<u32> {
446        self.tok.encode(text)
447    }
448
449    fn single(&self, text: &str) -> Option<u32> {
450        let ids = self.encode(text);
451        (ids.len() == 1).then(|| ids[0])
452    }
453
454    /// The single-token encodings of `texts`, each once (`prompt._ids`).
455    fn ids(&self, texts: &[String]) -> Vec<u32> {
456        let mut out = Vec::new();
457        for t in texts {
458            if let Some(id) = self.single(t) {
459                if !out.contains(&id) {
460                    out.push(id);
461                }
462            }
463        }
464        out
465    }
466
467    /// Each label's distinct single-token code (`prompt.aliases`).
468    fn aliases(&self, labels: &[String], scheme: u8) -> Result<Vec<(String, u32)>, String> {
469        let codes = option_codes(labels, scheme);
470        let mut pool: Vec<String> = ('A'..='Z').map(|c| c.to_string()).collect();
471        pool.extend((0..100).map(|i| format!("{i:02}")));
472        pool.extend(('a'..='z').map(|c| c.to_string()));
473        pool.extend((0..200).map(|i| format!("#{i}")));
474        for a in 'A'..='Z' {
475            for b in 'A'..='Z' {
476                pool.push(format!("{a}{b}"));
477            }
478        }
479        let mut used = Vec::new();
480        let mut out = Vec::new();
481        for code in codes {
482            let mut take = |raw: &str| -> bool {
483                match self.single(raw) {
484                    Some(id) if !used.contains(&id) => {
485                        out.push((raw.to_string(), id));
486                        used.push(id);
487                        true
488                    }
489                    _ => false,
490                }
491            };
492            if take(&code) {
493                continue;
494            }
495            if !pool.iter().any(|raw| take(raw)) {
496                return Err(format!("no single-token alias left for {} options", labels.len()));
497            }
498        }
499        Ok(out)
500    }
501
502    /// `text` with every special token's string broken by a zero-width space
503    /// after its first character (`prompt.plain`), and when `offsets` the map
504    /// from each boundary of the result to the original text.
505    fn plain_offsets(&self, text: &str) -> (String, Vec<usize>) {
506        let mut text: Vec<char> = text.chars().collect();
507        let mut boundaries: Vec<usize> = (0..=text.len()).collect();
508        if self.specials.is_empty() {
509            return (text.into_iter().collect(), boundaries);
510        }
511        let pats: Vec<Vec<char>> = self.specials.iter().map(|s| s.chars().collect()).collect();
512        loop {
513            // Leftmost-first, longest alternative first (the regex alternation).
514            let mut hits = Vec::new();
515            let mut i = 0;
516            while i < text.len() {
517                let found = pats.iter().find(|p| text[i..].starts_with(p));
518                match found {
519                    Some(p) => {
520                        hits.push(i);
521                        i += p.len();
522                    }
523                    None => i += 1,
524                }
525            }
526            if hits.is_empty() {
527                break;
528            }
529            for &at in hits.iter().rev() {
530                boundaries.insert(at + 1, boundaries[at + 1]);
531                text.insert(at + 1, ZWSP);
532            }
533        }
534        (text.into_iter().collect(), boundaries)
535    }
536
537    fn plain(&self, text: &str) -> String {
538        self.plain_offsets(text).0
539    }
540
541    /// Token ids and their character offsets in `text` (byte-level: each
542    /// token's bytes mapped back to the characters they fall in).
543    fn encode_offsets(&self, text: &str) -> (Vec<u32>, Vec<(usize, usize)>) {
544        let ids = self.encode(text);
545        let char_of_byte: Vec<usize> = {
546            let mut v = vec![0usize; text.len() + 1];
547            let mut ci = 0;
548            for (bi, ch) in text.char_indices() {
549                for k in 0..ch.len_utf8() {
550                    v[bi + k] = ci;
551                }
552                ci += 1;
553            }
554            v[text.len()] = ci;
555            v
556        };
557        let mut offsets = Vec::with_capacity(ids.len());
558        let mut at = 0usize;
559        for &id in &ids {
560            let n = self.tok.token_bytes(id).len();
561            let (a, b) = (at.min(text.len()), (at + n).min(text.len()));
562            let start = char_of_byte[a];
563            let end = if b > a { char_of_byte[b - 1] + 1 } else { start };
564            offsets.push((start, end));
565            at += n;
566        }
567        (ids, offsets)
568    }
569}
570
571// ───────────────────────────── readouts ─────────────────────────────
572
573/// One answer position's readout.
574#[derive(Clone, Debug)]
575enum Slot {
576    /// A read's start or end: a pointer over state positions.
577    Position,
578    /// Option token groups; `bucket` names the calibration temperature of
579    /// an untyped question.
580    Codes { groups: Vec<Vec<u32>>, bucket: Option<String> },
581}
582
583fn bucket(kind: &str, k: usize) -> String {
584    match kind {
585        "noul" => "noul".into(),
586        "score" => format!("score:{k}"),
587        _ if k <= 3 => "choice:small".into(),
588        _ if k <= 8 => "choice:mid".into(),
589        _ if k <= 26 => "choice:large".into(),
590        _ => "choice:inventory".into(),
591    }
592}
593
594struct Proto<'a> {
595    t: Tok<'a>,
596    spec: &'a D1Spec,
597}
598
599impl Proto<'_> {
600    fn yes_no(&self) -> Result<Vec<Vec<u32>>, String> {
601        let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
602        let (yes, no) = (self.t.ids(&s(&["yes", "Yes", "YES"])), self.t.ids(&s(&["no", "No", "NO"])));
603        if yes.is_empty() || no.is_empty() {
604            return Err("tokenizer has no single-token yes/no".into());
605        }
606        Ok(vec![yes, no])
607    }
608
609    fn score_groups(&self, n: usize) -> Result<Vec<Vec<u32>>, String> {
610        let groups: Vec<Vec<u32>> = (0..n).map(|i| self.t.ids(&[i.to_string()])).collect();
611        if groups.iter().any(Vec::is_empty) {
612            return Err(format!("score with {n} levels needs single-token digits"));
613        }
614        Ok(groups)
615    }
616
617    fn choice_groups(&self, labels: &[String]) -> Result<Vec<Vec<u32>>, String> {
618        let mut groups = Vec::new();
619        for (code, tid) in self.t.aliases(labels, self.spec.codes)? {
620            let mut g = vec![tid];
621            g.extend(self.t.ids(&[format!(" {code}")]).into_iter().filter(|&i| i != tid));
622            groups.push(g);
623        }
624        Ok(groups)
625    }
626
627    /// Each grid value times `scale` as one token, plain and after a space
628    /// (`prompt.number_ids`); None when the tokenizer cannot write one so.
629    fn number_ids(&self, grid: &Grid, scale: usize) -> Option<Vec<Vec<u32>>> {
630        let mut groups = Vec::new();
631        for value in grid.values() {
632            let x = value * scale as f64;
633            let n = x.round_ties_even();
634            if (n - x).abs() > 1e-6 {
635                return None;
636            }
637            let n = n as i64;
638            let group = self.t.ids(&[n.to_string(), format!(" {n}")]);
639            if group.is_empty() || self.t.encode(&n.to_string()).len() != 1 {
640                return None;
641            }
642            groups.push(group);
643        }
644        Some(groups)
645    }
646
647    /// The labels of a grid's bins on the code readout (`grid_question`).
648    fn grid_labels(grid: &Grid) -> Vec<String> {
649        grid.values().iter().map(|&v| py_g(v)).collect()
650    }
651
652    /// A grid's slot: the tokenizer's numbers on a checkpoint that reads
653    /// them so, else digits (≤ 10 values) or option codes.
654    fn grid_slot(&self, grid: &Grid, scale: usize) -> Result<Slot, String> {
655        if self.spec.numbers {
656            if let Some(groups) = self.number_ids(grid, scale) {
657                return Ok(Slot::Codes { groups, bucket: None });
658            }
659        }
660        let labels = Self::grid_labels(grid);
661        let groups = if grid.size <= 10 {
662            self.score_groups(grid.size)?
663        } else {
664            let keys: Vec<String> = (0..labels.len()).map(|i| i.to_string()).collect();
665            self.choice_groups(&keys)?
666        };
667        Ok(Slot::Codes { groups, bucket: None })
668    }
669
670    fn uses_numbers(&self, grid: &Grid, scale: usize) -> bool {
671        self.spec.numbers && self.number_ids(grid, scale).is_some()
672    }
673
674    /// An edge's grid and scale (`box.edge_grid`).
675    fn edge_grid(&self) -> (Grid, usize) {
676        if self.uses_numbers(&THOUSANDTHS, 1000) {
677            (THOUSANDTHS, 1000)
678        } else {
679            (EDGE_CODES, 1)
680        }
681    }
682
683    fn box_slots(&self) -> Result<Vec<Slot>, String> {
684        let (grid, scale) = self.edge_grid();
685        let mut out = vec![Slot::Codes { groups: self.yes_no()?, bucket: None }];
686        for _ in EDGES {
687            out.push(self.grid_slot(&grid, scale)?);
688        }
689        Ok(out)
690    }
691
692    fn slots(&self, q: &Question) -> Result<Vec<Slot>, String> {
693        Ok(match q {
694            Question::Read { .. } => vec![Slot::Position, Slot::Position],
695            Question::Estimate { grid, .. } => vec![self.grid_slot(grid, 1)?],
696            Question::Box { .. } => self.box_slots()?,
697            Question::Boxes { max, .. } => {
698                let one = self.box_slots()?;
699                let blocks = max.unwrap_or(INSTANCES);
700                (0..blocks).flat_map(|_| one.clone()).collect()
701            }
702            Question::Choice { criteria, multiple: true, .. } => {
703                let yn = self.yes_no()?;
704                criteria.iter().map(|_| Slot::Codes { groups: yn.clone(), bucket: None }).collect()
705            }
706            Question::Noul { .. } => vec![Slot::Codes { groups: self.yes_no()?, bucket: Some(bucket("noul", 2)) }],
707            Question::Score { levels, .. } => vec![Slot::Codes {
708                groups: self.score_groups(levels.len())?,
709                bucket: Some(bucket("score", levels.len())),
710            }],
711            Question::Choice { criteria, .. } => {
712                let labels: Vec<String> = criteria.iter().map(|(k, _)| k.clone()).collect();
713                vec![Slot::Codes {
714                    groups: self.choice_groups(&labels)?,
715                    bucket: Some(bucket("choice", labels.len())),
716                }]
717            }
718        })
719    }
720
721    // ─────────────────────────── prompt text ───────────────────────────
722
723    fn edge_legend(&self) -> Result<String, String> {
724        let (grid, scale) = self.edge_grid();
725        if scale > 1 {
726            return Ok("Each edge is a number from 0 to 999, in thousandths of the picture: top and bottom from its \
727                       top edge, left and right from its left edge."
728                .into());
729        }
730        let keys: Vec<String> = (0..grid.size).map(|i| i.to_string()).collect();
731        let codes = self.t.aliases(&keys, self.spec.codes)?;
732        Ok(format!(
733            "Each edge is a code on a {}-value scale: {} is 0 (top or left), {} is 1 (bottom or right). Values are \
734             equally spaced.",
735            grid.size,
736            codes[0].0,
737            codes[codes.len() - 1].0
738        ))
739    }
740
741    fn question_block(&self, q: &Question) -> Result<String, String> {
742        let said = q.said();
743        Ok(match q {
744            Question::Box { .. } => {
745                let one = if self.edge_grid().1 > 1 { "one number" } else { "one code" };
746                format!(
747                    "{said}\n\nPresence is yes or no. {} Reply with yes or no at present, then {one} at top, bottom, \
748                     left and right.",
749                    self.edge_legend()?
750                )
751            }
752            Question::Boxes { .. } => {
753                let most = if self.spec.prompt == 1 { format!("{INSTANCES} at most, ") } else { String::new() };
754                format!(
755                    "{said}\n\nEvery one of them, {most}in reading order (left edge, then top). Present k is yes \
756                     when there are at least k. {} Reply with yes or no at each present, then its top, bottom, left \
757                     and right.",
758                    self.edge_legend()?
759                )
760            }
761            Question::Estimate { grid, unit, .. } => {
762                let unit = unit.as_deref().map(|u| format!(" {u}")).unwrap_or_default();
763                if self.uses_numbers(grid, 1) {
764                    format!(
765                        "{said}\n\nEstimate from {} to {}{unit}, in steps of {}. Reply with the number only.",
766                        py_g(grid.low),
767                        py_g(grid.high),
768                        py_g(grid.step())
769                    )
770                } else {
771                    let codes: Vec<String> = if grid.size <= 10 {
772                        (0..grid.size).map(|i| i.to_string()).collect()
773                    } else {
774                        let keys: Vec<String> = (0..grid.size).map(|i| i.to_string()).collect();
775                        self.t.aliases(&keys, self.spec.codes)?.into_iter().map(|(c, _)| c).collect()
776                    };
777                    format!(
778                        "{said}\n\nEstimate from {} to {}{unit} on {} equally spaced values, step {}: {} is {}, {} \
779                         is {}. Reply with the code only.",
780                        py_g(grid.low),
781                        py_g(grid.high),
782                        grid.size,
783                        py_g(grid.step()),
784                        codes[0],
785                        py_g(grid.low),
786                        codes[codes.len() - 1],
787                        py_g(grid.high)
788                    )
789                }
790            }
791            Question::Read { kind, unit, .. } => {
792                let unit = match (kind, unit) {
793                    (ReadKind::Number, Some(u)) => format!(" (unit: {u})"),
794                    _ => String::new(),
795                };
796                format!("{said}\n\nRead the {}{unit} from the state, or nothing if it is absent.", kind.name())
797            }
798            Question::Choice { criteria, multiple: true, .. } => {
799                let lines: Vec<String> = criteria
800                    .iter()
801                    .map(|(name, desc)| {
802                        format!("{}: {}", serde_json::to_string(name).unwrap(), desc.as_deref().unwrap_or(name))
803                    })
804                    .collect();
805                format!("{said}\n\nOptions:\n{}\n\nReply with yes or no for each option.", lines.join("\n"))
806            }
807            Question::Choice { criteria, .. } => {
808                let labels: Vec<String> = criteria.iter().map(|(k, _)| k.clone()).collect();
809                let codes = self.t.aliases(&labels, self.spec.codes)?;
810                let lines: Vec<String> = codes
811                    .iter()
812                    .zip(criteria)
813                    .map(|((code, _), (label, desc))| {
814                        let text = desc.clone().unwrap_or_else(|| label.replace('_', " "));
815                        format!("{code} {text}")
816                    })
817                    .collect();
818                format!("{said}\n\nOptions:\n{}\n\nReply with the option code only.", lines.join("\n"))
819            }
820            Question::Noul { yes_no, .. } => {
821                let extra = yes_no
822                    .as_ref()
823                    .map(|(y, n)| format!("\nYes: {y}\nNo: {n}"))
824                    .unwrap_or_default();
825                format!("{said}{extra}\n\nReply with yes or no only.")
826            }
827            Question::Score { levels, .. } => {
828                let legend: Vec<String> = levels.iter().enumerate().map(|(i, l)| format!("{i} {l}")).collect();
829                format!(
830                    "{said}\n\n{}\n\nReply with a single digit 0-{} only.",
831                    legend.join("\n"),
832                    levels.len() - 1
833                )
834            }
835        })
836    }
837
838    /// The question and the assistant header up to its answer slot(s), and
839    /// each slot's token offset from the suffix's last token
840    /// (`prompt.suffix_layout`).
841    fn suffix_layout(&self, q: &Question) -> Result<(String, Vec<isize>), String> {
842        let body = self.question_block(q)?;
843        let names: Vec<String> = match q {
844            Question::Read { .. } => vec!["start".into(), "end".into()],
845            Question::Box { .. } => ["present"].iter().chain(EDGES.iter()).map(|s| s.to_string()).collect(),
846            Question::Boxes { max, .. } => {
847                let blocks = max.unwrap_or(INSTANCES);
848                (1..=blocks)
849                    .flat_map(|k| ["present"].iter().chain(EDGES.iter()).map(move |n| format!("{n} {k}")))
850                    .collect()
851            }
852            Question::Choice { criteria, multiple: true, .. } => {
853                criteria.iter().map(|(n, _)| serde_json::to_string(n).unwrap()).collect()
854            }
855            _ => {
856                return Ok((
857                    format!("{}{IM_END}\n{IM_START}assistant\n{LEAD}", self.t.plain(&body)),
858                    vec![0],
859                ));
860            }
861        };
862        let after = match q {
863            Question::Boxes { max, .. } if max.unwrap_or(INSTANCES) < INSTANCES || self.spec.prompt > 1 => "\n",
864            _ => "",
865        };
866        self.marked_suffix(&body, &names, after)
867    }
868
869    fn marked_suffix(&self, body: &str, names: &[String], after: &str) -> Result<(String, Vec<isize>), String> {
870        let mut text = format!("{}{IM_END}\n{IM_START}assistant\n{LEAD}", self.t.plain(body));
871        let mut ends = Vec::new();
872        for (i, name) in names.iter().enumerate() {
873            if i > 0 {
874                text.push('\n');
875            }
876            text.push_str(&self.t.plain(name));
877            text.push(':');
878            ends.push(text.chars().count());
879        }
880        if !after.is_empty() {
881            let ids = self.t.encode(&format!("{text}{after}"));
882            let last = self.t.tok.decode_token(*ids.last().unwrap());
883            if last.trim_end().ends_with(':') {
884                text.push_str(after);
885            }
886        }
887        let (ids, offsets) = self.t.encode_offsets(&text);
888        let chars: Vec<char> = text.chars().collect();
889        let mut slots = Vec::new();
890        for &end in &ends {
891            let matching: Vec<(usize, usize)> = offsets
892                .iter()
893                .enumerate()
894                .filter(|(_, se)| se.0 < end && end - 1 < se.1)
895                .map(|(i, se)| (i, se.1))
896                .collect();
897            if matching.len() != 1 {
898                return Err("a slot marker must have an exact colon token position".into());
899            }
900            let (tok_i, stop) = matching[0];
901            let tail: String = chars[end..stop.min(chars.len())].iter().collect();
902            if !tail.trim().is_empty() || !self.t.tok.decode_token(ids[tok_i]).trim_end().ends_with(':') {
903                return Err("a slot marker token must end with its colon".into());
904            }
905            slots.push(tok_i as isize - ids.len() as isize + 1);
906        }
907        Ok((text, slots))
908    }
909}
910
911// ──────────────────────────── state text ────────────────────────────
912
913/// The state as the block before `QUESTION:` (`prompt.state_block`, style
914/// `json_only`): a string as written, anything else as indented JSON.
915fn state_block(state: &Value) -> String {
916    match state {
917        Value::String(s) => format!("{s}\n\n"),
918        other => format!("{}\n\n", serde_json::to_string_pretty(other).unwrap_or_default()),
919    }
920}
921
922/// RFC 6901 paths of the string values in rendered JSON and their character
923/// spans, quotes excluded (`json_spans.string_paths`).
924fn string_paths(text: &str) -> Vec<(usize, usize, String)> {
925    let chars: Vec<char> = text.chars().collect();
926    let mut out = Vec::new();
927    fn skip(c: &[char], mut at: usize) -> usize {
928        while at < c.len() && c[at].is_whitespace() {
929            at += 1;
930        }
931        at
932    }
933    fn string_end(c: &[char], at: usize) -> Option<usize> {
934        // `at` is the opening quote; returns the index after the closing one.
935        let mut i = at + 1;
936        while i < c.len() {
937            match c[i] {
938                '\\' => i += 2,
939                '"' => return Some(i + 1),
940                _ => i += 1,
941            }
942        }
943        None
944    }
945    fn value(c: &[char], at: usize, path: &str, out: &mut Vec<(usize, usize, String)>) -> Option<usize> {
946        let at = skip(c, at);
947        match c.get(at)? {
948            '{' => {
949                let mut at = skip(c, at + 1);
950                while c.get(at)? != &'}' {
951                    let end = string_end(c, at)?;
952                    let key: String = serde_json::from_str(&c[at..end].iter().collect::<String>()).ok()?;
953                    let colon = skip(c, end);
954                    if c.get(colon)? != &':' {
955                        return None;
956                    }
957                    let name = key.replace('~', "~0").replace('/', "~1");
958                    at = skip(c, value(c, colon + 1, &format!("{path}/{name}"), out)?);
959                    if c.get(at)? == &',' {
960                        at = skip(c, at + 1);
961                    }
962                }
963                Some(at + 1)
964            }
965            '[' => {
966                let mut at = skip(c, at + 1);
967                let mut index = 0;
968                while c.get(at)? != &']' {
969                    at = skip(c, value(c, at, &format!("{path}/{index}"), out)?);
970                    index += 1;
971                    if c.get(at)? == &',' {
972                        at = skip(c, at + 1);
973                    }
974                }
975                Some(at + 1)
976            }
977            '"' => {
978                let end = string_end(c, at)?;
979                out.push((at + 1, end - 1, path.to_string()));
980                Some(end)
981            }
982            _ => {
983                let mut i = at;
984                while i < c.len() && !matches!(c[i], ',' | '}' | ']') && !c[i].is_whitespace() {
985                    i += 1;
986                }
987                Some(i)
988            }
989        }
990    }
991    if value(&chars, 0, "", &mut out).is_none() {
992        out.clear();
993    }
994    out
995}
996
997/// Token positions mapped to the rendered state (`pointer.StatePositions`).
998struct StatePositions {
999    text: Vec<char>,
1000    offsets: Vec<Option<(usize, usize)>>,
1001    paths: Vec<(usize, usize, String)>,
1002}
1003
1004impl StatePositions {
1005    fn pointer_mask(&self) -> Vec<bool> {
1006        let valid: Vec<bool> = self.offsets.iter().enumerate().map(|(i, s)| i == 0 || s.is_some()).collect();
1007        let last = valid.iter().rposition(|&v| v).unwrap_or(0);
1008        valid[..=last].to_vec()
1009    }
1010
1011    fn read(&self, first: usize, last: usize) -> Result<Map<String, Value>, String> {
1012        let mut out = Map::new();
1013        if first == 0 && last == 0 {
1014            out.insert("text".into(), Value::Null);
1015            out.insert("span".into(), Value::Null);
1016            return Ok(out);
1017        }
1018        let (a, b) = match (self.offsets.get(first).copied().flatten(), self.offsets.get(last).copied().flatten()) {
1019            (Some(a), Some(b)) => (a, b),
1020            _ => return Err("pointer leaves the state text".into()),
1021        };
1022        let (mut s0, mut s1) = (a.0, b.1);
1023        while s0 < s1 && self.text[s0].is_whitespace() {
1024            s0 += 1;
1025        }
1026        while s1 > s0 && self.text[s1 - 1].is_whitespace() {
1027            s1 -= 1;
1028        }
1029        let held: Vec<&(usize, usize, String)> =
1030            self.paths.iter().filter(|(start, end, _)| *start < s1 && s0 < *end).collect();
1031        if held.len() == 1 {
1032            let (start, end, path) = held[0];
1033            let (s0, s1) = (s0.max(*start), s1.min(*end));
1034            let slice: String = self.text[s0..s1].iter().collect();
1035            let text = serde_json::from_str::<String>(&format!("\"{slice}\"")).unwrap_or(slice);
1036            out.insert("text".into(), json!(text));
1037            out.insert("span".into(), json!([s0, s1]));
1038            out.insert("path".into(), json!(path));
1039            return Ok(out);
1040        }
1041        out.insert("text".into(), json!(self.text[s0..s1].iter().collect::<String>()));
1042        out.insert("span".into(), json!([s0, s1]));
1043        Ok(out)
1044    }
1045}
1046
1047/// `pointer.state_positions`: align the prompt `full` (prefix + first
1048/// suffix) to the rendered state.
1049fn state_positions(
1050    t: &Tok,
1051    state: &Value,
1052    prefix: &str,
1053    full: &str,
1054    offsets: &[(usize, usize)],
1055) -> Result<StatePositions, String> {
1056    let full_c: Vec<char> = full.chars().collect();
1057    let prefix_len = prefix.chars().count();
1058    let (text, boundaries, start) = if state.is_null() {
1059        (String::new(), vec![0usize], prefix_len)
1060    } else {
1061        let body = state_block(state);
1062        let (escaped, boundaries) = t.plain_offsets(&body);
1063        let tail = format!("{escaped}\nQUESTION:\n");
1064        if !prefix.ends_with(&tail) {
1065            return Err("state rendering differs from the prompt prefix".into());
1066        }
1067        let start = prefix_len - tail.chars().count();
1068        let text: String = body.chars().take(body.chars().count() - 2).collect();
1069        (text, boundaries, start)
1070    };
1071    let text_len = text.chars().count();
1072    let text_c: Vec<char> = text.chars().collect();
1073    let stop = start + boundaries.partition_point(|&b| b <= text_len) - 1;
1074    let blank = |s: &[char]| s.iter().all(|c| c.is_whitespace());
1075    let mut out = Vec::with_capacity(offsets.len());
1076    for &(a, b) in offsets {
1077        let (lo, hi) = (a.max(start), b.min(stop));
1078        let mut span = None;
1079        if lo < hi && blank(&full_c[a..lo]) && blank(&full_c[hi..b.max(hi)]) {
1080            let (left, right) = (boundaries[lo - start], boundaries[hi - start]);
1081            if left < right && right <= text_len && !blank(&text_c[left..right]) {
1082                span = Some((left, right));
1083            }
1084        }
1085        out.push(span);
1086    }
1087    if out.first().is_none_or(|s| s.is_some()) {
1088        return Err("position 0 must be a template token for the nothing readout".into());
1089    }
1090    let paths = if !state.is_null() && !state.is_string() { string_paths(&text) } else { Vec::new() };
1091    Ok(StatePositions { text: text_c, offsets: out, paths })
1092}
1093
1094// ─────────────────────────────── boxes ───────────────────────────────
1095
1096fn halton() -> Vec<[f64; 4]> {
1097    let radical = |mut i: usize, base: usize| {
1098        let (mut value, mut scale) = (0.0, 1.0);
1099        while i > 0 {
1100            let digit = i % base;
1101            i /= base;
1102            scale /= base as f64;
1103            value += digit as f64 * scale;
1104        }
1105        value
1106    };
1107    (1..=DRAWS).map(|i| [radical(i, 2), radical(i, 3), radical(i, 5), radical(i, 7)]).collect()
1108}
1109
1110fn grid_of(row: &[f64]) -> Grid {
1111    if row.len() == THOUSANDTHS.size { THOUSANDTHS } else { EDGE_CODES }
1112}
1113
1114fn ordered(a: f64, b: f64, grid: &Grid) -> (f64, f64) {
1115    if a < b {
1116        return (a, b);
1117    }
1118    let half = grid.step() / 2.0;
1119    let middle = (1.0 - half).min(half.max((a + b) / 2.0));
1120    (middle - half, middle + half)
1121}
1122
1123fn rectangle(e: [f64; 4], grid: &Grid) -> [f64; 4] {
1124    let (top, bottom) = ordered(e[0], e[1], grid);
1125    let (left, right) = ordered(e[2], e[3], grid);
1126    [top, bottom, left, right]
1127}
1128
1129fn iou(a: &[f64; 4], b: &[f64; 4]) -> f64 {
1130    let area = |x: &[f64; 4]| (x[1] - x[0]) * (x[3] - x[2]);
1131    let overlap = (a[1].min(b[1]) - a[0].max(b[0])).max(0.0) * (a[3].min(b[3]) - a[2].max(b[2])).max(0.0);
1132    overlap / (area(a) + area(b) - overlap)
1133}
1134
1135fn crossing(probs: &[f64], grid: &Grid) -> f64 {
1136    let total: f64 = probs.iter().sum();
1137    let mut mass = 0.0;
1138    for (value, p) in grid.values().iter().zip(probs) {
1139        let share = p / total;
1140        if share > 0.0 && mass + share >= 0.5 {
1141            return grid.high.min(grid.low.max(value - grid.step() / 2.0 + (0.5 - mass) / share * grid.step()));
1142        }
1143        mass += share;
1144    }
1145    grid.high
1146}
1147
1148fn medians(edges: &[Vec<f64>], interp: bool) -> [f64; 4] {
1149    let grid = grid_of(&edges[0]);
1150    let at = |row: &Vec<f64>| {
1151        if interp {
1152            crossing(row, &grid)
1153        } else {
1154            grid.read(row, None)["median"].as_f64().unwrap()
1155        }
1156    };
1157    rectangle([at(&edges[0]), at(&edges[1]), at(&edges[2]), at(&edges[3])], &grid)
1158}
1159
1160fn box_confidence(edges: &[Vec<f64>], b: &[f64; 4]) -> f64 {
1161    let grid = grid_of(&edges[0]);
1162    let values = grid.values();
1163    let cdfs: Vec<Vec<f64>> = edges
1164        .iter()
1165        .map(|row| {
1166            let total: f64 = row.iter().sum();
1167            let mut acc = 0.0;
1168            row.iter()
1169                .map(|p| {
1170                    acc += p / total;
1171                    acc
1172                })
1173                .collect()
1174        })
1175        .collect();
1176    let hits = halton()
1177        .iter()
1178        .filter(|point| {
1179            let mut e = [0f64; 4];
1180            for (k, (cdf, &u)) in cdfs.iter().zip(point.iter()).enumerate() {
1181                let i = cdf.partition_point(|&c| c < u).min(grid.size - 1);
1182                e[k] = values[i];
1183            }
1184            iou(&rectangle(e, &grid), b) >= 0.5
1185        })
1186        .count();
1187    hits as f64 / DRAWS as f64
1188}
1189
1190fn box_json(b: &[f64; 4]) -> Value {
1191    json!({"top": b[0], "bottom": b[1], "left": b[2], "right": b[3]})
1192}
1193
1194// ─────────────────────────────── API ───────────────────────────────
1195
1196/// One request: a state (string, any JSON, or null with media alone), named
1197/// questions, image sources (path, URL or `data:`) and, on d1-Omni, audio
1198/// clips (path, URL, `data:` or `{"data": base64}`), pictures first.
1199pub struct Request {
1200    pub state: Value,
1201    pub questions: Map<String, Value>,
1202    pub images: Vec<Value>,
1203    pub audios: Vec<Value>,
1204}
1205
1206/// A d1 engine over a loaded pipeline.
1207pub struct D1 {
1208    spec: D1Spec,
1209}
1210
1211/// A probability readout of one question.
1212enum Probs {
1213    Codes(Vec<Vec<f64>>),
1214    Read(Map<String, Value>),
1215}
1216
1217fn softmax(scores: &[f64]) -> Vec<f64> {
1218    let m = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1219    let exps: Vec<f64> = scores.iter().map(|s| (s - m).exp()).collect();
1220    let total: f64 = exps.iter().sum();
1221    exps.iter().map(|e| e / total).collect()
1222}
1223
1224fn log_softmax(x: &[f64]) -> Vec<f64> {
1225    let m = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1226    if !m.is_finite() {
1227        return x.to_vec();
1228    }
1229    let lse = m + x.iter().map(|v| (v - m).exp()).sum::<f64>().ln();
1230    x.iter().map(|v| v - lse).collect()
1231}
1232
1233impl D1 {
1234    pub fn from_model(model: &CmfModel) -> Result<Self, String> {
1235        Ok(Self { spec: D1Spec::from_model(model)? })
1236    }
1237
1238    /// `system_one`: `{"answers": {name: answer}, "usage": {...}}`.
1239    pub fn system_one(&self, pipeline: &Pipeline, req: &Request) -> Result<Value, String> {
1240        let p = Proto { t: Tok::new(&pipeline.tokenizer), spec: &self.spec };
1241        let mut questions = Vec::new();
1242        for (name, q) in &req.questions {
1243            let q = parse_question(q, self.spec.prompt).map_err(|e| format!("question {name:?}: {e}"))?;
1244            questions.push((name.clone(), q));
1245        }
1246        if questions.is_empty() {
1247            return Err("no questions".into());
1248        }
1249        if req.state.is_null() && req.images.is_empty() && req.audios.is_empty() {
1250            return Err("a request needs a state, media, or both".into());
1251        }
1252        let boxes = questions.iter().any(|(_, q)| matches!(q, Question::Box { .. } | Question::Boxes { .. }));
1253        if boxes && req.images.len() != 1 {
1254            return Err(format!(
1255                "a box or boxes question reads exactly one picture; this request has {}",
1256                req.images.len()
1257            ));
1258        }
1259        // Media: each picture capped to the checkpoint's pixel budget, its
1260        // processor expansion at the head of the user turn.
1261        let mut markup = String::new();
1262        let mut media_rows: Vec<f32> = Vec::new();
1263        if !req.images.is_empty() {
1264            let model = pipeline.model.as_ref().ok_or("d1 media need a file-backed pipeline")?;
1265            let tower = crate::lfm3_vl::Lfm3VlTower::from_model(model, crate::pool::Pool::from_env())?;
1266            for src in &req.images {
1267                let frame = crate::media::decode_rgb(&crate::media::load_image_bytes(src)?)?;
1268                let frame = cap_pixels(&frame, self.spec.vision_max_pixels);
1269                let input = crate::lfm3_vl::prepare_image(&frame, &tower.cfg, None)?;
1270                media_rows.extend(tower.forward(&input)?);
1271                markup.push_str("<|vision_start|>");
1272                markup.push_str(&"<|image|>".repeat(input.tokens()));
1273                markup.push_str("<|vision_end|>");
1274            }
1275        }
1276        // Audio clips (d1-Omni): the speech encoder's rows, one <|audio|>
1277        // per encoder token inside the template's vision markers.
1278        if !req.audios.is_empty() {
1279            let model = pipeline.model.as_ref().ok_or("d1 media need a file-backed pipeline")?;
1280            if !crate::lfm3_audio::has_audio(model) {
1281                return Err("this d1 file takes no audio (d1-Omni does)".into());
1282            }
1283            let enc = crate::lfm3_audio::Lfm3AudioEncoder::from_model(model, crate::pool::Pool::from_env())?;
1284            for src in &req.audios {
1285                let rows = enc.rows(&crate::egemma2_audio::load_audio_source(src)?)?;
1286                markup.push_str("<|vision_start|>");
1287                markup.push_str(&"<|audio|>".repeat(rows.len() / enc.text_hidden));
1288                markup.push_str("<|vision_end|>");
1289                media_rows.extend(rows);
1290            }
1291        }
1292        let bos = pipeline
1293            .tokenizer
1294            .bos_token_id
1295            .map(|id| pipeline.tokenizer.decode_for_protocol(&[id]))
1296            .unwrap_or_default();
1297        let header = if req.state.is_null() { "" } else { "QUESTION:\n" };
1298        let prefix = if req.state.is_null() {
1299            format!("{bos}{IM_START}user\n{markup}")
1300        } else {
1301            format!("{bos}{IM_START}user\n{markup}{}\n{header}", p.t.plain(&state_block(&req.state)))
1302        };
1303        let layouts: Vec<(String, Vec<isize>)> =
1304            questions.iter().map(|(_, q)| p.suffix_layout(q)).collect::<Result<_, _>>()?;
1305        let slot_kinds: Vec<Vec<Slot>> = questions.iter().map(|(_, q)| p.slots(q)).collect::<Result<_, _>>()?;
1306        let mut probs: Vec<Option<Probs>> = (0..questions.len()).map(|_| None).collect();
1307        let mut read_tokens = 0usize;
1308        let step = self.spec.set_size.max(1);
1309        for k0 in (0..questions.len()).step_by(step) {
1310            let k1 = (k0 + step).min(questions.len());
1311            let first_text = format!("{prefix}{}", layouts[k0].0);
1312            if std::env::var_os("CMF_D1_DEBUG").is_some() {
1313                eprintln!("d1: prompt {first_text:?}");
1314            }
1315            let (mut ids, offsets) = p.t.encode_offsets(&first_text);
1316            let state_len = ids.len() - p.t.encode(&layouts[k0].0).len();
1317            let mut group = vec![-1i32; state_len];
1318            group.extend(std::iter::repeat_n(1, ids.len() - state_len));
1319            let mut ends: Vec<Vec<usize>> =
1320                vec![layouts[k0].1.iter().map(|o| (ids.len() as isize - 1 + o) as usize).collect()];
1321            for (j, k) in (k0 + 1..k1).enumerate() {
1322                let own = p.t.encode(&format!("{header}{}", layouts[k].0));
1323                ids.extend(&own);
1324                group.extend(std::iter::repeat_n(j as i32 + 2, own.len()));
1325                ends.push(layouts[k].1.iter().map(|o| (ids.len() as isize - 1 + o) as usize).collect());
1326            }
1327            read_tokens += ids.len();
1328            let needs_positions = (k0..k1).any(|k| matches!(questions[k].1, Question::Read { .. }));
1329            let positions = if needs_positions {
1330                Some(state_positions(&p.t, &req.state, &prefix, &first_text, &offsets)?)
1331            } else {
1332                None
1333            };
1334            // Prompt rows: the embedding, the tower's rows at the pictures.
1335            let h = pipeline.hidden_size;
1336            let image_id = p.t.single("<|image|>").ok_or("tokenizer lacks <|image|>")?;
1337            let audio_id = p.t.single("<|audio|>");
1338            let mut rows = Vec::with_capacity(ids.len() * h);
1339            let mut at = 0usize;
1340            for &id in &ids {
1341                if (id == image_id || Some(id) == audio_id) && !media_rows.is_empty() {
1342                    rows.extend_from_slice(media_rows.get(at..at + h).ok_or("picture rows run short")?);
1343                    at += h;
1344                } else {
1345                    rows.extend(pipeline.embed_id(id));
1346                }
1347            }
1348            if at != media_rows.len() {
1349                return Err("picture placeholders and rows differ".into());
1350            }
1351            // Every code slot of the group through the head in one GEMM,
1352            // on the device with the forward where it runs.
1353            let mut code_at: Vec<usize> = Vec::new();
1354            for (j, k) in (k0..k1).enumerate() {
1355                if !matches!(questions[k].1, Question::Read { .. }) {
1356                    code_at.extend(&ends[j]);
1357                }
1358            }
1359            code_at.sort_unstable();
1360            code_at.dedup();
1361            let t_fwd = std::time::Instant::now();
1362            let (hidden, logz_all) =
1363                pipeline.forward_masked_head(&rows, &SetMask { state: state_len, group: &group }, &code_at)?;
1364            if std::env::var_os("CMF_D1_DEBUG").is_some() {
1365                eprintln!(
1366                    "d1: forward {} tokens + head over {} slots in {:.0} ms",
1367                    ids.len(),
1368                    code_at.len(),
1369                    t_fwd.elapsed().as_secs_f64() * 1e3
1370                );
1371            }
1372            let vocab = pipeline.vocab_size;
1373            let logz_of = |pos: usize| {
1374                let r = code_at.binary_search(&pos).expect("code slot");
1375                &logz_all[r * vocab..(r + 1) * vocab]
1376            };
1377            for (j, k) in (k0..k1).enumerate() {
1378                let q = &questions[k].1;
1379                let slots = &slot_kinds[k];
1380                if slots.len() != ends[j].len() {
1381                    return Err("slot layout and readouts disagree".into());
1382                }
1383                if let Question::Read { kind, .. } = q {
1384                    let pos = positions.as_ref().unwrap();
1385                    let mask = pos.pointer_mask();
1386                    let width = mask.len();
1387                    let logits: Vec<Vec<f64>> = ends[j]
1388                        .iter()
1389                        .map(|&slot| {
1390                            let qv = &hidden[slot * h..(slot + 1) * h];
1391                            (0..width)
1392                                .map(|i| {
1393                                    if !mask[i] {
1394                                        return f64::NEG_INFINITY;
1395                                    }
1396                                    let kv = &hidden[i * h..(i + 1) * h];
1397                                    let dot: f32 = qv.iter().zip(kv).map(|(a, b)| a * b).sum();
1398                                    (dot / (h as f32).sqrt()) as f64
1399                                })
1400                                .collect()
1401                        })
1402                        .collect();
1403                    let (first, last, found) = span(&logits[0], &logits[1], kind.cap());
1404                    let mut read = pos.read(first, last)?;
1405                    read.insert("found".into(), json!(found));
1406                    probs[k] = Some(Probs::Read(read));
1407                    continue;
1408                }
1409                let mut rows_p = Vec::new();
1410                for (slot, &end) in slots.iter().zip(&ends[j]) {
1411                    let Slot::Codes { groups, bucket } = slot else {
1412                        return Err("a pointer slot outside a read".into());
1413                    };
1414                    let logz = logz_of(end);
1415                    let mut scores: Vec<f64> = groups
1416                        .iter()
1417                        .map(|g| g.iter().map(|&i| logz[i as usize] as f64).fold(f64::NEG_INFINITY, f64::max))
1418                        .collect();
1419                    if let (Some(b), false) = (bucket, q.typed()) {
1420                        let temp = self.spec.calibration.temperature(b);
1421                        for s in &mut scores {
1422                            *s /= temp;
1423                        }
1424                    }
1425                    rows_p.push(softmax(&scores));
1426                }
1427                probs[k] = Some(Probs::Codes(rows_p));
1428            }
1429        }
1430        let context = |q: &Question| match q {
1431            Question::Read { .. } if !req.state.is_null() => {
1432                let b = state_block(&req.state);
1433                b[..b.len() - 2].to_string()
1434            }
1435            _ => String::new(),
1436        };
1437        let debug = std::env::var_os("CMF_D1_DEBUG").is_some();
1438        let mut answers = Map::new();
1439        for ((name, q), pr) in questions.iter().zip(probs) {
1440            let pr = pr.ok_or("unanswered question")?;
1441            if debug {
1442                match &pr {
1443                    Probs::Codes(rows) => eprintln!("d1: {name}: {rows:?}"),
1444                    Probs::Read(r) => eprintln!("d1: {name}: {r:?}"),
1445                }
1446            }
1447            answers.insert(name.clone(), self.answer(q, pr, &context(q))?);
1448        }
1449        Ok(json!({"answers": answers, "usage": {"input_tokens": read_tokens, "output_tokens": 0}}))
1450    }
1451
1452    /// `api.answer`: a question's typed answer from its readouts.
1453    fn answer(&self, q: &Question, probs: Probs, context: &str) -> Result<Value, String> {
1454        let cal = &self.spec.calibration;
1455        Ok(match (q, probs) {
1456            (Question::Read { kind, unit, .. }, Probs::Read(r)) => {
1457                let raw = r["found"].as_f64().unwrap_or(0.0);
1458                let found = cal.through(&format!("found:{}", kind.name()), raw);
1459                let present = found >= 0.5;
1460                let text = if present { r.get("text").cloned().unwrap_or(Value::Null) } else { Value::Null };
1461                let span = if present { r.get("span").cloned().unwrap_or(Value::Null) } else { Value::Null };
1462                let mut out = Map::new();
1463                out.insert("type".into(), json!(kind.name()));
1464                let (mut value, mut unit_out) = (text.clone(), unit.clone().map(Value::String).unwrap_or(Value::Null));
1465                if let Some(s) = text.as_str() {
1466                    match kind {
1467                        ReadKind::Number => {
1468                            if unit.is_none() {
1469                                unit_out = crate::d1_parse::written_unit(s).map(Value::String).unwrap_or(Value::Null);
1470                            }
1471                            value = crate::d1_parse::parse_number(s, context, unit.as_deref());
1472                        }
1473                        ReadKind::Date => value = crate::d1_parse::parse_date(s, context).0.map(Value::String).unwrap_or(Value::Null),
1474                        ReadKind::Text => {}
1475                    }
1476                }
1477                out.insert(kind.name().into(), value);
1478                out.insert("span".into(), span);
1479                out.insert("found".into(), json!(found));
1480                if *kind != ReadKind::Text {
1481                    out.insert("text".into(), text);
1482                }
1483                if present {
1484                    if let Some(path) = r.get("path") {
1485                        out.insert("path".into(), path.clone());
1486                    }
1487                }
1488                if *kind == ReadKind::Number {
1489                    out.insert("unit".into(), unit_out);
1490                }
1491                Value::Object(out)
1492            }
1493            (Question::Box { .. }, Probs::Codes(rows)) => {
1494                let found = cal.through("found:box", rows[0][0]);
1495                if found < 0.5 {
1496                    json!({"type": "box", "box": null, "found": found, "confidence": 0.0})
1497                } else {
1498                    let b = medians(&rows[1..5], self.spec.edges_interp);
1499                    let conf = cal.through("box", box_confidence(&rows[1..5], &b));
1500                    json!({"type": "box", "box": box_json(&b), "found": found, "confidence": conf})
1501                }
1502            }
1503            (Question::Boxes { .. }, Probs::Codes(rows)) => {
1504                let mut presence = Vec::new();
1505                let mut below = 1.0f64;
1506                for k in (0..rows.len()).step_by(5) {
1507                    below = below.min(cal.through("found:boxes", rows[k][0]));
1508                    presence.push(below);
1509                }
1510                let mut out: Vec<([f64; 4], f64, f64)> = Vec::new();
1511                for (k, &found) in presence.iter().enumerate() {
1512                    if found < 0.5 {
1513                        break;
1514                    }
1515                    let block = &rows[5 * k + 1..5 * k + 5];
1516                    let b = medians(block, self.spec.edges_interp);
1517                    out.push((b, found, cal.through("boxes", box_confidence(block, &b))));
1518                }
1519                out.sort_by(|a, b| (a.0[2], a.0[0]).partial_cmp(&(b.0[2], b.0[0])).unwrap());
1520                let count = out.len();
1521                json!({
1522                    "type": "boxes",
1523                    "boxes": out.iter().map(|(b, f, c)| json!({"box": box_json(b), "found": f, "confidence": c})).collect::<Vec<_>>(),
1524                    "count": count,
1525                    "at_least": count > 0 && count == presence.len(),
1526                    "presence": presence,
1527                })
1528            }
1529            (Question::Estimate { grid, unit, .. }, Probs::Codes(rows)) => {
1530                let level = |share: f64| cal.level("estimate", share);
1531                let has = !cal.knots("estimate").is_empty();
1532                let v = grid.read(&rows[0], if has { Some(&level) } else { None });
1533                json!({"type": "number", "number": v["median"], "interval": [v["p10"], v["p90"]], "unit": unit})
1534            }
1535            (Question::Noul { .. }, Probs::Codes(rows)) => json!({"type": "noul", "noul": rows[0][0]}),
1536            (Question::Choice { criteria, multiple: true, .. }, Probs::Codes(rows)) => {
1537                let ps: Vec<f64> = rows.iter().map(|r| cal.through("multiple", r[0])).collect();
1538                let names: Vec<&String> = criteria.iter().map(|(n, _)| n).collect();
1539                let chosen: Vec<&String> = names.iter().zip(&ps).filter(|(_, p)| **p >= 0.5).map(|(n, _)| *n).collect();
1540                let probs: Map<String, Value> = names.iter().zip(&ps).map(|(n, p)| ((*n).clone(), json!(p))).collect();
1541                json!({"type": "choice", "choices": chosen, "probabilities": probs})
1542            }
1543            (Question::Choice { criteria, .. }, Probs::Codes(rows)) => {
1544                let ps = &rows[0];
1545                let best = argmax(ps);
1546                let probs: Map<String, Value> = criteria.iter().zip(ps).map(|((n, _), p)| (n.clone(), json!(p))).collect();
1547                json!({"type": "choice", "choice": criteria[best].0, "confidence": ps[best], "probabilities": probs})
1548            }
1549            (Question::Score { levels, .. }, Probs::Codes(rows)) => {
1550                let ps = &rows[0];
1551                let best = argmax(ps);
1552                let total: f64 = ps.iter().sum();
1553                let mut mass = 0.0;
1554                let mut level = levels.len() - 1;
1555                for (i, p) in ps.iter().enumerate() {
1556                    mass += p / total;
1557                    if mass >= 0.5 {
1558                        level = i;
1559                        break;
1560                    }
1561                }
1562                let probs: Map<String, Value> = ps.iter().enumerate().map(|(i, p)| (i.to_string(), json!(p))).collect();
1563                let legend: Map<String, Value> =
1564                    levels.iter().enumerate().map(|(i, l)| (i.to_string(), json!(l))).collect();
1565                json!({"type": "score", "score": level as f64, "confidence": ps[best], "probabilities": probs, "legend": legend})
1566            }
1567            _ => return Err("readout and question disagree".into()),
1568        })
1569    }
1570}
1571
1572fn argmax(v: &[f64]) -> usize {
1573    let mut best = 0;
1574    for (i, &x) in v.iter().enumerate() {
1575        if x > v[best] {
1576            best = i;
1577        }
1578    }
1579    best
1580}
1581
1582/// `pointer.spans` for one read: the likeliest span within `cap` tokens of
1583/// its start/end scores over state positions, `(0, 0)` when not found.
1584fn span(start_logits: &[f64], end_logits: &[f64], cap: usize) -> (usize, usize, f64) {
1585    let start = log_softmax(start_logits);
1586    let end = log_softmax(end_logits);
1587    let found = -start[0].exp_m1();
1588    let t = start.len();
1589    if t == 1 {
1590        return (0, 0, found);
1591    }
1592    let cap = cap.min(t - 1);
1593    let mut best = (f64::NEG_INFINITY, 1usize, 1usize);
1594    for i in 1..t {
1595        // max over end[i..i+cap] (first index of the max on ties)
1596        let mut mj = i;
1597        for j in i..(i + cap).min(t) {
1598            if end[j] > end[mj] {
1599                mj = j;
1600            }
1601        }
1602        let score = start[i] + end[mj];
1603        if score > best.0 {
1604            best = (score, i, mj);
1605        }
1606    }
1607    if found < 0.5 {
1608        return (0, 0, found);
1609    }
1610    (best.1, best.2, found)
1611}
1612
1613/// `media.cap_pixels`: a picture larger than `max_pixels`, scaled down
1614/// (PIL bicubic, `int` sizes).
1615fn cap_pixels(frame: &crate::media::RgbFrame, max_pixels: usize) -> crate::media::RgbFrame {
1616    let (w, h) = (frame.width, frame.height);
1617    if w * h <= max_pixels {
1618        return frame.clone();
1619    }
1620    let scale = (max_pixels as f64 / (w * h) as f64).sqrt();
1621    let (nw, nh) = (((w as f64 * scale) as usize).max(1), ((h as f64 * scale) as usize).max(1));
1622    crate::egemma2_vision::resize_bicubic_aa(frame, nh, nw)
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627    use super::*;
1628
1629    #[test]
1630    fn py_g_matches_python() {
1631        for (x, want) in [
1632            (0.0, "0"),
1633            (10.0, "10"),
1634            (0.5, "0.5"),
1635            (0.999, "0.999"),
1636            (1234567.0, "1.23457e+06"),
1637            (0.00001, "1e-05"),
1638            (0.1111111, "0.111111"),
1639            (100000.0, "100000"),
1640            (-2.5, "-2.5"),
1641        ] {
1642            assert_eq!(py_g(x), want, "{x}");
1643        }
1644    }
1645
1646    #[test]
1647    fn option_codes_follow_the_schemes() {
1648        let labels = |n: usize| (0..n).map(|i| format!("o{i}")).collect::<Vec<_>>();
1649        assert_eq!(option_codes(&labels(3), 2), vec!["A", "B", "C"]);
1650        assert_eq!(option_codes(&labels(28), 2)[26..], ["AA".to_string(), "AB".to_string()]);
1651        assert_eq!(option_codes(&labels(28), 1)[27], "27");
1652        assert_eq!(option_codes(&["x".into(), "y".into()], 2), vec!["x", "y"]);
1653    }
1654
1655    #[test]
1656    fn grid_reads_quantiles() {
1657        let g = Grid { low: 0.0, high: 10.0, size: 11 };
1658        let mut p = vec![0.0; 11];
1659        p[2] = 0.6;
1660        p[3] = 0.4;
1661        let r = g.read(&p, None);
1662        assert_eq!(r["median"], json!(2.0));
1663        assert_eq!(r["p90"], json!(3.0));
1664        assert_eq!(r["p10"], json!(2.0));
1665    }
1666
1667    #[test]
1668    fn string_paths_of_rendered_json() {
1669        let text = "{\n  \"customer\": {\n    \"name\": \"Maria\"\n  },\n  \"m\": [\n    \"a\",\n    1\n  ]\n}";
1670        let p = string_paths(text);
1671        assert_eq!(p.len(), 2);
1672        assert_eq!(p[0].2, "/customer/name");
1673        assert_eq!(p[1].2, "/m/0");
1674    }
1675
1676    #[test]
1677    fn span_picks_the_best_pair_or_nothing() {
1678        let ninf = f64::NEG_INFINITY;
1679        let (a, b, f) = span(&[-5.0, 3.0, 0.0, ninf], &[-5.0, 0.0, 3.0, ninf], 32);
1680        assert_eq!((a, b), (1, 2));
1681        assert!(f > 0.5);
1682        let (a, b, _) = span(&[5.0, -3.0, -3.0], &[5.0, -3.0, -3.0], 32);
1683        assert_eq!((a, b), (0, 0));
1684    }
1685}