juncture_tracing/types.rs
1//! Types for server metadata and LLM caching policies
2//!
3//! This module provides types for deployment metadata and LLM response caching configuration.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Server deployment metadata for observability
9///
10/// Contains optional information about the deployment environment that can be
11/// attached to traces and metrics for better observability in multi-instance
12/// deployments.
13///
14/// # Examples
15///
16/// ```
17/// use juncture_tracing::types::ServerInfo;
18///
19/// let info = ServerInfo {
20/// assistant_id: Some("asst_123".to_string()),
21/// deployment: Some("production".to_string()),
22/// ..Default::default()
23/// };
24/// ```
25#[derive(Clone, Debug, Default, Deserialize, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct ServerInfo {
28 /// Assistant ID for multi-assistant deployments
29 pub assistant_id: Option<String>,
30
31 /// Graph ID identifying the deployed graph
32 pub graph_id: Option<String>,
33
34 /// Authenticated user (if applicable)
35 pub user: Option<String>,
36
37 /// Deployment environment identifier
38 pub deployment: Option<String>,
39
40 /// Service version
41 pub version: Option<String>,
42
43 /// Instance ID for multi-instance deployments
44 pub instance_id: Option<String>,
45}
46
47impl ServerInfo {
48 /// Create a new empty `ServerInfo`
49 ///
50 /// # Examples
51 ///
52 /// ```
53 /// use juncture_tracing::types::ServerInfo;
54 ///
55 /// let info = ServerInfo::new();
56 /// assert!(info.assistant_id.is_none());
57 /// ```
58 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 /// Set the assistant ID
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use juncture_tracing::types::ServerInfo;
69 ///
70 /// let info = ServerInfo::new().with_assistant_id("asst_123");
71 /// assert_eq!(info.assistant_id, Some("asst_123".to_string()));
72 /// ```
73 #[must_use]
74 pub fn with_assistant_id(mut self, id: impl Into<String>) -> Self {
75 self.assistant_id = Some(id.into());
76 self
77 }
78
79 /// Set the graph ID
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use juncture_tracing::types::ServerInfo;
85 ///
86 /// let info = ServerInfo::new().with_graph_id("graph_456");
87 /// assert_eq!(info.graph_id, Some("graph_456".to_string()));
88 /// ```
89 #[must_use]
90 pub fn with_graph_id(mut self, id: impl Into<String>) -> Self {
91 self.graph_id = Some(id.into());
92 self
93 }
94
95 /// Set the user
96 ///
97 /// # Examples
98 ///
99 /// ```
100 /// use juncture_tracing::types::ServerInfo;
101 ///
102 /// let info = ServerInfo::new().with_user("user@example.com");
103 /// assert_eq!(info.user, Some("user@example.com".to_string()));
104 /// ```
105 #[must_use]
106 pub fn with_user(mut self, user: impl Into<String>) -> Self {
107 self.user = Some(user.into());
108 self
109 }
110
111 /// Set the deployment environment
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use juncture_tracing::types::ServerInfo;
117 ///
118 /// let info = ServerInfo::new().with_deployment("production");
119 /// assert_eq!(info.deployment, Some("production".to_string()));
120 /// ```
121 #[must_use]
122 pub fn with_deployment(mut self, deployment: impl Into<String>) -> Self {
123 self.deployment = Some(deployment.into());
124 self
125 }
126
127 /// Set the version
128 ///
129 /// # Examples
130 ///
131 /// ```
132 /// use juncture_tracing::types::ServerInfo;
133 ///
134 /// let info = ServerInfo::new().with_version("1.0.0");
135 /// assert_eq!(info.version, Some("1.0.0".to_string()));
136 /// ```
137 #[must_use]
138 pub fn with_version(mut self, version: impl Into<String>) -> Self {
139 self.version = Some(version.into());
140 self
141 }
142
143 /// Set the instance ID
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use juncture_tracing::types::ServerInfo;
149 ///
150 /// let info = ServerInfo::new().with_instance_id("pod-abc123");
151 /// assert_eq!(info.instance_id, Some("pod-abc123".to_string()));
152 /// ```
153 #[must_use]
154 pub fn with_instance_id(mut self, id: impl Into<String>) -> Self {
155 self.instance_id = Some(id.into());
156 self
157 }
158}
159
160/// Cache policy for LLM response caching
161///
162/// Controls how LLM responses are cached, including the ability to customize
163/// the cache key generation function.
164///
165/// # Examples
166///
167/// ```
168/// use juncture_tracing::types::LlmCachePolicy;
169///
170/// let policy = LlmCachePolicy::default();
171/// assert!(policy.key_func.is_none());
172/// ```
173#[derive(Default)]
174pub struct LlmCachePolicy {
175 /// Optional custom cache key function
176 ///
177 /// If `None`, the default key function using `(model, messages_hash)` is used.
178 pub key_func: Option<LlmCacheKeyFn>,
179}
180
181/// Type alias for LLM cache key function
182type LlmCacheKeyFn = std::sync::Arc<dyn Fn(&LlmCacheKeyInput) -> String + Send + Sync>;
183
184impl fmt::Debug for LlmCachePolicy {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 f.debug_struct("LlmCachePolicy")
187 .field(
188 "key_func",
189 if self.key_func.is_some() {
190 &"Some(custom function)"
191 } else {
192 &"None"
193 },
194 )
195 .finish()
196 }
197}
198
199impl LlmCachePolicy {
200 /// Create a new cache policy with the default key function
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// use juncture_tracing::types::LlmCachePolicy;
206 ///
207 /// let policy = LlmCachePolicy::new();
208 /// assert!(policy.key_func.is_none());
209 /// ```
210 #[must_use]
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 /// Set a custom cache key function
216 ///
217 /// # Parameters
218 ///
219 /// * `f` - Function that generates cache keys from input
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use juncture_tracing::types::{LlmCachePolicy, LlmCacheKeyInput};
225 /// use serde_json::json;
226 ///
227 /// let policy = LlmCachePolicy::new().with_key_func(|input| {
228 /// format!("{}:{}", input.model, input.messages.len())
229 /// });
230 ///
231 /// let key_input = LlmCacheKeyInput {
232 /// model: "gpt-4".to_string(),
233 /// messages: vec![],
234 /// tools: vec![],
235 /// config: None,
236 /// };
237 /// assert!(policy.key_func.as_ref().map(|f| f(&key_input)).is_some());
238 /// ```
239 #[must_use]
240 #[allow(
241 clippy::type_complexity,
242 reason = "Function pointer type is necessary for the cache key API"
243 )]
244 pub fn with_key_func<F>(mut self, f: F) -> Self
245 where
246 F: Fn(&LlmCacheKeyInput) -> String + Send + Sync + 'static,
247 {
248 self.key_func = Some(std::sync::Arc::new(f));
249 self
250 }
251}
252
253/// Input for LLM cache key generation
254///
255/// Contains the parameters that influence cache key generation for LLM calls.
256///
257/// # Examples
258///
259/// ```
260/// use juncture_tracing::types::LlmCacheKeyInput;
261/// use serde_json::json;
262///
263/// let input = LlmCacheKeyInput {
264/// model: "gpt-4".to_string(),
265/// messages: vec![json!({"role": "user", "content": "Hello"})],
266/// tools: vec![],
267/// config: None,
268/// };
269/// ```
270#[derive(Clone, Debug)]
271pub struct LlmCacheKeyInput {
272 /// Model name
273 pub model: String,
274
275 /// Messages in the conversation
276 pub messages: Vec<serde_json::Value>,
277
278 /// Tools available in the call
279 pub tools: Vec<serde_json::Value>,
280
281 /// Optional call configuration
282 ///
283 /// Reserved for future use with `CallOptions` type.
284 /// Currently unused but kept for API compatibility.
285 pub config: Option<()>,
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use serde_json::json;
292
293 #[test]
294 fn test_server_info_default() {
295 let info = ServerInfo::default();
296 assert!(info.assistant_id.is_none());
297 assert!(info.graph_id.is_none());
298 assert!(info.user.is_none());
299 assert!(info.deployment.is_none());
300 assert!(info.version.is_none());
301 assert!(info.instance_id.is_none());
302 }
303
304 #[test]
305 fn test_server_info_builder() {
306 let info = ServerInfo::new()
307 .with_assistant_id("asst_123")
308 .with_graph_id("graph_456")
309 .with_user("user@example.com")
310 .with_deployment("production")
311 .with_version("1.0.0")
312 .with_instance_id("pod-abc123");
313
314 assert_eq!(info.assistant_id, Some("asst_123".to_string()));
315 assert_eq!(info.graph_id, Some("graph_456".to_string()));
316 assert_eq!(info.user, Some("user@example.com".to_string()));
317 assert_eq!(info.deployment, Some("production".to_string()));
318 assert_eq!(info.version, Some("1.0.0".to_string()));
319 assert_eq!(info.instance_id, Some("pod-abc123".to_string()));
320 }
321
322 #[test]
323 fn test_server_info_serialization() {
324 let info = ServerInfo {
325 assistant_id: Some("asst_123".to_string()),
326 deployment: Some("production".to_string()),
327 ..Default::default()
328 };
329
330 let json = serde_json::to_string(&info).unwrap();
331 let deserialized: ServerInfo = serde_json::from_str(&json).unwrap();
332
333 assert_eq!(deserialized.assistant_id, info.assistant_id);
334 assert_eq!(deserialized.deployment, info.deployment);
335 }
336
337 #[test]
338 fn test_llm_cache_policy_default() {
339 let policy = LlmCachePolicy::default();
340 assert!(policy.key_func.is_none());
341 }
342
343 #[test]
344 fn test_llm_cache_policy_with_custom_func() {
345 let policy = LlmCachePolicy::new()
346 .with_key_func(|input| format!("custom:{}:{}", input.model, input.messages.len()));
347
348 assert!(policy.key_func.is_some());
349
350 let input = LlmCacheKeyInput {
351 model: "gpt-4".to_string(),
352 messages: vec![json!({}), json!({})],
353 tools: vec![],
354 config: None,
355 };
356
357 let key = policy.key_func.as_ref().unwrap()(&input);
358 assert_eq!(key, "custom:gpt-4:2");
359 }
360
361 #[test]
362 fn test_llm_cache_policy_debug() {
363 let policy_without = LlmCachePolicy::default();
364 let debug_str = format!("{policy_without:?}");
365 assert!(debug_str.contains("None"));
366
367 let policy_with = LlmCachePolicy::new().with_key_func(|_| "key".to_string());
368 let debug_str = format!("{policy_with:?}");
369 assert!(debug_str.contains("Some"));
370 }
371
372 #[test]
373 fn test_llm_cache_key_input() {
374 let input = LlmCacheKeyInput {
375 model: "claude-3".to_string(),
376 messages: vec![json!({"role": "user"})],
377 tools: vec![],
378 config: None,
379 };
380
381 assert_eq!(input.model, "claude-3");
382 assert_eq!(input.messages.len(), 1);
383 assert!(input.tools.is_empty());
384 assert!(input.config.is_none());
385 }
386}
387
388// Rust guideline compliant 2026-05-19