Skip to main content

rill_runtime/handler/
builtin.rs

1//! Built-in handler that executes linear-regression inference in-process.
2//!
3//! This handler is preserved for backwards compatibility and as a fallback
4//! when a WASM handler is not available. It does not cross a sandbox
5//! boundary; the runtime binary selects it via `--builtin-handler
6//! linear-regression`.
7
8use serde::Deserialize;
9use serde_json::Value;
10
11use crate::package::LoadedModelPack;
12use crate::server::InvokeHandler;
13
14pub const LINEAR_REGRESSION_CAPABILITY: &str = "rillml.linearRegression.predict";
15
16#[derive(Debug, Deserialize)]
17#[serde(rename_all = "camelCase", deny_unknown_fields)]
18struct LinearRegressionModel {
19    kind: String,
20    weights: Vec<f64>,
21    intercept: f64,
22}
23
24#[derive(Debug, Deserialize)]
25#[serde(deny_unknown_fields)]
26struct LinearRegressionInput {
27    features: Vec<f64>,
28}
29
30/// Business-neutral linear-regression handler used by the distributed runtime binary.
31#[derive(Debug, Clone)]
32pub struct LinearRegressionInvokeHandler {
33    weights: Vec<f64>,
34    intercept: f64,
35}
36
37impl LinearRegressionInvokeHandler {
38    pub fn from_pack(pack: &LoadedModelPack) -> Result<Self, String> {
39        if pack.manifest.capabilities.as_slice() != [LINEAR_REGRESSION_CAPABILITY] {
40            return Err(format!(
41                "standalone runtime requires exactly the {LINEAR_REGRESSION_CAPABILITY} capability"
42            ));
43        }
44        let model: LinearRegressionModel = serde_json::from_value(pack.model.clone())
45            .map_err(|error| format!("invalid linear-regression model: {error}"))?;
46        if model.kind != "linearRegression" {
47            return Err("unsupported built-in model kind".into());
48        }
49        if model.weights.is_empty() || model.weights.len() > 65_536 {
50            return Err("linear-regression weights must contain 1..=65536 values".into());
51        }
52        if !model.intercept.is_finite() || model.weights.iter().any(|value| !value.is_finite()) {
53            return Err("linear-regression model values must be finite".into());
54        }
55        Ok(Self {
56            weights: model.weights,
57            intercept: model.intercept,
58        })
59    }
60}
61
62impl InvokeHandler for LinearRegressionInvokeHandler {
63    fn invoke(&self, capability: &str, input: &Value) -> Result<Value, String> {
64        if capability != LINEAR_REGRESSION_CAPABILITY {
65            return Err("unsupported capability".into());
66        }
67        let input: LinearRegressionInput = serde_json::from_value(input.clone())
68            .map_err(|error| format!("invalid linear-regression input: {error}"))?;
69        if input.features.len() != self.weights.len() {
70            return Err(format!(
71                "expected {} features, received {}",
72                self.weights.len(),
73                input.features.len()
74            ));
75        }
76        if input.features.iter().any(|value| !value.is_finite()) {
77            return Err("linear-regression input values must be finite".into());
78        }
79        let prediction = self
80            .weights
81            .iter()
82            .zip(&input.features)
83            .try_fold(self.intercept, |sum, (weight, feature)| {
84                let next = sum + weight * feature;
85                next.is_finite().then_some(next)
86            })
87            .ok_or_else(|| "linear-regression prediction overflowed".to_string())?;
88        Ok(serde_json::json!({ "prediction": prediction }))
89    }
90}