1use std::collections::{btree_map::Entry, BTreeMap};
4
5use serde::{Deserialize, Serialize};
6
7pub const MODEL_LOGITS_OBSERVATION_PATH: &str = "model.logits";
13
14pub const PROCESSOR_OUTPUT_OBSERVATION_PATH: &str = "model.processor.output";
16
17pub const VISION_PROJECTOR_OUTPUT_OBSERVATION_PATH: &str = "model.vision.projector.output";
19
20pub const AUDIO_PROJECTOR_OUTPUT_OBSERVATION_PATH: &str = "model.audio.projector.output";
22
23pub const MODALITY_MERGE_OUTPUT_OBSERVATION_PATH: &str = "model.modality.merge.output";
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(tag = "dtype", content = "values", rename_all = "snake_case")]
33pub enum TensorObservationData {
34 F32(Vec<f32>),
36 I64(Vec<i64>),
38 U64(Vec<u64>),
40 Bool(Vec<bool>),
42}
43
44impl TensorObservationData {
45 pub fn len(&self) -> usize {
47 match self {
48 Self::F32(values) => values.len(),
49 Self::I64(values) => values.len(),
50 Self::U64(values) => values.len(),
51 Self::Bool(values) => values.len(),
52 }
53 }
54
55 pub fn is_empty(&self) -> bool {
57 self.len() == 0
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct TensorObservation {
64 shape: Vec<usize>,
65 data: TensorObservationData,
66}
67
68impl TensorObservation {
69 pub fn new(shape: Vec<usize>, data: TensorObservationData) -> Result<Self, ObservationError> {
71 let elements = shape.iter().try_fold(1usize, |count, dimension| {
72 count
73 .checked_mul(*dimension)
74 .ok_or(ObservationError::ShapeOverflow)
75 })?;
76 if elements != data.len() {
77 return Err(ObservationError::ElementCount {
78 shape,
79 expected: elements,
80 actual: data.len(),
81 });
82 }
83 Ok(Self { shape, data })
84 }
85
86 pub fn shape(&self) -> &[usize] {
88 &self.shape
89 }
90
91 pub const fn data(&self) -> &TensorObservationData {
93 &self.data
94 }
95
96 pub fn into_parts(self) -> (Vec<usize>, TensorObservationData) {
98 (self.shape, self.data)
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
105pub enum ObservationValue {
106 Tensor(TensorObservation),
108 Float(f64),
110 Integer(i64),
112 Unsigned(u64),
114 Boolean(bool),
116 Text(String),
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
122pub struct ObservationSet {
123 values: BTreeMap<String, ObservationValue>,
124}
125
126impl ObservationSet {
127 pub const fn new() -> Self {
129 Self {
130 values: BTreeMap::new(),
131 }
132 }
133
134 pub fn insert(
136 &mut self,
137 path: impl Into<String>,
138 value: ObservationValue,
139 ) -> Result<(), ObservationError> {
140 let path = path.into();
141 if path.is_empty() {
142 return Err(ObservationError::EmptyPath);
143 }
144 match self.values.entry(path) {
145 Entry::Vacant(entry) => {
146 entry.insert(value);
147 }
148 Entry::Occupied(entry) => {
149 return Err(ObservationError::DuplicatePath(entry.key().clone()));
150 }
151 }
152 Ok(())
153 }
154
155 pub fn get(&self, path: &str) -> Option<&ObservationValue> {
157 self.values.get(path)
158 }
159
160 pub fn iter(&self) -> impl Iterator<Item = (&str, &ObservationValue)> {
162 self.values
163 .iter()
164 .map(|(path, value)| (path.as_str(), value))
165 }
166
167 pub fn len(&self) -> usize {
169 self.values.len()
170 }
171
172 pub fn is_empty(&self) -> bool {
174 self.values.is_empty()
175 }
176
177 pub fn prefixed(self, prefix: &str) -> Result<Self, ObservationError> {
179 if prefix.is_empty() {
180 return Ok(self);
181 }
182 let mut output = Self::new();
183 for (path, value) in self.values {
184 output.insert(format!("{prefix}.{path}"), value)?;
185 }
186 Ok(output)
187 }
188
189 pub fn extend(&mut self, other: Self) -> Result<(), ObservationError> {
191 for (path, value) in other.values {
192 self.insert(path, value)?;
193 }
194 Ok(())
195 }
196}
197
198#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
200#[serde(tag = "match", content = "path", rename_all = "snake_case")]
201pub enum ObservationSelector {
202 Exact(String),
204 Prefix(String),
206}
207
208impl ObservationSelector {
209 pub fn matches(&self, path: &str) -> bool {
211 match self {
212 Self::Exact(expected) => path == expected,
213 Self::Prefix(prefix) => {
214 path == prefix
215 || path
216 .strip_prefix(prefix)
217 .is_some_and(|suffix| suffix.starts_with('.'))
218 }
219 }
220 }
221}
222
223#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
225pub struct ObservationRequest {
226 selectors: Vec<ObservationSelector>,
227}
228
229impl ObservationRequest {
230 pub const fn all() -> Self {
232 Self {
233 selectors: Vec::new(),
234 }
235 }
236
237 pub fn selected(selectors: impl IntoIterator<Item = ObservationSelector>) -> Self {
239 Self {
240 selectors: selectors.into_iter().collect(),
241 }
242 }
243
244 pub fn matches(&self, path: &str) -> bool {
246 self.selectors.is_empty() || self.selectors.iter().any(|selector| selector.matches(path))
247 }
248
249 pub fn selectors(&self) -> &[ObservationSelector] {
251 &self.selectors
252 }
253}
254
255#[derive(Debug)]
257pub struct InspectedOutput<O> {
258 pub output: O,
260 pub observations: ObservationSet,
262}
263
264#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
266pub enum ObservationError {
267 #[error("tensor observation shape element count overflowed")]
269 ShapeOverflow,
270 #[error(
272 "tensor observation shape {shape:?} requires {expected} values, but received {actual}"
273 )]
274 ElementCount {
275 shape: Vec<usize>,
277 expected: usize,
279 actual: usize,
281 },
282 #[error("observation path must not be empty")]
284 EmptyPath,
285 #[error("duplicate observation path {0:?}")]
287 DuplicatePath(String),
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn tensor_shape_and_values_must_agree() {
296 let tensor = TensorObservation::new(
297 vec![2, 2],
298 TensorObservationData::F32(vec![1.0, 2.0, 3.0, 4.0]),
299 )
300 .unwrap();
301 assert_eq!(tensor.shape(), [2, 2]);
302 assert!(matches!(tensor.data(), TensorObservationData::F32(_)));
303 assert!(matches!(
304 TensorObservation::new(vec![2], TensorObservationData::I64(vec![1])),
305 Err(ObservationError::ElementCount { .. })
306 ));
307 }
308
309 #[test]
310 fn selectors_and_sets_are_stable_and_collision_safe() {
311 let request = ObservationRequest::selected([
312 ObservationSelector::Exact(MODEL_LOGITS_OBSERVATION_PATH.into()),
313 ObservationSelector::Prefix("model.layers.2".into()),
314 ]);
315 assert_eq!(MODEL_LOGITS_OBSERVATION_PATH, "model.logits");
316 assert!(request.matches(MODEL_LOGITS_OBSERVATION_PATH));
317 assert!(request.matches("model.layers.2.output"));
318 assert!(!request.matches("model.layers.20.output"));
319
320 let mut set = ObservationSet::new();
321 set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(3))
322 .unwrap();
323 assert_eq!(
324 set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(4)),
325 Err(ObservationError::DuplicatePath(
326 MODEL_LOGITS_OBSERVATION_PATH.into()
327 ))
328 );
329 }
330}