latch_billing/
identity.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct BillingSubject {
18 pub tenant_id: Option<String>,
20
21 pub org_id: Option<String>,
23
24 pub project_id: Option<String>,
26
27 pub api_key_id: Option<String>,
29
30 pub end_user_id: Option<String>,
32
33 pub feature: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct UsageEventId {
49 pub idempotency_key: String,
54}
55
56impl UsageEventId {
57 pub fn from_attempt(
75 request_id: &str,
76 attempt_index: i32,
77 provider_id: &str,
78 ) -> Result<Self, UsageEventIdError> {
79 if attempt_index < 0 {
80 return Err(UsageEventIdError::InvalidAttemptIndex(attempt_index));
81 }
82 Ok(Self {
83 idempotency_key: format!("{request_id}:{attempt_index}:{provider_id}"),
84 })
85 }
86
87 pub fn from_raw(key: impl Into<String>) -> Self {
92 Self {
93 idempotency_key: key.into(),
94 }
95 }
96}
97
98pub struct UsageEventIdBuilder {
100 request_id: String,
101 attempt_index: i32,
102 provider_id: String,
103 step_id: Option<String>,
104 phase: Option<String>,
105}
106
107impl UsageEventIdBuilder {
108 pub fn new(request_id: &str, attempt_index: i32, provider_id: &str) -> Self {
110 Self {
111 request_id: request_id.to_string(),
112 attempt_index,
113 provider_id: provider_id.to_string(),
114 step_id: None,
115 phase: None,
116 }
117 }
118
119 pub fn step_id(mut self, id: impl Into<String>) -> Self {
121 self.step_id = Some(id.into());
122 self
123 }
124
125 pub fn phase(mut self, p: impl Into<String>) -> Self {
127 self.phase = Some(p.into());
128 self
129 }
130
131 pub fn build(self) -> Result<UsageEventId, UsageEventIdError> {
133 if self.attempt_index < 0 {
134 return Err(UsageEventIdError::InvalidAttemptIndex(self.attempt_index));
135 }
136
137 let mut key = format!(
138 "{}:{}:{}",
139 self.request_id, self.attempt_index, self.provider_id
140 );
141
142 if let Some(ref s) = self.step_id {
143 key.push(':');
144 key.push_str(s);
145 }
146
147 if let Some(ref p) = self.phase {
148 key.push(':');
149 key.push_str(p);
150 }
151
152 Ok(UsageEventId { idempotency_key: key })
153 }
154}
155
156#[derive(Debug, Clone)]
158pub enum UsageEventIdError {
159 InvalidAttemptIndex(i32),
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct CorrelationIds {
168 pub request_id: Option<String>,
170
171 pub trace_id: Option<String>,
173
174 pub session_id: Option<String>,
176
177 pub turn_id: Option<String>,
179
180 pub run_id: Option<String>,
182
183 pub step_id: Option<String>,
185
186 pub attempt_index: Option<i32>,
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn from_attempt_constructs_correct_key() {
197 let id = UsageEventId::from_attempt("req-123", 0, "openai").unwrap();
198 assert_eq!(id.idempotency_key, "req-123:0:openai");
199 }
200
201 #[test]
202 fn from_attempt_with_retry() {
203 let id = UsageEventId::from_attempt("req-123", 1, "anthropic").unwrap();
204 assert_eq!(id.idempotency_key, "req-123:1:anthropic");
205 }
206
207 #[test]
208 fn from_attempt_rejects_negative_attempt() {
209 let result = UsageEventId::from_attempt("req-123", -1, "openai");
210 assert!(matches!(result, Err(UsageEventIdError::InvalidAttemptIndex(-1))));
211 }
212
213 #[test]
214 fn from_raw_uses_caller_key() {
215 let id = UsageEventId::from_raw("custom-key-123");
216 assert_eq!(id.idempotency_key, "custom-key-123");
217 }
218
219 #[test]
220 fn billing_subject_default_all_none() {
221 let sub = BillingSubject::default();
222 assert_eq!(sub.tenant_id, None);
223 assert_eq!(sub.api_key_id, None);
224 }
225
226 #[test]
227 fn usage_event_id_builder_basic() {
228 let id = UsageEventIdBuilder::new("req-123", 0, "openai")
229 .build()
230 .unwrap();
231 assert_eq!(id.idempotency_key, "req-123:0:openai");
232 }
233
234 #[test]
235 fn usage_event_id_builder_with_step() {
236 let id = UsageEventIdBuilder::new("req-123", 0, "openai")
237 .step_id("step-1")
238 .build()
239 .unwrap();
240 assert_eq!(id.idempotency_key, "req-123:0:openai:step-1");
241 }
242
243 #[test]
244 fn usage_event_id_builder_with_phase() {
245 let id = UsageEventIdBuilder::new("req-123", 0, "openai")
246 .phase("init")
247 .build()
248 .unwrap();
249 assert_eq!(id.idempotency_key, "req-123:0:openai:init");
250 }
251}