use serde_json::Value;
use super::schema::{FieldKind, FieldSchema};
use crate::errors::FieldError;
pub(super) const MODEL_INFER_FIELDS: &[FieldSchema] = &[
FieldSchema {
name: "model",
description: "The model id; the active version resolves. JSONLogic here is what lets one \
workflow route to any model.",
kind: FieldKind::String,
required: true,
template_at: &[""],
..FieldSchema::DEFAULT
},
FieldSchema {
name: "input",
description: "The JSON root every input adapter of the manifest sees. `{\"var\": \"\"}` \
hands the adapters the whole message context.",
kind: FieldKind::Any,
required: true,
template_at: &[""],
..FieldSchema::DEFAULT
},
FieldSchema {
name: "runtime",
description: "Which runtime runs the graph: one of the compiled-in names (`tract`); \
default `[models.default_runtime]` for the model's format.",
kind: FieldKind::String,
..FieldSchema::DEFAULT
},
FieldSchema {
name: "output",
description: "Dotted result path, default `temp_data.inference`.",
kind: FieldKind::String,
..FieldSchema::DEFAULT
},
FieldSchema {
name: "raw",
description: "Skip `result`; write `{name: tensor}` in wire form for chaining.",
kind: FieldKind::Bool,
..FieldSchema::DEFAULT
},
FieldSchema {
name: "timeout_ms",
description: "Per-call deadline (JSONLogic), capped by `models.max_timeout_ms`; a cold \
load on first use is charged to it.",
kind: FieldKind::Number,
template_at: &[""],
..FieldSchema::DEFAULT
},
FieldSchema {
name: "stats_output",
description: "Path for `{id, version, digest, runtime, device, parameters, \
artifact_bytes, ops, peak_ops, queued_ms, inference_ms, cold_load}`; \
absent means not written.",
kind: FieldKind::String,
..FieldSchema::DEFAULT
},
];
pub(crate) fn unknown_runtime(path: impl Into<String>, name: &str) -> FieldError {
FieldError::new(
path,
"MODEL_RUNTIME_UNKNOWN",
format!(
"runtime '{name}' is not one this build knows: {}",
crate::model::runtimes::NAMES.join(", ")
),
)
}
pub(super) fn validate_static_input(
obj: &serde_json::Map<String, Value>,
) -> Vec<(&'static str, &'static str, String)> {
let mut errors: Vec<(&'static str, &'static str, String)> = Vec::new();
if let Some(name) = obj.get("runtime").and_then(Value::as_str)
&& crate::model::runtimes::intern(name).is_none()
{
let refusal = unknown_runtime("runtime", name);
errors.push((
"runtime",
orion_api::error::field_codes::MODEL_RUNTIME_UNKNOWN,
refusal.message,
));
}
if let Some(timeout) = obj
.get("timeout_ms")
.filter(|v| !v.is_null() && !v.is_object() && !v.is_array())
&& timeout.as_u64().is_none_or(|ms| ms == 0)
{
errors.push((
"timeout_ms",
"INVALID",
"timeout_ms must be a positive integer (milliseconds)".to_string(),
));
}
if let Some(Value::String(id)) = obj.get("model")
&& id.trim().is_empty()
{
errors.push(("model", "INVALID", "model must name a model".to_string()));
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::FunctionRegistry;
use serde_json::json;
fn errors_for(input: Value) -> Vec<(String, String)> {
FunctionRegistry::builtin()
.validate_input("model_infer", &input, "tasks[0]")
.into_iter()
.map(|e| (e.path, e.code))
.collect()
}
#[test]
fn the_table_is_registered_in_order_and_a_good_input_passes() {
let entry = FunctionRegistry::builtin()
.get("model_infer")
.expect("registered");
let names: Vec<&str> = entry
.input_fields
.as_deref()
.unwrap_or(&[])
.iter()
.map(|f| f.name.as_str())
.collect();
assert_eq!(
names,
[
"model",
"input",
"runtime",
"output",
"raw",
"timeout_ms",
"stats_output"
]
);
assert_eq!(entry.category, "compute");
assert!(
errors_for(json!({
"model": "ada.c4-tiny",
"input": {"var": ""},
"runtime": "tract",
"output": "data.policy",
"raw": false,
"timeout_ms": 250,
"stats_output": "temp_data.stats"
}))
.is_empty()
);
assert!(
errors_for(json!({"model": {"var": "data.model"}, "input": {"var": "data"}}))
.is_empty()
);
}
#[test]
fn an_unknown_runtime_and_a_bad_timeout_are_refused_by_code() {
let errors = errors_for(json!({
"model": "ada.c4-tiny",
"input": {},
"runtime": "nope",
"timeout_ms": 0
}));
assert!(
errors.contains(&(
"tasks[0].function.input.runtime".to_string(),
"MODEL_RUNTIME_UNKNOWN".to_string()
)),
"{errors:?}"
);
assert!(
errors.contains(&(
"tasks[0].function.input.timeout_ms".to_string(),
"INVALID".to_string()
)),
"{errors:?}"
);
let errors = errors_for(json!({"model": " ", "input": {}, "timeout_ms": -5}));
assert!(
errors.contains(&(
"tasks[0].function.input.model".to_string(),
"INVALID".to_string()
)),
"{errors:?}"
);
assert!(
errors.iter().any(|(p, _)| p.ends_with(".timeout_ms")),
"{errors:?}"
);
let errors = errors_for(json!({"model": "m", "input": {}, "stats": "x"}));
assert!(
errors
.iter()
.any(|(p, c)| p.ends_with(".stats") && c == "UNKNOWN_FIELD"),
"{errors:?}"
);
let errors = errors_for(json!({}));
assert!(
errors.contains(&(
"tasks[0].function.input.model".to_string(),
"REQUIRED".to_string()
)),
"{errors:?}"
);
assert!(
errors.contains(&(
"tasks[0].function.input.input".to_string(),
"REQUIRED".to_string()
)),
"{errors:?}"
);
}
#[test]
fn a_computed_timeout_is_an_expression_and_a_literal_one_is_still_judged() {
let timeout = FunctionRegistry::builtin()
.get("model_infer")
.and_then(|e| {
e.input_fields
.as_deref()?
.iter()
.find(|f| f.name == "timeout_ms")
.map(|f| f.template_at)
})
.expect("timeout_ms is registered");
assert!(timeout.contains(&""), "the field itself is the expression");
for computed in [json!({"var": "temp_data.ms"}), json!({"+": [40, 10]})] {
let errors = errors_for(json!({
"model": "ada.c4-tiny",
"input": {"var": ""},
"timeout_ms": computed
}));
assert!(errors.is_empty(), "{errors:?}");
}
for literal in [json!(0), json!(-5), json!("250")] {
let errors = errors_for(json!({
"model": "ada.c4-tiny",
"input": {},
"timeout_ms": literal
}));
assert!(
errors.iter().any(|(p, _)| p.ends_with(".timeout_ms")),
"{errors:?}"
);
}
}
#[test]
fn every_known_runtime_passes_the_static_check() {
for name in crate::model::runtimes::NAMES {
let obj = json!({"model": "m", "input": {}, "runtime": name});
assert!(
validate_static_input(obj.as_object().expect("object")).is_empty(),
"{name}"
);
}
let refusal = unknown_runtime("tasks[0].function.input.runtime", "ort");
assert_eq!(
refusal.code,
orion_api::error::field_codes::MODEL_RUNTIME_UNKNOWN
);
assert_eq!(refusal.path, "tasks[0].function.input.runtime");
assert!(refusal.message.contains("'ort'") && refusal.message.contains("tract"));
}
}