use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
pub type IntegrationResult<T> = Result<T, IntegrationError>;
#[derive(Debug, thiserror::Error)]
pub enum IntegrationError {
#[error("Integration not enabled: {name}")]
NotEnabled { name: String },
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Authentication error: {0}")]
Authentication(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Rate limited: retry after {retry_after_ms}ms")]
RateLimited { retry_after_ms: u64 },
#[error("Timeout after {timeout_ms}ms")]
Timeout { timeout_ms: u64 },
#[error("Integration error: {0}")]
Other(String),
}
impl IntegrationError {
pub fn config(msg: impl Into<String>) -> Self {
Self::Configuration(msg.into())
}
pub fn connection(msg: impl Into<String>) -> Self {
Self::Connection(msg.into())
}
pub fn auth(msg: impl Into<String>) -> Self {
Self::Authentication(msg.into())
}
pub fn other(msg: impl Into<String>) -> Self {
Self::Other(msg.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmStartEvent {
pub request_id: String,
pub model: String,
pub provider: Option<String>,
pub input: serde_json::Value,
pub parameters: HashMap<String, serde_json::Value>,
pub user_id: Option<String>,
pub session_id: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub tags: Vec<String>,
pub timestamp_ms: i64,
}
impl LlmStartEvent {
pub fn new(request_id: impl Into<String>, model: impl Into<String>) -> Self {
Self {
request_id: request_id.into(),
model: model.into(),
provider: None,
input: serde_json::Value::Null,
parameters: HashMap::new(),
user_id: None,
session_id: None,
metadata: HashMap::new(),
tags: Vec::new(),
timestamp_ms: chrono::Utc::now().timestamp_millis(),
}
}
pub fn provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn input(mut self, input: serde_json::Value) -> Self {
self.input = input;
self
}
pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn param(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.parameters.insert(key.into(), value);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmEndEvent {
pub request_id: String,
pub model: String,
pub provider: Option<String>,
pub output: serde_json::Value,
pub input_tokens: Option<u32>,
pub output_tokens: Option<u32>,
pub cost_usd: Option<f64>,
pub latency_ms: u64,
pub ttft_ms: Option<u64>,
pub metadata: HashMap<String, serde_json::Value>,
pub timestamp_ms: i64,
}
impl LlmEndEvent {
pub fn new(request_id: impl Into<String>, model: impl Into<String>) -> Self {
Self {
request_id: request_id.into(),
model: model.into(),
provider: None,
output: serde_json::Value::Null,
input_tokens: None,
output_tokens: None,
cost_usd: None,
latency_ms: 0,
ttft_ms: None,
metadata: HashMap::new(),
timestamp_ms: chrono::Utc::now().timestamp_millis(),
}
}
pub fn provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn output(mut self, output: serde_json::Value) -> Self {
self.output = output;
self
}
pub fn tokens(mut self, input: u32, output: u32) -> Self {
self.input_tokens = Some(input);
self.output_tokens = Some(output);
self
}
pub fn cost(mut self, cost_usd: f64) -> Self {
self.cost_usd = Some(cost_usd);
self
}
pub fn latency(mut self, latency_ms: u64) -> Self {
self.latency_ms = latency_ms;
self
}
pub fn ttft(mut self, ttft_ms: u64) -> Self {
self.ttft_ms = Some(ttft_ms);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmErrorEvent {
pub request_id: String,
pub model: String,
pub provider: Option<String>,
pub error_message: String,
pub error_type: Option<String>,
pub status_code: Option<u16>,
pub retryable: bool,
pub metadata: HashMap<String, serde_json::Value>,
pub timestamp_ms: i64,
}
impl LlmErrorEvent {
pub fn new(
request_id: impl Into<String>,
model: impl Into<String>,
error_message: impl Into<String>,
) -> Self {
Self {
request_id: request_id.into(),
model: model.into(),
provider: None,
error_message: error_message.into(),
error_type: None,
status_code: None,
retryable: false,
metadata: HashMap::new(),
timestamp_ms: chrono::Utc::now().timestamp_millis(),
}
}
pub fn provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn error_type(mut self, error_type: impl Into<String>) -> Self {
self.error_type = Some(error_type.into());
self
}
pub fn status_code(mut self, code: u16) -> Self {
self.status_code = Some(code);
self
}
pub fn retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmStreamEvent {
pub request_id: String,
pub chunk_index: u32,
pub content: String,
pub is_final: bool,
pub tokens_so_far: Option<u32>,
pub timestamp_ms: i64,
}
impl LlmStreamEvent {
pub fn new(
request_id: impl Into<String>,
chunk_index: u32,
content: impl Into<String>,
) -> Self {
Self {
request_id: request_id.into(),
chunk_index,
content: content.into(),
is_final: false,
tokens_so_far: None,
timestamp_ms: chrono::Utc::now().timestamp_millis(),
}
}
pub fn final_chunk(mut self) -> Self {
self.is_final = true;
self
}
pub fn tokens_so_far(mut self, tokens: u32) -> Self {
self.tokens_so_far = Some(tokens);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingStartEvent {
pub request_id: String,
pub model: String,
pub provider: Option<String>,
pub input_count: usize,
pub user_id: Option<String>,
pub timestamp_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingEndEvent {
pub request_id: String,
pub model: String,
pub provider: Option<String>,
pub total_tokens: Option<u32>,
pub cost_usd: Option<f64>,
pub latency_ms: u64,
pub timestamp_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheHitEvent {
pub request_id: String,
pub cache_key: String,
pub cache_backend: String,
pub time_saved_ms: Option<u64>,
pub cost_saved_usd: Option<f64>,
pub timestamp_ms: i64,
}
#[async_trait]
pub trait Integration: Send + Sync {
fn name(&self) -> &'static str;
fn is_enabled(&self) -> bool;
async fn on_llm_start(&self, event: &LlmStartEvent) -> IntegrationResult<()>;
async fn on_llm_end(&self, event: &LlmEndEvent) -> IntegrationResult<()>;
async fn on_llm_error(&self, event: &LlmErrorEvent) -> IntegrationResult<()>;
async fn on_llm_stream(&self, event: &LlmStreamEvent) -> IntegrationResult<()> {
let _ = event;
Ok(())
}
async fn on_embedding_start(&self, event: &EmbeddingStartEvent) -> IntegrationResult<()> {
let _ = event;
Ok(())
}
async fn on_embedding_end(&self, event: &EmbeddingEndEvent) -> IntegrationResult<()> {
let _ = event;
Ok(())
}
async fn on_cache_hit(&self, event: &CacheHitEvent) -> IntegrationResult<()> {
let _ = event;
Ok(())
}
async fn flush(&self) -> IntegrationResult<()>;
async fn shutdown(&self) -> IntegrationResult<()>;
}
pub type BoxedIntegration = Arc<dyn Integration>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llm_start_event_builder() {
let event = LlmStartEvent::new("req-123", "gpt-4")
.provider("openai")
.input(serde_json::json!({"messages": []}))
.user_id("user-456")
.session_id("session-789")
.param("temperature", serde_json::json!(0.7))
.metadata("custom", serde_json::json!("value"))
.tag("production");
assert_eq!(event.request_id, "req-123");
assert_eq!(event.model, "gpt-4");
assert_eq!(event.provider, Some("openai".to_string()));
assert_eq!(event.user_id, Some("user-456".to_string()));
assert!(event.parameters.contains_key("temperature"));
assert!(event.tags.contains(&"production".to_string()));
}
#[test]
fn test_llm_end_event_builder() {
let event = LlmEndEvent::new("req-123", "gpt-4")
.provider("openai")
.output(serde_json::json!({"content": "Hello!"}))
.tokens(100, 50)
.cost(0.05)
.latency(150)
.ttft(50);
assert_eq!(event.request_id, "req-123");
assert_eq!(event.input_tokens, Some(100));
assert_eq!(event.output_tokens, Some(50));
assert_eq!(event.cost_usd, Some(0.05));
assert_eq!(event.latency_ms, 150);
assert_eq!(event.ttft_ms, Some(50));
}
#[test]
fn test_llm_error_event_builder() {
let event = LlmErrorEvent::new("req-123", "gpt-4", "Rate limited")
.provider("openai")
.error_type("RateLimitError")
.status_code(429)
.retryable(true);
assert_eq!(event.request_id, "req-123");
assert_eq!(event.error_message, "Rate limited");
assert_eq!(event.error_type, Some("RateLimitError".to_string()));
assert_eq!(event.status_code, Some(429));
assert!(event.retryable);
}
#[test]
fn test_llm_stream_event_builder() {
let event = LlmStreamEvent::new("req-123", 0, "Hello")
.tokens_so_far(5)
.final_chunk();
assert_eq!(event.request_id, "req-123");
assert_eq!(event.chunk_index, 0);
assert_eq!(event.content, "Hello");
assert!(event.is_final);
assert_eq!(event.tokens_so_far, Some(5));
}
#[test]
fn test_integration_error_constructors() {
let err = IntegrationError::config("Invalid API key");
assert!(matches!(err, IntegrationError::Configuration(_)));
let err = IntegrationError::connection("Connection refused");
assert!(matches!(err, IntegrationError::Connection(_)));
let err = IntegrationError::auth("Invalid token");
assert!(matches!(err, IntegrationError::Authentication(_)));
let err = IntegrationError::other("Unknown error");
assert!(matches!(err, IntegrationError::Other(_)));
}
}