use crate::engine::Attachments;
use crate::lora::LoraAdapter;
use crate::video::RgbImage;
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokenizers::Tokenizer;
pub const MAX_OPTIONS: usize = 256;
pub const ASSET_FILES: &[&str] = &[
"config.json",
"judge_config.json",
"calibration.json",
"head.safetensors",
"adapter/adapter_config.json",
"adapter/adapter_model.safetensors",
"adapter_vllm/decision_head.json",
];
pub const LM_ADAPTER_FILES: &[&str] = &["adapter_vllm/adapter_config.json", "adapter_vllm/adapter_model.safetensors"];
pub fn has_assets(dir: &Path) -> bool {
dir.is_dir() && (dir.join("judge_config.json").is_file() || dir.join("adapter_vllm/decision_head.json").is_file())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DecisionKind {
Noul,
Score,
Choice,
}
impl DecisionKind {
pub fn parse(s: &str) -> Option<Self> {
Some(match s {
"noul" => Self::Noul,
"score" => Self::Score,
"choice" => Self::Choice,
_ => return None,
})
}
pub fn as_str(self) -> &'static str {
match self {
Self::Noul => "noul",
Self::Score => "score",
Self::Choice => "choice",
}
}
fn index(self) -> usize {
self as usize
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImageSource {
Path(PathBuf),
Bytes(Vec<u8>),
Url(String),
#[cfg(feature = "oxideav")]
Frame(Box<crate::av::Picture>),
}
impl ImageSource {
pub fn parse(s: &str) -> Result<Self> {
if let Some(d) = s.strip_prefix("data:") {
let (_, b64) = d
.split_once(";base64,")
.context("image data URL: only base64 data URLs are supported")?;
return Ok(Self::Bytes(crate::b64::decode(b64).context("image data URL: bad base64")?));
}
if s.starts_with("http://") || s.starts_with("https://") {
return Ok(Self::Url(s.to_string()));
}
if s.is_empty() {
bail!("empty image reference");
}
Ok(Self::Path(PathBuf::from(s.strip_prefix("file://").unwrap_or(s))))
}
pub fn bytes(&self) -> Result<Vec<u8>> {
match self {
Self::Path(p) => crate::fsx::read(p).with_context(|| format!("read image {}", p.display())),
Self::Bytes(b) => Ok(b.clone()),
#[cfg(feature = "hf")]
Self::Url(u) => crate::hf::http_get(u),
#[cfg(not(feature = "hf"))]
Self::Url(u) => bail!("{u}: built without the `hf` feature, so image URLs cannot be fetched"),
#[cfg(feature = "oxideav")]
Self::Frame(f) => {
let img = f.to_rgb()?;
Ok(crate::png::encode_rgb(img.width, img.height, &img.data))
}
}
}
pub fn to_rgb_all(images: &[ImageSource]) -> Result<Vec<RgbImage>> {
let encoded = images
.iter()
.filter(|s| !s.is_frame())
.map(|s| s.bytes())
.collect::<Result<Vec<_>>>()?;
let mut decoded = crate::mm::decode_images(&encoded)?.into_iter();
images
.iter()
.map(|s| match s {
#[cfg(feature = "oxideav")]
Self::Frame(f) => f.to_rgb(),
_ => Ok(decoded.next().expect("one decoded image per encoded one")),
})
.collect()
}
fn is_frame(&self) -> bool {
#[cfg(feature = "oxideav")]
if let Self::Frame(_) = self {
return true;
}
false
}
pub fn to_part(&self) -> Value {
let s = match self {
Self::Path(p) => p.display().to_string(),
Self::Bytes(b) => format!("data:application/octet-stream;base64,{}", crate::b64::encode(b)),
Self::Url(u) => u.clone(),
#[cfg(feature = "oxideav")]
Self::Frame(_) => match self.bytes() {
Ok(png) => format!("data:image/png;base64,{}", crate::b64::encode(&png)),
Err(e) => format!("(unconvertible picture: {e})"),
},
};
json!({ "image": s })
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum StatePart {
Text(String),
Image(ImageSource),
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct DecisionRequest {
pub kind: DecisionKind,
pub state: Value,
pub question: String,
pub options: Vec<String>,
pub attachments: Attachments,
}
impl DecisionRequest {
pub fn new(kind: DecisionKind, state: impl Into<Value>, question: impl Into<String>, options: Vec<String>) -> Self {
Self {
kind,
state: state.into(),
question: question.into(),
options,
attachments: Attachments::default(),
}
}
pub fn with_images<P: Into<PathBuf>>(mut self, images: impl IntoIterator<Item = P>) -> Self {
self.attachments.images.extend(images.into_iter().map(Into::into));
self
}
#[cfg(feature = "oxideav")]
pub fn with_image_frames(mut self, images: impl IntoIterator<Item = crate::av::Picture>) -> Self {
self.attachments.image_frames.extend(images);
self
}
pub fn noul(state: impl Into<Value>, question: impl Into<String>) -> Self {
Self::new(DecisionKind::Noul, state, question, Vec::new())
}
pub fn score(state: impl Into<Value>, question: impl Into<String>) -> Self {
Self::new(DecisionKind::Score, state, question, Vec::new())
}
pub fn choice<S: Into<String>>(state: impl Into<Value>, question: impl Into<String>, options: impl IntoIterator<Item = S>) -> Self {
Self::new(DecisionKind::Choice, state, question, options.into_iter().map(Into::into).collect())
}
pub fn from_json(v: &Value) -> Result<Self> {
let o = v.as_object().context("decision request: expected a JSON object")?;
let kind = o
.get("kind")
.and_then(Value::as_str)
.context("decision request: missing \"kind\"")?;
let kind = DecisionKind::parse(kind).with_context(|| format!("decision request: unknown kind {kind:?} (noul, score, choice)"))?;
let question = o
.get("question")
.and_then(Value::as_str)
.context("decision request: missing \"question\"")?;
let options = match o.get("options") {
None | Some(Value::Null) => Vec::new(),
Some(Value::Array(a)) => a
.iter()
.map(|x| x.as_str().map(str::to_string).context("decision request: options must be strings"))
.collect::<Result<_>>()?,
Some(_) => bail!("decision request: \"options\" must be a list of strings"),
};
let mut r = Self::new(
kind,
o.get("state").cloned().unwrap_or(Value::String(String::new())),
question,
options,
);
if let Some(a) = o.get("attachments").filter(|a| !a.is_null()) {
r.attachments = Attachments::from_json(a).context("decision request")?;
}
Ok(r)
}
pub fn state_text(&self) -> Result<String> {
let mut s = String::new();
for p in self.state_parts()? {
match p {
StatePart::Text(t) => s.push_str(&t),
StatePart::Image(_) => bail!("the state has images (see state_parts)"),
}
}
Ok(s)
}
pub fn state_parts(&self) -> Result<Vec<StatePart>> {
use crate::tools::qwen::py_json;
let mut out: Vec<StatePart> = self
.attachments
.images
.iter()
.map(|p| StatePart::Image(ImageSource::Path(p.clone())))
.collect();
#[cfg(feature = "oxideav")]
out.extend(
self.attachments
.image_frames
.iter()
.map(|f| StatePart::Image(ImageSource::Frame(Box::new(f.clone())))),
);
let text = |out: &mut Vec<StatePart>, t: &str| match out.last_mut() {
Some(StatePart::Text(s)) => s.push_str(t),
_ => out.push(StatePart::Text(t.to_string())),
};
let parts = match &self.state {
Value::Array(parts) => parts,
Value::String(s) => {
text(&mut out, s);
return Ok(out);
}
other => {
text(&mut out, &py_json(other));
return Ok(out);
}
};
for p in parts {
let Value::Object(o) = p else {
match p {
Value::String(t) => text(&mut out, t),
other => text(&mut out, &py_json(other)),
}
continue;
};
let image = match (o.get("image"), o.get("type").and_then(Value::as_str)) {
(Some(Value::String(s)), _) => Some(s.as_str()),
(Some(_), _) => bail!("decision state: \"image\" must be a string (a data URL, URL or path)"),
(None, Some("image_url")) => Some(
match o.get("image_url") {
Some(Value::String(s)) => Some(s.as_str()),
Some(u) => u.get("url").and_then(Value::as_str),
None => None,
}
.context("decision state: image_url part without a url")?,
),
_ => None,
};
match image {
Some(s) => out.push(StatePart::Image(ImageSource::parse(s)?)),
None if o.get("type").and_then(Value::as_str) == Some("text") => {
text(&mut out, o.get("text").and_then(Value::as_str).unwrap_or_default())
}
None => text(&mut out, &py_json(p)),
}
}
Ok(out)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Adaptation {
Native,
WideLabels,
}
impl Adaptation {
pub fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::WideLabels => "wide-labels",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Readout {
SlotHead,
LmHead,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct DecisionStats {
pub prompt_tokens: usize,
pub images: usize,
pub image_tokens: usize,
pub vision_secs: f64,
pub prefill_secs: f64,
pub total_secs: f64,
}
impl DecisionStats {
pub fn prefill_tok_s(&self) -> f64 {
self.prompt_tokens as f64 / self.prefill_secs.max(1e-9)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Decision {
pub kind: DecisionKind,
pub options: Vec<String>,
pub probabilities: Vec<f64>,
pub best_index: usize,
pub best: String,
pub adaptation: Adaptation,
pub readout: Readout,
pub stats: DecisionStats,
}
impl Decision {
pub fn expected_score(&self) -> Option<f64> {
(self.kind == DecisionKind::Score).then(|| self.probabilities.iter().enumerate().map(|(i, p)| i as f64 * p).sum())
}
pub fn to_json(&self, model: &str, protocol: &str) -> Value {
let n = self.stats.prompt_tokens;
json!({
"kind": self.kind.as_str(),
"effective_kind": self.kind.as_str(),
"options": self.options,
"probabilities": self.probabilities,
"choice_index": self.best_index,
"choice": self.best,
"adaptation": self.adaptation.as_str(),
"protocol": protocol,
"model": model,
"usage": {"prompt_tokens": n, "completion_tokens": 0, "total_tokens": n},
"num_model_requests": 1,
"elapsed_seconds": self.stats.total_secs,
})
}
}
#[derive(Clone, Debug)]
struct Slots {
verbalizers: Vec<String>,
ids: Vec<u32>,
ranges: [(usize, usize); 3],
}
fn parse_slots(v: &Value, ids: &Value, what: &str) -> Result<Slots> {
let s = v.get("slots").with_context(|| format!("{what}: missing \"slots\""))?;
let verbalizers: Vec<String> = s
.get("verbalizers")
.and_then(Value::as_array)
.with_context(|| format!("{what}: missing slots.verbalizers"))?
.iter()
.map(|x| {
x.as_str()
.map(str::to_string)
.with_context(|| format!("{what}: verbalizers must be strings"))
})
.collect::<Result<_>>()?;
let ids: Vec<u32> = ids
.as_array()
.with_context(|| format!("{what}: missing verbalizer_ids"))?
.iter()
.map(|x| {
x.as_u64()
.map(|v| v as u32)
.with_context(|| format!("{what}: verbalizer ids must be integers"))
})
.collect::<Result<_>>()?;
if ids.len() != verbalizers.len() {
bail!("{what}: {} verbalizer ids for {} verbalizers", ids.len(), verbalizers.len());
}
let mut ranges = [(0, 0); 3];
for k in [DecisionKind::Noul, DecisionKind::Score, DecisionKind::Choice] {
let r = s
.get("ranges")
.and_then(|r| r.get(k.as_str()))
.and_then(Value::as_array)
.with_context(|| format!("{what}: missing slots.ranges.{}", k.as_str()))?;
let (lo, hi) = (r.first().and_then(Value::as_u64), r.get(1).and_then(Value::as_u64));
let (Some(lo), Some(hi)) = (lo, hi) else {
bail!("{what}: bad slot range for {}", k.as_str());
};
let (lo, hi) = (lo as usize, hi as usize);
if lo >= hi || hi > ids.len() {
bail!("{what}: slot range {lo}..{hi} of {} outside {} slots", k.as_str(), ids.len());
}
ranges[k.index()] = (lo, hi);
}
let want = |k: DecisionKind| -> Vec<String> {
match k {
DecisionKind::Noul => vec!["false".into(), "true".into()],
_ => (0..6).map(|i| i.to_string()).collect(),
}
};
for k in [DecisionKind::Noul, DecisionKind::Score] {
let (lo, hi) = ranges[k.index()];
if verbalizers[lo..hi] != want(k)[..] {
bail!("{what}: unexpected {} verbalizers {:?}", k.as_str(), &verbalizers[lo..hi]);
}
}
Ok(Slots { verbalizers, ids, ranges })
}
pub struct DecisionAssets {
pub source: PathBuf,
pub model_name: String,
pub template: String,
pub readout: Readout,
slots: Slots,
head: Option<(Vec<f32>, Vec<f32>)>,
lm_bias: Vec<f32>,
temps: [f64; 3],
prefix: String,
labels: Vec<(String, u32)>,
pub(crate) switch: Option<Arc<AtomicBool>>,
pub adapter: Option<AdapterInfo>,
hidden: usize,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AdapterInfo {
pub dir: PathBuf,
pub rank: usize,
pub scale: f32,
pub modules: usize,
pub bytes: usize,
}
pub struct BaseCheck {
pub hidden_size: usize,
pub layers: usize,
pub vocab_size: usize,
}
impl DecisionAssets {
pub fn load(dir: &Path, tok: &Tokenizer, base: &BaseCheck, gemma: bool) -> Result<(Self, Option<LoraAdapter>)> {
let read_json = |name: &str| -> Result<Option<Value>> {
let p = dir.join(name);
if !p.is_file() {
return Ok(None);
}
Ok(Some(
serde_json::from_slice(&crate::fsx::read(&p)?).with_context(|| format!("parse {}", p.display()))?,
))
};
let judge = read_json("judge_config.json")?;
let dhead = read_json("adapter_vllm/decision_head.json")?;
let head_path = dir.join("head.safetensors");
let has_head = head_path.is_file() && judge.is_some();
let (readout, slots) = if has_head {
let j = judge.as_ref().unwrap();
(
Readout::SlotHead,
parse_slots(j, j.get("verbalizer_ids").unwrap_or(&Value::Null), "judge_config.json")?,
)
} else if let Some(d) = &dhead {
(
Readout::LmHead,
parse_slots(
d,
d.get("verbalizer_ids").unwrap_or(&Value::Null),
"adapter_vllm/decision_head.json",
)?,
)
} else {
bail!(
"{}: no decision readout (head.safetensors with judge_config.json, or adapter_vllm/decision_head.json)",
dir.display()
);
};
let n = slots.ids.len();
let mut checks: Vec<(&str, Option<u64>, usize)> = Vec::new();
if let Some(j) = &judge {
checks.push(("hidden_size", j.get("hidden_size").and_then(Value::as_u64), base.hidden_size));
}
if let Some(c) = read_json("config.json")? {
let c = c.get("text_config").cloned().unwrap_or(c);
checks.push(("hidden_size", c.get("hidden_size").and_then(Value::as_u64), base.hidden_size));
checks.push(("num_hidden_layers", c.get("num_hidden_layers").and_then(Value::as_u64), base.layers));
checks.push(("vocab_size", c.get("vocab_size").and_then(Value::as_u64), base.vocab_size));
}
for (what, theirs, ours) in checks {
if let Some(t) = theirs
&& t as usize != ours
{
bail!(
"decision assets in {} are for another base model: {what} {t}, the loaded model has {ours}",
dir.display()
);
}
}
for (v, &id) in slots.verbalizers.iter().zip(&slots.ids) {
let enc = tok.encode(v.as_str(), false).map_err(|e| anyhow::anyhow!("encode: {e}"))?;
if enc.get_ids() != [id] {
bail!(
"decision assets in {}: verbalizer {v:?} is token {id} in the bundle but {:?} for this tokenizer (another base model?)",
dir.display(),
enc.get_ids()
);
}
if id as usize >= base.vocab_size {
bail!("verbalizer token {id} is outside the vocabulary ({})", base.vocab_size);
}
}
let head = if readout == Readout::SlotHead {
let st = crate::safetensors::SafeTensors::open(&head_path, true)?;
let get = |name: &str| -> Result<(Vec<f32>, Vec<usize>)> {
let info = st.info(name).with_context(|| format!("{}: missing {name}", head_path.display()))?;
let b = st.bytes(name)?;
let v: Vec<f32> = match info.dtype.as_str() {
"F32" => b.as_chunks::<4>().0.iter().map(|c| f32::from_le_bytes(*c)).collect(),
"BF16" => b
.as_chunks::<2>()
.0
.iter()
.map(|c| crate::kernels::bf16_to_f32(u16::from_le_bytes(*c)))
.collect(),
d => bail!("{}: {name} has dtype {d}", head_path.display()),
};
Ok((v, info.shape.clone()))
};
let (w, ws) = get("proj.weight")?;
let (b, bs) = match get("proj.bias") {
Ok(x) => x,
Err(_) => (vec![0.0; n], vec![n]),
};
if ws != [n, base.hidden_size] || bs != [n] {
bail!(
"{}: head is {ws:?} + {bs:?}, expected [{n}, {}] for {n} slots and the model's hidden size",
head_path.display(),
base.hidden_size
);
}
Some((w, b))
} else {
None
};
let lm_bias: Vec<f32> = match &dhead {
Some(d) => {
let b: Vec<f32> = d
.get("bias")
.and_then(Value::as_array)
.map(|a| a.iter().map(|x| x.as_f64().unwrap_or(0.0) as f32).collect())
.unwrap_or_default();
if readout == Readout::LmHead && b.len() != n {
bail!("adapter_vllm/decision_head.json: {} biases for {n} slots", b.len());
}
b
}
None => Vec::new(),
};
let mut temps = [1.0f64; 3];
if let Some(c) = read_json("calibration.json")? {
for k in [DecisionKind::Noul, DecisionKind::Score, DecisionKind::Choice] {
if let Some(t) = c.get("per_kind").and_then(|p| p.get(k.as_str())).and_then(Value::as_f64) {
if !(t > 0.0 && t.is_finite()) {
bail!("calibration.json: bad temperature {t} for {}", k.as_str());
}
temps[k.index()] = t;
}
}
}
let merged = judge
.as_ref()
.and_then(|j| j.get("weights_mode"))
.and_then(Value::as_str)
.is_some_and(|m| m == "merged")
|| dhead.as_ref().and_then(|d| d.get("merged")).and_then(Value::as_bool) == Some(true);
let adapter_dir = match readout {
Readout::SlotHead => {
let sub = judge
.as_ref()
.and_then(|j| j.get("adapter_subfolder"))
.and_then(Value::as_str)
.unwrap_or("adapter");
dir.join(sub)
}
Readout::LmHead => dir.join("adapter_vllm"),
};
let adapter = if crate::lora::is_adapter_dir(&adapter_dir) {
let ad = LoraAdapter::load(&adapter_dir)?;
let l = ad.layer_count();
if l != base.layers {
bail!(
"LoRA adapter {} covers {l} layers, the loaded model has {}: the adapter is for another base model",
adapter_dir.display(),
base.layers
);
}
Some(ad)
} else if merged {
None
} else {
bail!(
"{}: the decision weights are unmerged but there is no adapter in {}",
dir.display(),
adapter_dir.display()
);
};
let template = judge
.as_ref()
.or(dhead.as_ref())
.and_then(|j| j.get("slots"))
.and_then(|s| s.get("template_version"))
.and_then(Value::as_str)
.unwrap_or("bare-v1")
.to_string();
if !template.starts_with("bare-v1") {
bail!("{}: template {template:?} is not supported (bare-v1)", dir.display());
}
let model_name = judge
.as_ref()
.and_then(|j| j.get("model_name"))
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| bundle_name(dir));
let prefix = match dhead.as_ref().and_then(|d| d.get("prefix_token")).and_then(Value::as_str) {
Some(p) => p.to_string(),
None if gemma => "<bos>".into(),
None => String::new(),
};
let labels = choice_labels(tok, &slots)?;
let switch = adapter.as_ref().map(LoraAdapter::switch);
Ok((
Self {
source: dir.to_path_buf(),
model_name,
template,
readout,
slots,
head,
lm_bias,
temps,
prefix,
labels,
switch,
adapter: None,
hidden: base.hidden_size,
},
adapter,
))
}
pub fn protocol(&self) -> String {
if self.template == "bare-v1" {
"jev27-bare-v1".into()
} else {
format!("jev-{}", self.template)
}
}
pub fn native_choice_options(&self) -> usize {
let (lo, hi) = self.slots.ranges[DecisionKind::Choice.index()];
hi - lo
}
pub fn max_choice_options(&self) -> usize {
self.labels.len().max(self.native_choice_options())
}
pub fn temperatures(&self) -> [f64; 3] {
self.temps
}
pub fn plan(&self, req: &DecisionRequest) -> Result<Plan> {
let k = req.kind;
let (lo, hi) = self.slots.ranges[k.index()];
let native = hi - lo;
let (options, lines, adaptation) = match k {
DecisionKind::Choice => {
let n = req.options.len();
let max = self.max_choice_options();
if n < 2 || n > max {
bail!("choice needs 2-{max} options, got {n}");
}
if n > native && self.labels.len() < n {
bail!(
"choice with {n} options: this tokenizer has single-token labels for {}",
self.labels.len()
);
}
let lines: Vec<String> = req
.options
.iter()
.enumerate()
.map(|(i, o)| format!("{}) {o}", self.label(i)))
.collect();
let a = if n > native { Adaptation::WideLabels } else { Adaptation::Native };
(req.options.clone(), lines, a)
}
_ => {
let v = self.slots.verbalizers[lo..hi].to_vec();
if !req.options.is_empty() && req.options != v {
bail!("{} options must be {v:?} (or left out)", k.as_str());
}
(v.clone(), v, Adaptation::Native)
}
};
let a = &req.attachments;
#[cfg(feature = "oxideav")]
let frames = !a.video_frames.is_empty() || !a.audio_frames.is_empty();
#[cfg(not(feature = "oxideav"))]
let frames = false;
if !a.videos.is_empty() || !a.audios.is_empty() || frames {
bail!("decisions take images only (the /v1/decide protocol has no video or audio parts)");
}
let mut pieces = vec![format!("{}[kind] {}\n[state] ", self.prefix, k.as_str())];
let mut images = Vec::new();
for p in req.state_parts()? {
match p {
StatePart::Text(t) => pieces.last_mut().unwrap().push_str(&t),
StatePart::Image(src) => {
images.push(src);
pieces.push(String::new());
}
}
}
pieces.last_mut().unwrap().push_str(&format!(
"\n[question] {}\n[options]\n{}\n[decision]:",
req.question,
lines.join("\n")
));
let prompt = pieces.join(IMAGE_PLACEHOLDER);
Ok(Plan {
kind: k,
prompt,
pieces,
images,
options,
adaptation,
})
}
fn label(&self, i: usize) -> &str {
let (lo, hi) = self.slots.ranges[DecisionKind::Choice.index()];
if lo + i < hi {
&self.slots.verbalizers[lo + i]
} else {
&self.labels[i].0
}
}
pub fn needs_logits(&self, plan: &Plan) -> bool {
self.readout == Readout::LmHead || plan.adaptation == Adaptation::WideLabels
}
pub fn read(&self, plan: &Plan, hidden: &[f32], logits: Option<&[f32]>) -> Result<Vec<f64>> {
assert_eq!(hidden.len(), self.hidden);
let k = plan.kind;
let (lo, hi) = self.slots.ranges[k.index()];
let n = plan.options.len();
let logit = |id: u32| -> Result<f64> {
let l = logits.context("readout needs the LM-head logits")?;
Ok(l[id as usize] as f64)
};
let mut z = Vec::with_capacity(n);
for i in 0..n {
let slot = lo + i;
let v = if slot < hi {
match (&self.head, self.readout) {
(Some((w, b)), Readout::SlotHead) => {
let row = &w[slot * self.hidden..(slot + 1) * self.hidden];
row.iter().zip(hidden).map(|(&a, &x)| a as f64 * x as f64).sum::<f64>() + b[slot] as f64
}
_ => logit(self.slots.ids[slot])? + self.lm_bias[slot] as f64,
}
} else {
logit(self.labels[i].1)?
};
z.push(v / self.temps[k.index()]);
}
let m = z.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let e: Vec<f64> = z.iter().map(|v| (v - m).exp()).collect();
let s: f64 = e.iter().sum();
Ok(e.into_iter().map(|v| v / s).collect())
}
pub fn decision(&self, plan: Plan, probabilities: Vec<f64>, stats: DecisionStats) -> Decision {
let best_index = probabilities
.iter()
.enumerate()
.fold((0, f64::NEG_INFINITY), |b, (i, &p)| if p > b.1 { (i, p) } else { b })
.0;
Decision {
kind: plan.kind,
best: plan.options[best_index].clone(),
options: plan.options,
probabilities,
best_index,
adaptation: plan.adaptation,
readout: self.readout,
stats,
}
}
pub fn info_json(&self) -> Value {
json!({
"protocol": self.protocol(),
"model": self.model_name,
"readout": match self.readout { Readout::SlotHead => "slot-head", Readout::LmHead => "lm-head" },
"adapter": self.adapter.as_ref().map(|a| json!({"dir": a.dir.display().to_string(), "rank": a.rank, "modules": a.modules})),
"max_options": self.max_choice_options(),
"native_choice_options": self.native_choice_options(),
"temperatures": {"noul": self.temps[0], "score": self.temps[1], "choice": self.temps[2]},
"source": self.source.display().to_string(),
})
}
}
pub const IMAGE_PLACEHOLDER: &str = "<|vision_start|><|image_pad|><|vision_end|>";
#[derive(Clone, Debug)]
pub struct Plan {
pub kind: DecisionKind,
pub prompt: String,
pub pieces: Vec<String>,
pub images: Vec<ImageSource>,
pub options: Vec<String>,
pub adaptation: Adaptation,
}
fn bundle_name(dir: &Path) -> String {
let name = |p: &Path| p.file_name().map(|s| s.to_string_lossy().to_string());
let own = name(dir).unwrap_or_default();
if !own.contains("--")
&& let Some(parent) = dir.parent().and_then(name)
&& parent.contains("--")
{
return parent.replace("--", "/");
}
own.replace("--", "/")
}
fn choice_labels(tok: &Tokenizer, slots: &Slots) -> Result<Vec<(String, u32)>> {
let (lo, hi) = slots.ranges[DecisionKind::Choice.index()];
let enc = |s: &str| -> Result<Vec<u32>> { Ok(tok.encode(s, false).map_err(|e| anyhow::anyhow!("encode: {e}"))?.get_ids().to_vec()) };
let letters: Vec<char> = ('A'..='Z').collect();
let cands = letters
.iter()
.map(|c| c.to_string())
.chain(letters.iter().flat_map(|a| letters.iter().map(move |b| format!("{a}{b}"))));
let mut out: Vec<(String, u32)> = Vec::new();
for lab in cands {
if out.len() == MAX_OPTIONS {
break;
}
let ids = enc(&lab)?;
if ids.len() == 1 && enc(&format!("x\n{lab}) y"))?.contains(&ids[0]) {
out.push((lab, ids[0]));
}
}
let trained: Vec<(String, u32)> = slots.verbalizers[lo..hi]
.iter()
.cloned()
.zip(slots.ids[lo..hi].iter().cloned())
.collect();
if out.len() < trained.len() || out[..trained.len()] != trained[..] {
return Ok(trained);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_json() {
let v: Value =
serde_json::from_str(r#"{"kind":"choice","state":{"sku":"AX-330","stock":0.08},"question":"Q?","options":["a","b"]}"#).unwrap();
let r = DecisionRequest::from_json(&v).unwrap();
assert_eq!(r.kind, DecisionKind::Choice);
assert_eq!(r.state_text().unwrap(), r#"{"sku": "AX-330", "stock": 0.08}"#);
assert_eq!(r.options, ["a", "b"]);
let v: Value = serde_json::from_str(r#"{"kind":"noul","state":"s","question":"q"}"#).unwrap();
let r = DecisionRequest::from_json(&v).unwrap();
assert!(r.options.is_empty() && r.state_text().unwrap() == "s");
assert!(DecisionRequest::from_json(&serde_json::json!({"kind": "maybe", "question": "q"})).is_err());
}
#[test]
fn state_images() {
let v = json!({"kind": "noul", "question": "q", "state": [
"Photo: ", {"image": "data:image/png;base64,Zm9vYmFy"}, "\nTask", {"a": 1},
{"type": "image_url", "image_url": {"url": "https://example.com/x.jpg"}}, {"type": "text", "text": "!"},
{"image": "file:///tmp/x.png"}]});
let r = DecisionRequest::from_json(&v).unwrap().with_images(["/tmp/first.png"]);
let parts = r.state_parts().unwrap();
assert_eq!(
parts,
[
StatePart::Image(ImageSource::Path("/tmp/first.png".into())),
StatePart::Text("Photo: ".into()),
StatePart::Image(ImageSource::Bytes(b"foobar".to_vec())),
StatePart::Text("\nTask{\"a\": 1}".into()),
StatePart::Image(ImageSource::Url("https://example.com/x.jpg".into())),
StatePart::Text("!".into()),
StatePart::Image(ImageSource::Path("/tmp/x.png".into())),
]
);
assert!(r.state_text().is_err());
assert_eq!(
ImageSource::parse(ImageSource::Bytes(b"fo".to_vec()).to_part()["image"].as_str().unwrap()).unwrap(),
ImageSource::Bytes(b"fo".to_vec())
);
assert!(ImageSource::parse("data:image/png,raw").is_err());
let v = json!({"kind": "noul", "question": "q", "state": "s", "attachments": {"images": ["a.png"], "max_soft_tokens": 64}});
let r = DecisionRequest::from_json(&v).unwrap();
assert_eq!((r.attachments.images.len(), r.attachments.max_soft_tokens), (1, 64));
assert_eq!(bundle_name(Path::new("/c/hub/autotrust--JEV-9B/vl")), "autotrust/JEV-9B");
assert_eq!(bundle_name(Path::new("/c/hub/autotrust--JEV-27B-VL")), "autotrust/JEV-27B-VL");
}
#[cfg(feature = "oxideav")]
#[test]
fn state_image_frames() {
let pic = crate::av::Picture::from_rgb(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap();
let r = DecisionRequest::noul("state", "q")
.with_images(["/tmp/a.png"])
.with_image_frames([pic.clone()]);
let frame = ImageSource::Frame(Box::new(pic));
assert_eq!(
r.state_parts().unwrap(),
[
StatePart::Image(ImageSource::Path("/tmp/a.png".into())),
StatePart::Image(frame.clone()),
StatePart::Text("state".into()),
]
);
let rgb = ImageSource::to_rgb_all(std::slice::from_ref(&frame)).unwrap();
assert_eq!((rgb[0].width, rgb[0].height, &rgb[0].data), (2, 1, &vec![1, 2, 3, 4, 5, 6]));
let part = frame.to_part();
assert!(part["image"].as_str().unwrap().starts_with("data:image/png;base64,"));
}
#[test]
fn slots_parse() {
let j: Value = serde_json::from_str(
r#"{"slots":{"ranges":{"noul":[0,2],"score":[2,8],"choice":[8,10]},
"verbalizers":["false","true","0","1","2","3","4","5","A","B"]},
"verbalizer_ids":[1,2,3,4,5,6,7,8,9,10]}"#,
)
.unwrap();
let s = parse_slots(&j, &j["verbalizer_ids"], "t").unwrap();
assert_eq!(s.ranges, [(0, 2), (2, 8), (8, 10)]);
}
}