kernel/event/record/modality.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The two faces of an attached endpoint that are not the conversation:
7//! what one embedding or rerank call asked, and what came back.
8//!
9//! One line per settled call rather than a called/returned pair, because
10//! neither answer is part of a conversation: what a reader asks of these
11//! lines is what was bought - which model read how many texts, and how
12//! many vectors or ranks came back for them. The vectors themselves are
13//! absent for the reason `ModelCalled` carries no request body: they are
14//! derived values a replay recomputes from the recorded inputs, and a
15//! second copy here would be a second answer to the same question.
16
17use serde::{Deserialize, Serialize};
18
19use crate::budget::Tokens;
20
21/// `embedding_called`: one call to an attached endpoint's embeddings
22/// face.
23///
24/// The two counts are recorded rather than one being derived from the
25/// other: an answer that returned fewer vectors than it was given texts
26/// is a retrieval defect, and this is the line that shows it.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct EmbeddingCalled {
30 /// The endpoint's own model id, as the request spelled it.
31 pub model: String,
32 /// How many texts the call asked to embed.
33 pub inputs: u64,
34 /// How many vectors the answer carried.
35 pub vectors: u64,
36 /// The width the caller asked for. Absent when it named none, which
37 /// is not the same fact as a width of zero.
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub dimensions: Option<u64>,
40 /// What the provider said it read. Absent when it said nothing.
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub prompt_tokens: Option<Tokens>,
43}
44
45/// `rerank_called`: one call to an attached endpoint's rerank face.
46///
47/// The order the service returned is the answer and is not repeated
48/// here; what this line adds is the size of what was asked and of what
49/// came back, so a rerank that quietly scored half the passages can be
50/// found afterwards. The line states no token count, because the rerank
51/// faces this city writes to report none, and a column nothing can fill
52/// is a promise to the ledger's reader that is never kept.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55pub struct RerankCalled {
56 /// The endpoint's own model id, as the request spelled it.
57 pub model: String,
58 /// How many passages the call offered to rank.
59 pub passages: u64,
60 /// How many ranks the answer carried.
61 pub ranks: u64,
62}
63
64#[cfg(test)]
65#[allow(
66 clippy::unwrap_used,
67 clippy::expect_used,
68 clippy::panic,
69 clippy::indexing_slicing,
70 reason = "test code"
71)]
72mod tests {
73 use super::*;
74 use crate::event::Payload;
75
76 /// The keys are the contract: a ledger already written cannot be
77 /// re-spelled, so the shape of a line is pinned here.
78 #[test]
79 fn an_embedding_line_keeps_the_four_keys_and_omits_what_was_not_stated() {
80 let call = EmbeddingCalled {
81 model: "bge-m3".to_owned(),
82 inputs: 12,
83 vectors: 12,
84 dimensions: None,
85 prompt_tokens: Some(Tokens::new(77)),
86 };
87 let payload = Payload::of(&call).unwrap();
88 assert_eq!(
89 serde_json::to_string(&payload).unwrap(),
90 "{\"inputs\":12,\"model\":\"bge-m3\",\"prompt_tokens\":77,\"vectors\":12}"
91 );
92 assert_eq!(
93 serde_json::to_string(
94 &Payload::of(&EmbeddingCalled {
95 dimensions: Some(1024),
96 ..call.clone()
97 })
98 .unwrap()
99 )
100 .unwrap(),
101 "{\"dimensions\":1024,\"inputs\":12,\"model\":\"bge-m3\",\"prompt_tokens\":77,\"vectors\":12}"
102 );
103 assert_eq!(payload.read::<EmbeddingCalled>().unwrap(), call);
104 }
105
106 #[test]
107 fn a_rerank_line_carries_the_two_counts_and_no_token_column() {
108 let call = RerankCalled {
109 model: "bge-reranker".to_owned(),
110 passages: 40,
111 ranks: 40,
112 };
113 let payload = Payload::of(&call).unwrap();
114 assert_eq!(
115 serde_json::to_string(&payload).unwrap(),
116 "{\"model\":\"bge-reranker\",\"passages\":40,\"ranks\":40}"
117 );
118 assert_eq!(payload.read::<RerankCalled>().unwrap(), call);
119 }
120}