1 use serde::Serialize;
2
3 /// What gets sent to GresIQ when a model finishes loading.
4 #[derive(Debug, Clone, Serialize)]
5 pub struct ModelLoadedEvent {
6 /// Which machine this is. Keep it stable across restarts — change it
7 /// and you'll get a duplicate edge row in the dashboard.
8 pub edge_id: String,
9 /// The HuggingFace repo ID, e.g. `bartowski/Qwen2.5-3B-Instruct-GGUF`.
10 pub model_id: String,
11 /// What shows up in the UI, e.g. `Qwen 2.5 3B`.
12 pub model_name: String,
13 /// How long the load took. This is `elapsed.as_millis() as u64`,
14 /// where `elapsed` is what `load_gguf_model` returns.
15 pub load_duration_ms: u64,
16 /// The Onde app that loaded this model (UUID string from the
17 /// ondeinference.com dashboard). `None` for SDK builds without app
18 /// credentials or for direct Rust consumers. In the document store this
19 /// is a plain field on the `pulse_models` doc, not a foreign key.
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub onde_app_id: Option<String>,
22 }
23
24 /// What gets sent to GresIQ after each `send_message` or `generate`.
25 #[derive(Debug, Clone, Serialize)]
26 pub struct InferenceEvent {
27 /// Same edge as in `ModelLoadedEvent`.
28 pub edge_id: String,
29 /// Same model as in `ModelLoadedEvent`.
30 pub model_id: String,
31 /// Auto-generated per request. Looks like `onde-1720000000000-42`.
32 pub request_id: String,
33 /// Wall-clock time for the whole response, in milliseconds.
34 pub duration_ms: u64,
35 /// Time to first token, in milliseconds. Only the streaming path can
36 /// observe this; a blocking `send_message` gets the whole response at
37 /// once and reports `None` rather than a stand-in value.
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub ttft_ms: Option<u64>,
40 /// `"success"`, `"cancelled"`, or `"error"`. A blocking request only
41 /// reports `"success"`, since its errors return before reaching pulse. A
42 /// stream can fail after it starts, so a mid-stream model error reports
43 /// `"error"`, usually with no `ttft_ms`, and a stream whose receiver was
44 /// dropped reports `"cancelled"`.
45 pub status: String,
46 /// Same Onde app as the corresponding `ModelLoadedEvent`. `None` for SDK
47 /// builds without app credentials or direct Rust consumers.
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub onde_app_id: Option<String>,
50 }
51
52 #[cfg(test)]
53 mod tests {
54 use super::*;
55
56 #[test]
57 fn model_loaded_serializes_onde_app_id_when_present() {
58 let event = ModelLoadedEvent {
59 edge_id: "edge-1".into(),
60 model_id: "org/repo".into(),
61 model_name: "Repo 3B".into(),
62 load_duration_ms: 1234,
63 onde_app_id: Some("app-uuid".into()),
64 };
65 let value = serde_json::to_value(&event).expect("serialize");
66 assert_eq!(value["onde_app_id"], "app-uuid");
67 assert_eq!(value["load_duration_ms"], 1234);
68 }
69
70 #[test]
71 fn onde_app_id_is_omitted_when_absent() {
72 let event = InferenceEvent {
73 edge_id: "edge-1".into(),
74 model_id: "org/repo".into(),
75 request_id: "onde-1-0".into(),
76 duration_ms: 42,
77 ttft_ms: None,
78 status: "success".into(),
79 onde_app_id: None,
80 };
81 let value = serde_json::to_value(&event).expect("serialize");
82 assert!(value.get("onde_app_id").is_none());
83 }
84
85 #[test]
86 fn ttft_is_omitted_when_the_path_cannot_measure_it() {
87 let mut event = InferenceEvent {
88 edge_id: "edge-1".into(),
89 model_id: "org/repo".into(),
90 request_id: "onde-1-0".into(),
91 duration_ms: 42,
92 ttft_ms: None,
93 status: "success".into(),
94 onde_app_id: None,
95 };
96 assert!(serde_json::to_value(&event)
97 .expect("serialize")
98 .get("ttft_ms")
99 .is_none());
100
101 event.ttft_ms = Some(17);
102 assert_eq!(
103 serde_json::to_value(&event).expect("serialize")["ttft_ms"],
104 17
105 );
106 }
107 }