use serde::{Deserialize, Serialize};
use super::{catalog_evidence, chat_controls, decide, effective_result};
use super::{DenialRule, InferenceBoundary, RouteFacts};
use crate::llm::api::data_controls::DataControlsOutcome;
use crate::llm_config::DataPosture;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InferenceAdmissionRequest {
pub provider: String,
pub model: String,
#[serde(default)]
pub boundary: Option<InferenceBoundary>,
#[serde(default)]
pub data_controls: Option<DataPosture>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum InferenceAdmissionStatus {
Admitted,
Denied,
Unknown,
}
#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
pub struct InferenceAdmissionSnapshot {
pub schema: String,
pub provider: String,
pub model: String,
pub status: InferenceAdmissionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_boundary: Option<InferenceBoundary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub governing_rule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_runtime: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_weight: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub training_default: Option<String>,
pub training_control_planned: bool,
pub data_posture: DataPosture,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_controls_outcome: Option<DataControlsOutcome>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_controls_note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_controls_refusal: Option<DataControlsRefusal>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DataControlsRefusal {
TrainingWithoutControl,
}
pub const DATA_CONTROLS_TRAINING_REFUSED_RULE: &str = "data_controls.training_refused";
pub fn inference_admission_schemas() -> [(&'static str, serde_json::Value); 2] {
[
(
"InferenceAdmissionRequest",
serde_json::to_value(schemars::schema_for!(InferenceAdmissionRequest))
.expect("request schema encodes"),
),
(
"InferenceAdmissionSnapshot",
serde_json::to_value(schemars::schema_for!(InferenceAdmissionSnapshot))
.expect("snapshot schema encodes"),
),
]
}
pub fn preview_inference_admission(
request: &InferenceAdmissionRequest,
) -> InferenceAdmissionSnapshot {
let mut snapshot = InferenceAdmissionSnapshot {
schema: "harn.llm.inference-admission/v1".into(),
provider: request.provider.clone(),
model: request.model.clone(),
status: InferenceAdmissionStatus::Unknown,
effective_boundary: None,
governing_rule: None,
local_runtime: None,
open_weight: None,
training_default: None,
training_control_planned: false,
data_posture: request
.data_controls
.unwrap_or_else(crate::llm_config::data_controls_default_posture),
data_controls_outcome: None,
data_controls_note: None,
data_controls_refusal: None,
};
snapshot.effective_boundary = match effective_result(request.boundary) {
Ok(boundary) => boundary,
Err(rule) => {
snapshot.governing_rule = Some(rule);
return snapshot;
}
};
if request.provider.trim().is_empty() || request.model.trim().is_empty() {
snapshot.governing_rule = Some("inference_boundary.route_unresolved".into());
return snapshot;
}
let evidence = match catalog_evidence(&request.provider, &request.model) {
Ok(evidence) => evidence,
Err(denial) => {
snapshot.governing_rule = Some(denial.rule.as_str().into());
snapshot.status = if matches!(denial.rule, DenialRule::UnknownProvider) {
InferenceAdmissionStatus::Unknown
} else {
InferenceAdmissionStatus::Denied
};
return snapshot;
}
};
snapshot.local_runtime = Some(evidence.local_runtime);
snapshot.open_weight = evidence.open_weight;
let controls = chat_controls(&request.provider, &request.model, snapshot.data_posture);
snapshot.data_posture = serde_json::from_value(serde_json::Value::String(
controls.receipt.requested_posture.clone(),
))
.unwrap_or(DataPosture::Default);
snapshot.data_controls_outcome = Some(controls.receipt.outcome);
snapshot.data_controls_note = controls.receipt.note.clone();
snapshot.training_default =
controls
.receipt
.training_default
.and_then(|value| match serde_json::to_value(value) {
Ok(serde_json::Value::String(value)) => Some(value),
_ => None,
});
snapshot.training_control_planned = controls
.receipt
.applied
.iter()
.any(|control| control.effect == "training");
if crate::llm::api::data_controls::training_refusal(
&request.provider,
&request.model,
snapshot.data_posture,
&controls.receipt,
)
.is_some()
{
snapshot.status = InferenceAdmissionStatus::Denied;
snapshot.governing_rule = Some(DATA_CONTROLS_TRAINING_REFUSED_RULE.into());
snapshot.data_controls_refusal = Some(DataControlsRefusal::TrainingWithoutControl);
return snapshot;
}
let Some(boundary) = snapshot.effective_boundary else {
snapshot.status = InferenceAdmissionStatus::Admitted;
return snapshot;
};
let facts = RouteFacts {
local: evidence.local_runtime,
open_weight: evidence.open_weight,
training_default: controls.receipt.training_default,
training_control_applied: snapshot.training_control_planned,
};
match decide(boundary, &request.provider, &request.model, facts) {
Ok(rule) => {
snapshot.status = InferenceAdmissionStatus::Admitted;
snapshot.governing_rule = Some(rule.into());
}
Err(denial) => {
snapshot.governing_rule = Some(denial.rule.as_str().into());
snapshot.status = match denial.rule {
DenialRule::TrainingUnknown | DenialRule::UnknownProvider => {
InferenceAdmissionStatus::Unknown
}
DenialRule::HostedOpenWeight if evidence.open_weight.is_none() => {
InferenceAdmissionStatus::Unknown
}
_ => InferenceAdmissionStatus::Denied,
};
}
}
snapshot
}
#[cfg(test)]
mod tests;