use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BillingSubject {
pub tenant_id: Option<String>,
pub org_id: Option<String>,
pub project_id: Option<String>,
pub api_key_id: Option<String>,
pub end_user_id: Option<String>,
pub feature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageEventId {
pub idempotency_key: String,
}
impl UsageEventId {
pub fn from_attempt(
request_id: &str,
attempt_index: i32,
provider_id: &str,
) -> Result<Self, UsageEventIdError> {
if attempt_index < 0 {
return Err(UsageEventIdError::InvalidAttemptIndex(attempt_index));
}
Ok(Self {
idempotency_key: format!("{request_id}:{attempt_index}:{provider_id}"),
})
}
pub fn from_raw(key: impl Into<String>) -> Self {
Self {
idempotency_key: key.into(),
}
}
}
pub struct UsageEventIdBuilder {
request_id: String,
attempt_index: i32,
provider_id: String,
step_id: Option<String>,
phase: Option<String>,
}
impl UsageEventIdBuilder {
pub fn new(request_id: &str, attempt_index: i32, provider_id: &str) -> Self {
Self {
request_id: request_id.to_string(),
attempt_index,
provider_id: provider_id.to_string(),
step_id: None,
phase: None,
}
}
pub fn step_id(mut self, id: impl Into<String>) -> Self {
self.step_id = Some(id.into());
self
}
pub fn phase(mut self, p: impl Into<String>) -> Self {
self.phase = Some(p.into());
self
}
pub fn build(self) -> Result<UsageEventId, UsageEventIdError> {
if self.attempt_index < 0 {
return Err(UsageEventIdError::InvalidAttemptIndex(self.attempt_index));
}
let mut key = format!(
"{}:{}:{}",
self.request_id, self.attempt_index, self.provider_id
);
if let Some(ref s) = self.step_id {
key.push(':');
key.push_str(s);
}
if let Some(ref p) = self.phase {
key.push(':');
key.push_str(p);
}
Ok(UsageEventId { idempotency_key: key })
}
}
#[derive(Debug, Clone)]
pub enum UsageEventIdError {
InvalidAttemptIndex(i32),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CorrelationIds {
pub request_id: Option<String>,
pub trace_id: Option<String>,
pub session_id: Option<String>,
pub turn_id: Option<String>,
pub run_id: Option<String>,
pub step_id: Option<String>,
pub attempt_index: Option<i32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_attempt_constructs_correct_key() {
let id = UsageEventId::from_attempt("req-123", 0, "openai").unwrap();
assert_eq!(id.idempotency_key, "req-123:0:openai");
}
#[test]
fn from_attempt_with_retry() {
let id = UsageEventId::from_attempt("req-123", 1, "anthropic").unwrap();
assert_eq!(id.idempotency_key, "req-123:1:anthropic");
}
#[test]
fn from_attempt_rejects_negative_attempt() {
let result = UsageEventId::from_attempt("req-123", -1, "openai");
assert!(matches!(result, Err(UsageEventIdError::InvalidAttemptIndex(-1))));
}
#[test]
fn from_raw_uses_caller_key() {
let id = UsageEventId::from_raw("custom-key-123");
assert_eq!(id.idempotency_key, "custom-key-123");
}
#[test]
fn billing_subject_default_all_none() {
let sub = BillingSubject::default();
assert_eq!(sub.tenant_id, None);
assert_eq!(sub.api_key_id, None);
}
#[test]
fn usage_event_id_builder_basic() {
let id = UsageEventIdBuilder::new("req-123", 0, "openai")
.build()
.unwrap();
assert_eq!(id.idempotency_key, "req-123:0:openai");
}
#[test]
fn usage_event_id_builder_with_step() {
let id = UsageEventIdBuilder::new("req-123", 0, "openai")
.step_id("step-1")
.build()
.unwrap();
assert_eq!(id.idempotency_key, "req-123:0:openai:step-1");
}
#[test]
fn usage_event_id_builder_with_phase() {
let id = UsageEventIdBuilder::new("req-123", 0, "openai")
.phase("init")
.build()
.unwrap();
assert_eq!(id.idempotency_key, "req-123:0:openai:init");
}
}