1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::Duration;
12use utoipa::ToSchema;
13use uuid::Uuid;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
21#[schema(value_type = String, format = "uuid")]
22pub struct TenantId(pub Uuid);
23
24impl std::fmt::Display for TenantId {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}", self.0)
27 }
28}
29
30impl TenantId {
31 pub fn new() -> Self {
33 Self(Uuid::new_v4())
34 }
35}
36
37impl Default for TenantId {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
49#[serde(rename_all = "lowercase")]
50pub enum ApiKeyRole {
51 Admin,
53 Operator,
55 Viewer,
57}
58
59impl ApiKeyRole {
60 #[must_use]
62 pub fn has_permission(self, required: ApiKeyRole) -> bool {
63 self.privilege_level() >= required.privilege_level()
64 }
65
66 fn privilege_level(self) -> u8 {
68 match self {
69 Self::Admin => 2,
70 Self::Operator => 1,
71 Self::Viewer => 0,
72 }
73 }
74}
75
76impl std::fmt::Display for ApiKeyRole {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Admin => write!(f, "admin"),
80 Self::Operator => write!(f, "operator"),
81 Self::Viewer => write!(f, "viewer"),
82 }
83 }
84}
85
86impl std::str::FromStr for ApiKeyRole {
87 type Err = String;
88
89 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
90 match s.to_lowercase().as_str() {
91 "admin" => Ok(Self::Admin),
92 "operator" => Ok(Self::Operator),
93 "viewer" => Ok(Self::Viewer),
94 _ => Err(format!("unknown role: {s}")),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
101pub struct ApiKeyRecord {
102 #[schema(value_type = String, format = "uuid")]
104 pub id: Uuid,
105 pub tenant_id: TenantId,
107 pub name: String,
109 #[serde(skip_serializing)]
111 pub key_hash: String,
112 pub key_prefix: String,
114 pub role: ApiKeyRole,
116 #[schema(value_type = String, format = "date-time")]
118 pub created_at: DateTime<Utc>,
119 #[schema(value_type = String, format = "date-time")]
121 pub revoked_at: Option<DateTime<Utc>>,
122}
123
124#[derive(Debug, Clone)]
126pub struct AuthContext {
127 pub tenant_id: TenantId,
129 pub role: ApiKeyRole,
131 pub key_id: Option<Uuid>,
133}
134
135pub const VOTING_RESULT_KEY: &str = "voting_result";
141pub const VOTING_MAJORITY: &str = "majority";
143pub const VOTING_SINGLE_DETECTOR: &str = "single_detector";
145
146#[must_use]
150pub fn is_auxiliary_finding_type(finding_type: &str) -> bool {
151 matches!(
152 finding_type,
153 "is_hypothetical"
154 | "is_incentive"
155 | "is_urgent"
156 | "is_systemic"
157 | "is_covert"
158 | "is_immoral"
159 | "is_repeated_token"
160 | "context_flooding"
161 )
162}
163
164#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum OperatingPoint {
171 HighRecall,
173 #[default]
175 Balanced,
176 HighPrecision,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema)]
182pub enum SecuritySeverity {
183 Info,
185 Low,
187 Medium,
189 High,
191 Critical,
193}
194
195impl std::fmt::Display for SecuritySeverity {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 match self {
198 Self::Info => write!(f, "Info"),
199 Self::Low => write!(f, "Low"),
200 Self::Medium => write!(f, "Medium"),
201 Self::High => write!(f, "High"),
202 Self::Critical => write!(f, "Critical"),
203 }
204 }
205}
206
207impl std::str::FromStr for SecuritySeverity {
208 type Err = String;
209
210 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
211 match s {
212 "Info" | "info" | "INFO" => Ok(Self::Info),
213 "Low" | "low" | "LOW" => Ok(Self::Low),
214 "Medium" | "medium" | "MEDIUM" => Ok(Self::Medium),
215 "High" | "high" | "HIGH" => Ok(Self::High),
216 "Critical" | "critical" | "CRITICAL" => Ok(Self::Critical),
217 _ => Err(format!("unknown severity: {s}")),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
224pub struct SecurityFinding {
225 #[schema(value_type = String, format = "uuid")]
227 pub id: Uuid,
228 pub severity: SecuritySeverity,
230 pub finding_type: String,
232 pub description: String,
234 #[schema(value_type = String, format = "date-time")]
236 pub detected_at: DateTime<Utc>,
237 pub confidence_score: f64,
239 pub location: Option<String>,
241 #[schema(value_type = Object)]
243 pub metadata: HashMap<String, String>,
244 pub requires_alert: bool,
246}
247
248impl SecurityFinding {
249 pub fn new(
251 severity: SecuritySeverity,
252 finding_type: String,
253 description: String,
254 confidence_score: f64,
255 ) -> Self {
256 let requires_alert = matches!(
257 severity,
258 SecuritySeverity::Critical | SecuritySeverity::High
259 );
260 Self {
261 id: Uuid::new_v4(),
262 severity,
263 finding_type,
264 description,
265 detected_at: Utc::now(),
266 confidence_score,
267 location: None,
268 metadata: HashMap::new(),
269 requires_alert,
270 }
271 }
272
273 pub fn with_location(mut self, location: String) -> Self {
275 self.location = Some(location);
276 self
277 }
278
279 pub fn with_metadata(mut self, key: String, value: String) -> Self {
281 self.metadata.insert(key, value);
282 self
283 }
284
285 pub fn with_alert_required(mut self, requires_alert: bool) -> Self {
287 self.requires_alert = requires_alert;
288 self
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
298pub enum LLMProvider {
299 OpenAI,
300 Anthropic,
301 VLLm,
302 SGLang,
303 TGI,
304 Ollama,
305 AzureOpenAI,
306 Bedrock,
307 Custom(String),
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
316pub struct TraceSpan {
317 #[schema(value_type = String, format = "uuid")]
319 pub trace_id: Uuid,
320 #[schema(value_type = String, format = "uuid")]
322 pub span_id: Uuid,
323 #[schema(value_type = String, format = "uuid")]
325 pub parent_span_id: Option<Uuid>,
326 pub tenant_id: TenantId,
328 pub operation_name: String,
330 #[schema(value_type = String, format = "date-time")]
332 pub start_time: DateTime<Utc>,
333 #[schema(value_type = String, format = "date-time")]
335 pub end_time: Option<DateTime<Utc>>,
336 pub provider: LLMProvider,
338 pub model_name: String,
340 pub prompt: String,
342 pub response: Option<String>,
344 pub prompt_tokens: Option<u32>,
346 pub completion_tokens: Option<u32>,
348 pub total_tokens: Option<u32>,
350 pub time_to_first_token_ms: Option<u64>,
352 pub duration_ms: Option<u64>,
354 pub status_code: Option<u16>,
356 pub error_message: Option<String>,
358 pub estimated_cost_usd: Option<f64>,
360 pub security_score: Option<u8>,
362 pub security_findings: Vec<SecurityFinding>,
364 pub tags: HashMap<String, String>,
366 pub events: Vec<SpanEvent>,
368 #[serde(default)]
370 pub agent_actions: Vec<AgentAction>,
371}
372
373impl TraceSpan {
374 pub fn new(
376 trace_id: Uuid,
377 tenant_id: TenantId,
378 operation_name: String,
379 provider: LLMProvider,
380 model_name: String,
381 prompt: String,
382 ) -> Self {
383 Self {
384 trace_id,
385 span_id: Uuid::new_v4(),
386 parent_span_id: None,
387 tenant_id,
388 operation_name,
389 start_time: Utc::now(),
390 end_time: None,
391 provider,
392 model_name,
393 prompt,
394 response: None,
395 prompt_tokens: None,
396 completion_tokens: None,
397 total_tokens: None,
398 time_to_first_token_ms: None,
399 duration_ms: None,
400 status_code: None,
401 error_message: None,
402 estimated_cost_usd: None,
403 security_score: None,
404 security_findings: Vec::new(),
405 tags: HashMap::new(),
406 events: Vec::new(),
407 agent_actions: Vec::new(),
408 }
409 }
410
411 pub fn finish_with_response(mut self, response: String) -> Self {
413 self.response = Some(response);
414 let end_time = Utc::now();
415 self.end_time = Some(end_time);
416 self.duration_ms =
417 Some((end_time.signed_duration_since(self.start_time)).num_milliseconds() as u64);
418 self
419 }
420
421 pub fn finish_with_error(mut self, error: String, status_code: Option<u16>) -> Self {
423 self.error_message = Some(error);
424 self.status_code = status_code;
425 let end_time = Utc::now();
426 self.end_time = Some(end_time);
427 self.duration_ms =
428 Some((end_time.signed_duration_since(self.start_time)).num_milliseconds() as u64);
429 self
430 }
431
432 pub fn add_security_finding(&mut self, finding: SecurityFinding) {
434 self.security_findings.push(finding);
435 let max_score = self
436 .security_findings
437 .iter()
438 .map(|f| {
439 let base = match f.severity {
440 SecuritySeverity::Critical => 95,
441 SecuritySeverity::High => 80,
442 SecuritySeverity::Medium => 60,
443 SecuritySeverity::Low => 30,
444 SecuritySeverity::Info => 10,
445 };
446 if is_auxiliary_finding_type(&f.finding_type) {
449 return base.min(30);
450 }
451 if f.metadata
453 .get(VOTING_RESULT_KEY)
454 .is_some_and(|v| v == VOTING_SINGLE_DETECTOR)
455 {
456 base.min(60)
457 } else {
458 base
459 }
460 })
461 .max()
462 .unwrap_or(0);
463 self.security_score = Some(max_score as u8);
464 }
465
466 pub fn add_event(&mut self, event: SpanEvent) {
468 self.events.push(event);
469 }
470
471 pub fn add_agent_action(&mut self, action: AgentAction) {
473 self.agent_actions.push(action);
474 }
475
476 #[must_use]
478 pub fn tool_calls(&self) -> Vec<&AgentAction> {
479 self.agent_actions
480 .iter()
481 .filter(|a| a.action_type == AgentActionType::ToolCall)
482 .collect()
483 }
484
485 #[must_use]
487 pub fn web_accesses(&self) -> Vec<&AgentAction> {
488 self.agent_actions
489 .iter()
490 .filter(|a| a.action_type == AgentActionType::WebAccess)
491 .collect()
492 }
493
494 #[must_use]
496 pub fn commands(&self) -> Vec<&AgentAction> {
497 self.agent_actions
498 .iter()
499 .filter(|a| a.action_type == AgentActionType::CommandExecution)
500 .collect()
501 }
502
503 #[must_use]
505 pub fn has_tool_calls(&self) -> bool {
506 self.agent_actions
507 .iter()
508 .any(|a| a.action_type == AgentActionType::ToolCall)
509 }
510
511 #[must_use]
513 pub fn has_web_access(&self) -> bool {
514 self.agent_actions
515 .iter()
516 .any(|a| a.action_type == AgentActionType::WebAccess)
517 }
518
519 #[must_use]
521 pub fn has_commands(&self) -> bool {
522 self.agent_actions
523 .iter()
524 .any(|a| a.action_type == AgentActionType::CommandExecution)
525 }
526
527 pub fn duration(&self) -> Option<Duration> {
529 self.end_time.map(|end_time| {
530 Duration::from_millis(
531 end_time
532 .signed_duration_since(self.start_time)
533 .num_milliseconds() as u64,
534 )
535 })
536 }
537
538 pub fn is_complete(&self) -> bool {
540 self.end_time.is_some()
541 }
542
543 pub fn is_failed(&self) -> bool {
545 self.error_message.is_some()
546 }
547}
548
549pub const AGENT_ACTION_RESULT_MAX_BYTES: usize = 4096;
555
556#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
558#[serde(rename_all = "snake_case")]
559pub enum AgentActionType {
560 ToolCall,
562 SkillInvocation,
564 CommandExecution,
566 WebAccess,
568 FileAccess,
570}
571
572impl std::fmt::Display for AgentActionType {
573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574 match self {
575 Self::ToolCall => write!(f, "tool_call"),
576 Self::SkillInvocation => write!(f, "skill_invocation"),
577 Self::CommandExecution => write!(f, "command_execution"),
578 Self::WebAccess => write!(f, "web_access"),
579 Self::FileAccess => write!(f, "file_access"),
580 }
581 }
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
586pub struct AgentAction {
587 #[schema(value_type = String, format = "uuid")]
589 pub id: Uuid,
590 pub action_type: AgentActionType,
592 pub name: String,
594 #[serde(default)]
596 pub arguments: Option<String>,
597 #[serde(default)]
599 pub result: Option<String>,
600 #[serde(default)]
602 pub duration_ms: Option<u64>,
603 #[serde(default = "default_true")]
605 pub success: bool,
606 #[serde(default)]
608 pub exit_code: Option<i32>,
609 #[serde(default)]
611 pub http_method: Option<String>,
612 #[serde(default)]
614 pub http_status: Option<u16>,
615 #[serde(default)]
617 pub file_operation: Option<String>,
618 #[schema(value_type = String, format = "date-time")]
620 pub timestamp: DateTime<Utc>,
621 #[schema(value_type = Object)]
623 pub metadata: HashMap<String, String>,
624}
625
626fn default_true() -> bool {
627 true
628}
629
630impl AgentAction {
631 pub fn new(action_type: AgentActionType, name: String) -> Self {
633 Self {
634 id: Uuid::new_v4(),
635 action_type,
636 name,
637 arguments: None,
638 result: None,
639 duration_ms: None,
640 success: true,
641 exit_code: None,
642 http_method: None,
643 http_status: None,
644 file_operation: None,
645 timestamp: Utc::now(),
646 metadata: HashMap::new(),
647 }
648 }
649
650 pub fn with_arguments(mut self, arguments: String) -> Self {
652 self.arguments = Some(arguments);
653 self
654 }
655
656 pub fn with_result(mut self, result: String) -> Self {
658 if result.len() > AGENT_ACTION_RESULT_MAX_BYTES {
659 self.result = Some(result[..AGENT_ACTION_RESULT_MAX_BYTES].to_string());
660 } else {
661 self.result = Some(result);
662 }
663 self
664 }
665
666 pub fn with_duration_ms(mut self, ms: u64) -> Self {
668 self.duration_ms = Some(ms);
669 self
670 }
671
672 pub fn with_failure(mut self) -> Self {
674 self.success = false;
675 self
676 }
677
678 pub fn with_exit_code(mut self, code: i32) -> Self {
680 self.exit_code = Some(code);
681 self
682 }
683
684 pub fn with_http(mut self, method: String, status: u16) -> Self {
686 self.http_method = Some(method);
687 self.http_status = Some(status);
688 self
689 }
690
691 pub fn with_file_operation(mut self, op: String) -> Self {
693 self.file_operation = Some(op);
694 self
695 }
696
697 pub fn with_metadata(mut self, key: String, value: String) -> Self {
699 self.metadata.insert(key, value);
700 self
701 }
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
706pub struct SpanEvent {
707 #[schema(value_type = String, format = "uuid")]
709 pub id: Uuid,
710 #[schema(value_type = String, format = "date-time")]
712 pub timestamp: DateTime<Utc>,
713 pub event_type: String,
715 pub description: String,
717 #[schema(value_type = Object)]
719 pub data: HashMap<String, String>,
720}
721
722impl SpanEvent {
723 pub fn new(event_type: String, description: String) -> Self {
725 Self {
726 id: Uuid::new_v4(),
727 timestamp: Utc::now(),
728 event_type,
729 description,
730 data: HashMap::new(),
731 }
732 }
733
734 pub fn with_data(mut self, key: String, value: String) -> Self {
736 self.data.insert(key, value);
737 self
738 }
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
743pub struct TraceEvent {
744 #[schema(value_type = String, format = "uuid")]
746 pub trace_id: Uuid,
747 pub tenant_id: TenantId,
749 pub spans: Vec<TraceSpan>,
751 #[schema(value_type = String, format = "date-time")]
753 pub created_at: DateTime<Utc>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
762pub struct Tenant {
763 pub id: TenantId,
765 pub name: String,
767 pub api_token: String,
769 pub plan: String,
771 #[schema(value_type = String, format = "date-time")]
773 pub created_at: DateTime<Utc>,
774 #[schema(value_type = Object)]
776 pub config: serde_json::Value,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
781pub struct TenantConfig {
782 pub tenant_id: TenantId,
784 #[schema(value_type = Object)]
786 pub security_thresholds: HashMap<String, f64>,
787 #[schema(value_type = Object)]
789 pub feature_flags: HashMap<String, bool>,
790 pub monitoring_scope: MonitoringScope,
792 pub rate_limit_rpm: Option<u32>,
794 pub monthly_budget: Option<f64>,
796}
797
798#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
800#[serde(rename_all = "snake_case")]
801pub enum MonitoringScope {
802 #[default]
804 Hybrid,
805 InputOnly,
807 OutputOnly,
809}
810
811impl std::fmt::Display for MonitoringScope {
812 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
813 let s = match self {
814 Self::Hybrid => "hybrid",
815 Self::InputOnly => "input_only",
816 Self::OutputOnly => "output_only",
817 };
818 f.write_str(s)
819 }
820}
821
822impl std::str::FromStr for MonitoringScope {
823 type Err = String;
824
825 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
826 match s.trim().to_ascii_lowercase().as_str() {
827 "hybrid" => Ok(Self::Hybrid),
828 "input_only" => Ok(Self::InputOnly),
829 "output_only" => Ok(Self::OutputOnly),
830 other => Err(format!("Unknown monitoring scope: {other}")),
831 }
832 }
833}
834
835#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
837pub struct AuditEvent {
838 #[schema(value_type = String, format = "uuid")]
840 pub id: Uuid,
841 pub tenant_id: TenantId,
843 pub event_type: String,
845 pub actor: String,
847 pub resource: String,
849 #[schema(value_type = Object)]
851 pub data: serde_json::Value,
852 #[schema(value_type = String, format = "date-time")]
854 pub timestamp: DateTime<Utc>,
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
862pub struct ComplianceReportRecord {
863 #[schema(value_type = String, format = "uuid")]
865 pub id: Uuid,
866 pub tenant_id: TenantId,
868 pub report_type: String,
870 pub status: String,
872 #[schema(value_type = String, format = "date-time")]
874 pub period_start: DateTime<Utc>,
875 #[schema(value_type = String, format = "date-time")]
877 pub period_end: DateTime<Utc>,
878 #[schema(value_type = String, format = "date-time")]
880 pub created_at: DateTime<Utc>,
881 #[schema(value_type = String, format = "date-time")]
883 pub completed_at: Option<DateTime<Utc>>,
884 #[schema(value_type = Object)]
886 pub content: Option<serde_json::Value>,
887 pub error: Option<String>,
889}
890
891#[derive(Debug, Clone, ToSchema)]
893pub struct ReportQuery {
894 pub tenant_id: TenantId,
896 pub limit: Option<u32>,
898 pub offset: Option<u32>,
900}
901
902impl ReportQuery {
903 pub fn new(tenant_id: TenantId) -> Self {
905 Self {
906 tenant_id,
907 limit: None,
908 offset: None,
909 }
910 }
911
912 pub fn with_limit(mut self, limit: u32) -> Self {
914 self.limit = Some(limit);
915 self
916 }
917
918 pub fn with_offset(mut self, offset: u32) -> Self {
920 self.offset = Some(offset);
921 self
922 }
923}
924
925#[derive(Debug, Clone, ToSchema)]
927pub struct AuditQuery {
928 pub tenant_id: TenantId,
930 pub event_type: Option<String>,
932 #[schema(value_type = String, format = "date-time")]
934 pub start_time: Option<DateTime<Utc>>,
935 #[schema(value_type = String, format = "date-time")]
937 pub end_time: Option<DateTime<Utc>>,
938 pub limit: Option<u32>,
940 pub offset: Option<u32>,
942}
943
944impl AuditQuery {
945 pub fn new(tenant_id: TenantId) -> Self {
947 Self {
948 tenant_id,
949 event_type: None,
950 start_time: None,
951 end_time: None,
952 limit: None,
953 offset: None,
954 }
955 }
956
957 pub fn with_event_type(mut self, event_type: String) -> Self {
959 self.event_type = Some(event_type);
960 self
961 }
962
963 pub fn with_time_range(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
965 self.start_time = Some(start);
966 self.end_time = Some(end);
967 self
968 }
969
970 pub fn with_limit(mut self, limit: u32) -> Self {
972 self.limit = Some(limit);
973 self
974 }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
983pub struct ProxyConfig {
984 pub listen_addr: String,
986 pub upstream_url: String,
988 #[serde(default)]
990 pub storage: StorageConfig,
991 pub timeout_ms: u64,
993 pub connection_timeout_ms: u64,
995 pub max_connections: u32,
997 pub enable_tls: bool,
999 pub tls_cert_file: Option<String>,
1001 pub tls_key_file: Option<String>,
1003 pub enable_security_analysis: bool,
1005 pub enable_trace_storage: bool,
1007 pub enable_streaming: bool,
1009 pub max_request_size_bytes: u64,
1011 pub security_analysis_timeout_ms: u64,
1013 pub trace_storage_timeout_ms: u64,
1015 pub rate_limiting: RateLimitConfig,
1017 pub circuit_breaker: CircuitBreakerConfig,
1019 pub health_check: HealthCheckConfig,
1021 #[serde(default)]
1023 pub logging: LoggingConfig,
1024 #[serde(default)]
1026 pub cost_estimation: CostEstimationConfig,
1027 #[serde(default)]
1029 pub alerts: AlertConfig,
1030 #[serde(default)]
1032 pub cost_caps: CostCapConfig,
1033 #[serde(default)]
1035 pub security_analysis: SecurityAnalysisConfig,
1036 #[serde(default)]
1038 pub enforcement: EnforcementConfig,
1039 #[serde(default)]
1041 pub otel_ingest: OtelIngestConfig,
1042 #[serde(default)]
1044 pub auth: AuthConfig,
1045 #[serde(default)]
1047 pub grpc: GrpcConfig,
1048 #[serde(default)]
1050 pub anomaly_detection: AnomalyDetectionConfig,
1051 #[serde(default)]
1053 pub streaming_analysis: StreamingAnalysisConfig,
1054 #[serde(default)]
1056 pub pii: PiiConfig,
1057 #[serde(default)]
1059 pub output_safety: OutputSafetyConfig,
1060 #[serde(default)]
1062 pub shutdown: ShutdownConfig,
1063}
1064
1065impl Default for ProxyConfig {
1066 fn default() -> Self {
1067 Self {
1068 listen_addr: "0.0.0.0:8080".to_string(),
1069 upstream_url: "https://api.openai.com".to_string(),
1070 storage: StorageConfig::default(),
1071 timeout_ms: 30000,
1072 connection_timeout_ms: 5000,
1073 max_connections: 1000,
1074 enable_tls: false,
1075 tls_cert_file: None,
1076 tls_key_file: None,
1077 enable_security_analysis: true,
1078 enable_trace_storage: true,
1079 enable_streaming: true,
1080 max_request_size_bytes: 50 * 1024 * 1024, security_analysis_timeout_ms: 5000,
1082 trace_storage_timeout_ms: 10000,
1083 rate_limiting: RateLimitConfig::default(),
1084 circuit_breaker: CircuitBreakerConfig::default(),
1085 health_check: HealthCheckConfig::default(),
1086 logging: LoggingConfig::default(),
1087 cost_estimation: CostEstimationConfig::default(),
1088 alerts: AlertConfig::default(),
1089 cost_caps: CostCapConfig::default(),
1090 security_analysis: SecurityAnalysisConfig::default(),
1091 enforcement: EnforcementConfig::default(),
1092 otel_ingest: OtelIngestConfig::default(),
1093 auth: AuthConfig::default(),
1094 grpc: GrpcConfig::default(),
1095 anomaly_detection: AnomalyDetectionConfig::default(),
1096 streaming_analysis: StreamingAnalysisConfig::default(),
1097 pii: PiiConfig::default(),
1098 output_safety: OutputSafetyConfig::default(),
1099 shutdown: ShutdownConfig::default(),
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1106pub struct StorageConfig {
1107 #[serde(default = "default_storage_profile")]
1109 pub profile: String,
1110 #[serde(default = "default_database_path")]
1112 pub database_path: String,
1113 #[serde(default)]
1115 pub clickhouse_url: Option<String>,
1116 #[serde(default)]
1118 pub clickhouse_database: Option<String>,
1119 #[serde(default)]
1121 pub postgres_url: Option<String>,
1122 #[serde(default)]
1124 pub redis_url: Option<String>,
1125 #[serde(default = "default_auto_migrate")]
1130 pub auto_migrate: bool,
1131}
1132
1133fn default_storage_profile() -> String {
1134 "lite".to_string()
1135}
1136
1137fn default_database_path() -> String {
1138 "llmtrace.db".to_string()
1139}
1140
1141fn default_auto_migrate() -> bool {
1142 true
1143}
1144
1145impl Default for StorageConfig {
1146 fn default() -> Self {
1147 Self {
1148 profile: default_storage_profile(),
1149 database_path: default_database_path(),
1150 clickhouse_url: None,
1151 clickhouse_database: None,
1152 postgres_url: None,
1153 redis_url: None,
1154 auto_migrate: default_auto_migrate(),
1155 }
1156 }
1157}
1158
1159#[derive(Debug, Clone, Serialize, Deserialize)]
1161pub struct TenantRateLimitOverride {
1162 pub requests_per_second: u32,
1164 pub burst_size: u32,
1166}
1167
1168#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct RateLimitConfig {
1171 pub enabled: bool,
1173 pub requests_per_second: u32,
1175 pub burst_size: u32,
1177 pub window_seconds: u32,
1179 #[serde(default)]
1181 pub tenant_overrides: HashMap<String, TenantRateLimitOverride>,
1182}
1183
1184impl Default for RateLimitConfig {
1185 fn default() -> Self {
1186 Self {
1187 enabled: true,
1188 requests_per_second: 100,
1189 burst_size: 200,
1190 window_seconds: 60,
1191 tenant_overrides: HashMap::new(),
1192 }
1193 }
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize)]
1198pub struct CircuitBreakerConfig {
1199 pub enabled: bool,
1201 pub failure_threshold: u32,
1203 pub recovery_timeout_ms: u64,
1205 pub half_open_max_calls: u32,
1207}
1208
1209impl Default for CircuitBreakerConfig {
1210 fn default() -> Self {
1211 Self {
1212 enabled: true,
1213 failure_threshold: 10,
1214 recovery_timeout_ms: 30000,
1215 half_open_max_calls: 3,
1216 }
1217 }
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1222pub struct HealthCheckConfig {
1223 pub enabled: bool,
1225 pub path: String,
1227 pub interval_seconds: u32,
1229 pub timeout_ms: u64,
1231 pub retries: u32,
1233}
1234
1235impl Default for HealthCheckConfig {
1236 fn default() -> Self {
1237 Self {
1238 enabled: true,
1239 path: "/health".to_string(),
1240 interval_seconds: 10,
1241 timeout_ms: 5000,
1242 retries: 3,
1243 }
1244 }
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1249pub struct CostEstimationConfig {
1250 #[serde(default = "default_cost_estimation_enabled")]
1252 pub enabled: bool,
1253 #[serde(default)]
1259 pub pricing_file: Option<String>,
1260 #[serde(default)]
1262 pub custom_models: HashMap<String, ModelPricingConfig>,
1263}
1264
1265fn default_cost_estimation_enabled() -> bool {
1266 true
1267}
1268
1269impl Default for CostEstimationConfig {
1270 fn default() -> Self {
1271 Self {
1272 enabled: default_cost_estimation_enabled(),
1273 pricing_file: None,
1274 custom_models: HashMap::new(),
1275 }
1276 }
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize)]
1281pub struct ModelPricingConfig {
1282 pub input_per_million: f64,
1284 pub output_per_million: f64,
1286}
1287
1288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
1294#[serde(rename_all = "lowercase")]
1295pub enum BudgetWindow {
1296 Hourly,
1298 Daily,
1300 Weekly,
1302 Monthly,
1304}
1305
1306impl BudgetWindow {
1307 #[must_use]
1309 pub fn duration_secs(&self) -> u64 {
1310 match self {
1311 Self::Hourly => 3_600,
1312 Self::Daily => 86_400,
1313 Self::Weekly => 604_800,
1314 Self::Monthly => 2_592_000, }
1316 }
1317
1318 #[must_use]
1321 pub fn cache_ttl(&self) -> Duration {
1322 Duration::from_secs(self.duration_secs() + 300)
1323 }
1324}
1325
1326impl std::fmt::Display for BudgetWindow {
1327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1328 match self {
1329 Self::Hourly => write!(f, "hourly"),
1330 Self::Daily => write!(f, "daily"),
1331 Self::Weekly => write!(f, "weekly"),
1332 Self::Monthly => write!(f, "monthly"),
1333 }
1334 }
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1339pub struct BudgetCap {
1340 pub window: BudgetWindow,
1342 pub hard_limit_usd: f64,
1344 #[serde(default)]
1346 pub soft_limit_usd: Option<f64>,
1347}
1348
1349#[derive(Debug, Clone, Serialize, Deserialize)]
1351pub struct TokenCap {
1352 #[serde(default)]
1354 pub max_prompt_tokens: Option<u32>,
1355 #[serde(default)]
1357 pub max_completion_tokens: Option<u32>,
1358 #[serde(default)]
1360 pub max_total_tokens: Option<u32>,
1361}
1362
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1365pub struct AgentCostCap {
1366 pub agent_id: String,
1368 #[serde(default)]
1370 pub budget_caps: Vec<BudgetCap>,
1371 #[serde(default)]
1373 pub token_cap: Option<TokenCap>,
1374}
1375
1376#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1378pub struct CostCapConfig {
1379 #[serde(default)]
1381 pub enabled: bool,
1382 #[serde(default)]
1384 pub default_budget_caps: Vec<BudgetCap>,
1385 #[serde(default)]
1387 pub default_token_cap: Option<TokenCap>,
1388 #[serde(default)]
1390 pub agents: Vec<AgentCostCap>,
1391}
1392
1393#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1399#[serde(rename_all = "snake_case")]
1400pub enum AnomalyType {
1401 CostSpike,
1403 TokenSpike,
1405 VelocitySpike,
1407 LatencySpike,
1409}
1410
1411impl std::fmt::Display for AnomalyType {
1412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1413 match self {
1414 Self::CostSpike => write!(f, "cost_spike"),
1415 Self::TokenSpike => write!(f, "token_spike"),
1416 Self::VelocitySpike => write!(f, "velocity_spike"),
1417 Self::LatencySpike => write!(f, "latency_spike"),
1418 }
1419 }
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1440pub struct AnomalyDetectionConfig {
1441 #[serde(default)]
1443 pub enabled: bool,
1444 #[serde(default = "default_anomaly_window_size")]
1446 pub window_size: usize,
1447 #[serde(default = "default_anomaly_sigma_threshold")]
1449 pub sigma_threshold: f64,
1450 #[serde(default = "default_true_flag")]
1452 pub check_cost: bool,
1453 #[serde(default = "default_true_flag")]
1455 pub check_tokens: bool,
1456 #[serde(default = "default_true_flag")]
1458 pub check_velocity: bool,
1459 #[serde(default = "default_true_flag")]
1461 pub check_latency: bool,
1462}
1463
1464fn default_anomaly_window_size() -> usize {
1465 100
1466}
1467
1468fn default_anomaly_sigma_threshold() -> f64 {
1469 3.0
1470}
1471
1472fn default_true_flag() -> bool {
1473 true
1474}
1475
1476impl Default for AnomalyDetectionConfig {
1477 fn default() -> Self {
1478 Self {
1479 enabled: false,
1480 window_size: default_anomaly_window_size(),
1481 sigma_threshold: default_anomaly_sigma_threshold(),
1482 check_cost: true,
1483 check_tokens: true,
1484 check_velocity: true,
1485 check_latency: true,
1486 }
1487 }
1488}
1489
1490#[derive(Debug, Clone, Serialize, Deserialize)]
1505pub struct StreamingAnalysisConfig {
1506 #[serde(default)]
1508 pub enabled: bool,
1509 #[serde(default = "default_streaming_token_interval")]
1511 pub token_interval: u32,
1512 #[serde(default)]
1514 pub output_enabled: bool,
1515 #[serde(default)]
1517 pub early_stop_on_critical: bool,
1518}
1519
1520fn default_streaming_token_interval() -> u32 {
1521 50
1522}
1523
1524impl Default for StreamingAnalysisConfig {
1525 fn default() -> Self {
1526 Self {
1527 enabled: false,
1528 token_interval: default_streaming_token_interval(),
1529 output_enabled: false,
1530 early_stop_on_critical: false,
1531 }
1532 }
1533}
1534
1535#[derive(Debug, Clone, Serialize, Deserialize)]
1559pub struct OutputSafetyConfig {
1560 #[serde(default)]
1562 pub enabled: bool,
1563 #[serde(default)]
1565 pub toxicity_enabled: bool,
1566 #[serde(default = "default_toxicity_threshold")]
1568 pub toxicity_threshold: f32,
1569 #[serde(default)]
1571 pub block_on_critical: bool,
1572 #[serde(default)]
1577 pub hallucination_enabled: bool,
1578 #[serde(default = "default_hallucination_model")]
1580 pub hallucination_model: String,
1581 #[serde(default = "default_hallucination_threshold")]
1584 pub hallucination_threshold: f32,
1585 #[serde(default = "default_hallucination_min_response_length")]
1588 pub hallucination_min_response_length: usize,
1589}
1590
1591fn default_toxicity_threshold() -> f32 {
1592 0.7
1593}
1594
1595fn default_hallucination_model() -> String {
1596 "vectara/hallucination_evaluation_model".to_string()
1597}
1598
1599fn default_hallucination_threshold() -> f32 {
1600 0.5
1601}
1602
1603fn default_hallucination_min_response_length() -> usize {
1604 50
1605}
1606
1607impl Default for OutputSafetyConfig {
1608 fn default() -> Self {
1609 Self {
1610 enabled: false,
1611 toxicity_enabled: false,
1612 toxicity_threshold: default_toxicity_threshold(),
1613 block_on_critical: false,
1614 hallucination_enabled: false,
1615 hallucination_model: default_hallucination_model(),
1616 hallucination_threshold: default_hallucination_threshold(),
1617 hallucination_min_response_length: default_hallucination_min_response_length(),
1618 }
1619 }
1620}
1621
1622#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1628#[serde(rename_all = "snake_case")]
1629pub enum PiiAction {
1630 #[default]
1632 AlertOnly,
1633 AlertAndRedact,
1635 RedactSilent,
1637}
1638
1639impl std::fmt::Display for PiiAction {
1640 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1641 match self {
1642 Self::AlertOnly => write!(f, "alert_only"),
1643 Self::AlertAndRedact => write!(f, "alert_and_redact"),
1644 Self::RedactSilent => write!(f, "redact_silent"),
1645 }
1646 }
1647}
1648
1649#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1660pub struct PiiConfig {
1661 #[serde(default)]
1663 pub action: PiiAction,
1664}
1665
1666#[derive(Debug, Clone, Serialize, Deserialize)]
1680pub struct ShutdownConfig {
1681 #[serde(default = "default_shutdown_timeout_seconds")]
1685 pub timeout_seconds: u64,
1686}
1687
1688fn default_shutdown_timeout_seconds() -> u64 {
1689 30
1690}
1691
1692impl Default for ShutdownConfig {
1693 fn default() -> Self {
1694 Self {
1695 timeout_seconds: default_shutdown_timeout_seconds(),
1696 }
1697 }
1698}
1699
1700#[derive(Debug, Clone, Serialize, Deserialize)]
1728pub struct SecurityAnalysisConfig {
1729 #[serde(default = "default_ml_enabled")]
1732 pub ml_enabled: bool,
1733 #[serde(default = "default_ml_model")]
1735 pub ml_model: String,
1736 #[serde(default = "default_ml_threshold")]
1738 pub ml_threshold: f64,
1739 #[serde(default = "default_ml_cache_dir")]
1741 pub ml_cache_dir: String,
1742 #[serde(default = "default_ml_preload")]
1744 pub ml_preload: bool,
1745 #[serde(default = "default_ml_download_timeout_seconds")]
1747 pub ml_download_timeout_seconds: u64,
1748 #[serde(default)]
1750 pub ner_enabled: bool,
1751 #[serde(default = "default_ner_model")]
1753 pub ner_model: String,
1754 #[serde(default)]
1760 pub fusion_enabled: bool,
1761 #[serde(default)]
1766 pub fusion_model_path: Option<String>,
1767 #[serde(default = "default_jailbreak_enabled")]
1773 pub jailbreak_enabled: bool,
1774 #[serde(default = "default_jailbreak_threshold")]
1776 pub jailbreak_threshold: f32,
1777 #[serde(default)]
1783 pub injecguard_enabled: bool,
1784 #[serde(default = "default_injecguard_model")]
1786 pub injecguard_model: String,
1787 #[serde(default = "default_injecguard_threshold")]
1789 pub injecguard_threshold: f64,
1790 #[serde(default)]
1795 pub piguard_enabled: bool,
1796 #[serde(default = "default_piguard_model")]
1798 pub piguard_model: String,
1799 #[serde(default = "default_piguard_threshold")]
1801 pub piguard_threshold: f64,
1802 #[serde(default)]
1804 pub operating_point: OperatingPoint,
1805 #[serde(default)]
1807 pub over_defence: bool,
1808}
1809
1810fn default_ml_enabled() -> bool {
1811 true
1812}
1813
1814fn default_ml_model() -> String {
1815 "protectai/deberta-v3-base-prompt-injection-v2".to_string()
1816}
1817
1818fn default_ml_threshold() -> f64 {
1819 0.8
1820}
1821
1822fn default_ml_cache_dir() -> String {
1823 "~/.cache/llmtrace/models".to_string()
1824}
1825
1826fn default_ml_preload() -> bool {
1827 true
1828}
1829
1830fn default_ml_download_timeout_seconds() -> u64 {
1831 300
1832}
1833
1834fn default_ner_model() -> String {
1835 "dslim/bert-base-NER".to_string()
1836}
1837
1838fn default_jailbreak_enabled() -> bool {
1839 true
1840}
1841
1842fn default_jailbreak_threshold() -> f32 {
1843 0.7
1844}
1845
1846fn default_injecguard_model() -> String {
1847 "leolee99/InjecGuard".to_string()
1848}
1849
1850fn default_injecguard_threshold() -> f64 {
1851 0.85
1852}
1853
1854fn default_piguard_model() -> String {
1855 "leolee99/PIGuard".to_string()
1856}
1857
1858fn default_piguard_threshold() -> f64 {
1859 0.85
1860}
1861
1862impl Default for SecurityAnalysisConfig {
1863 fn default() -> Self {
1864 Self {
1865 ml_enabled: default_ml_enabled(),
1866 ml_model: default_ml_model(),
1867 ml_threshold: default_ml_threshold(),
1868 ml_cache_dir: default_ml_cache_dir(),
1869 ml_preload: default_ml_preload(),
1870 ml_download_timeout_seconds: default_ml_download_timeout_seconds(),
1871 ner_enabled: false,
1872 ner_model: default_ner_model(),
1873 fusion_enabled: false,
1874 fusion_model_path: None,
1875 jailbreak_enabled: default_jailbreak_enabled(),
1876 jailbreak_threshold: default_jailbreak_threshold(),
1877 injecguard_enabled: false,
1878 injecguard_model: default_injecguard_model(),
1879 injecguard_threshold: default_injecguard_threshold(),
1880 piguard_enabled: false,
1881 piguard_model: default_piguard_model(),
1882 piguard_threshold: default_piguard_threshold(),
1883 operating_point: OperatingPoint::default(),
1884 over_defence: false,
1885 }
1886 }
1887}
1888
1889#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1895#[serde(rename_all = "snake_case")]
1896pub enum EnforcementMode {
1897 #[default]
1899 Log,
1900 Block,
1902 Flag,
1904}
1905
1906#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1908#[serde(rename_all = "snake_case")]
1909pub enum AnalysisDepth {
1910 #[default]
1912 Fast,
1913 Full,
1915}
1916
1917#[derive(Debug, Clone, Serialize, Deserialize)]
1919pub struct CategoryEnforcement {
1920 pub finding_type: String,
1922 pub action: EnforcementMode,
1924}
1925
1926fn default_enforcement_min_severity() -> SecuritySeverity {
1927 SecuritySeverity::High
1928}
1929
1930fn default_enforcement_min_confidence() -> f64 {
1931 0.8
1932}
1933
1934fn default_enforcement_timeout_ms() -> u64 {
1935 2000
1936}
1937
1938#[derive(Debug, Clone, Serialize, Deserialize)]
1943pub struct EnforcementConfig {
1944 #[serde(default)]
1946 pub mode: EnforcementMode,
1947 #[serde(default)]
1949 pub analysis_depth: AnalysisDepth,
1950 #[serde(default = "default_enforcement_min_severity")]
1952 pub min_severity: SecuritySeverity,
1953 #[serde(default = "default_enforcement_min_confidence")]
1955 pub min_confidence: f64,
1956 #[serde(default = "default_enforcement_timeout_ms")]
1958 pub timeout_ms: u64,
1959 #[serde(default)]
1961 pub categories: Vec<CategoryEnforcement>,
1962}
1963
1964impl Default for EnforcementConfig {
1965 fn default() -> Self {
1966 Self {
1967 mode: EnforcementMode::Log,
1968 analysis_depth: AnalysisDepth::Fast,
1969 min_severity: default_enforcement_min_severity(),
1970 min_confidence: default_enforcement_min_confidence(),
1971 timeout_ms: default_enforcement_timeout_ms(),
1972 categories: Vec::new(),
1973 }
1974 }
1975}
1976
1977#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1989pub struct OtelIngestConfig {
1990 #[serde(default)]
1992 pub enabled: bool,
1993}
1994
1995#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2009pub struct AuthConfig {
2010 #[serde(default)]
2012 pub enabled: bool,
2013 #[serde(default)]
2016 pub admin_key: Option<String>,
2017}
2018
2019#[derive(Debug, Clone, Serialize, Deserialize)]
2033pub struct GrpcConfig {
2034 #[serde(default)]
2036 pub enabled: bool,
2037 #[serde(default = "default_grpc_listen_addr")]
2039 pub listen_addr: String,
2040}
2041
2042fn default_grpc_listen_addr() -> String {
2043 "0.0.0.0:50051".to_string()
2044}
2045
2046impl Default for GrpcConfig {
2047 fn default() -> Self {
2048 Self {
2049 enabled: false,
2050 listen_addr: default_grpc_listen_addr(),
2051 }
2052 }
2053}
2054
2055#[derive(Debug, Clone, Serialize, Deserialize)]
2062pub struct AlertConfig {
2063 #[serde(default)]
2065 pub enabled: bool,
2066 #[serde(default)]
2069 pub webhook_url: String,
2070 #[serde(default = "default_alert_min_severity")]
2073 pub min_severity: String,
2074 #[serde(default = "default_alert_min_security_score")]
2076 pub min_security_score: u8,
2077 #[serde(default = "default_alert_cooldown_seconds")]
2079 pub cooldown_seconds: u64,
2080 #[serde(default)]
2083 pub channels: Vec<AlertChannelConfig>,
2084 #[serde(default)]
2086 pub escalation: Option<AlertEscalationConfig>,
2087}
2088
2089#[derive(Debug, Clone, Serialize, Deserialize)]
2091pub struct AlertChannelConfig {
2092 #[serde(rename = "type")]
2094 pub channel_type: String,
2095 #[serde(default)]
2097 pub url: Option<String>,
2098 #[serde(default)]
2100 pub webhook_url: Option<String>,
2101 #[serde(default)]
2103 pub routing_key: Option<String>,
2104 #[serde(default = "default_alert_min_severity")]
2106 pub min_severity: String,
2107 #[serde(default = "default_alert_min_security_score")]
2109 pub min_security_score: u8,
2110}
2111
2112impl AlertChannelConfig {
2113 pub fn effective_url(&self) -> Option<&str> {
2115 self.url
2116 .as_deref()
2117 .or(self.webhook_url.as_deref())
2118 .filter(|s| !s.is_empty())
2119 }
2120}
2121
2122#[derive(Debug, Clone, Serialize, Deserialize)]
2127pub struct AlertEscalationConfig {
2128 #[serde(default)]
2130 pub enabled: bool,
2131 #[serde(default = "default_escalation_seconds")]
2133 pub escalate_after_seconds: u64,
2134}
2135
2136fn default_escalation_seconds() -> u64 {
2137 600
2138}
2139
2140fn default_alert_min_severity() -> String {
2141 "High".to_string()
2142}
2143
2144fn default_alert_min_security_score() -> u8 {
2145 70
2146}
2147
2148fn default_alert_cooldown_seconds() -> u64 {
2149 300
2150}
2151
2152impl Default for AlertConfig {
2153 fn default() -> Self {
2154 Self {
2155 enabled: false,
2156 webhook_url: String::new(),
2157 min_severity: default_alert_min_severity(),
2158 min_security_score: default_alert_min_security_score(),
2159 cooldown_seconds: default_alert_cooldown_seconds(),
2160 channels: Vec::new(),
2161 escalation: None,
2162 }
2163 }
2164}
2165
2166#[derive(Debug, Clone, Serialize, Deserialize)]
2168pub struct LoggingConfig {
2169 #[serde(default = "default_log_level")]
2171 pub level: String,
2172 #[serde(default = "default_log_format")]
2174 pub format: String,
2175}
2176
2177fn default_log_level() -> String {
2178 "info".to_string()
2179}
2180
2181fn default_log_format() -> String {
2182 "text".to_string()
2183}
2184
2185impl Default for LoggingConfig {
2186 fn default() -> Self {
2187 Self {
2188 level: default_log_level(),
2189 format: default_log_format(),
2190 }
2191 }
2192}
2193
2194#[derive(thiserror::Error, Debug)]
2200pub enum LLMTraceError {
2201 #[error("Storage error: {0}")]
2203 Storage(String),
2204
2205 #[error("Security analysis error: {0}")]
2207 Security(String),
2208
2209 #[error("Serialization error: {0}")]
2211 Serialization(#[from] serde_json::Error),
2212
2213 #[error("Invalid tenant: {tenant_id}")]
2215 InvalidTenant {
2216 tenant_id: TenantId,
2218 },
2219
2220 #[error("Configuration error: {0}")]
2222 Config(String),
2223}
2224
2225pub type Result<T> = std::result::Result<T, LLMTraceError>;
2227
2228#[derive(Debug, Clone, Serialize, Deserialize)]
2234pub struct TraceQuery {
2235 pub tenant_id: TenantId,
2237 pub start_time: Option<DateTime<Utc>>,
2239 pub end_time: Option<DateTime<Utc>>,
2241 pub provider: Option<LLMProvider>,
2243 pub model_name: Option<String>,
2245 pub operation_name: Option<String>,
2247 pub min_security_score: Option<u8>,
2249 pub max_security_score: Option<u8>,
2251 pub trace_id: Option<Uuid>,
2253 pub tags: HashMap<String, String>,
2255 pub limit: Option<u32>,
2257 pub offset: Option<u32>,
2259}
2260
2261impl TraceQuery {
2262 pub fn new(tenant_id: TenantId) -> Self {
2264 Self {
2265 tenant_id,
2266 start_time: None,
2267 end_time: None,
2268 provider: None,
2269 model_name: None,
2270 operation_name: None,
2271 min_security_score: None,
2272 max_security_score: None,
2273 trace_id: None,
2274 tags: HashMap::new(),
2275 limit: None,
2276 offset: None,
2277 }
2278 }
2279
2280 pub fn with_time_range(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
2282 self.start_time = Some(start);
2283 self.end_time = Some(end);
2284 self
2285 }
2286
2287 pub fn with_provider(mut self, provider: LLMProvider) -> Self {
2289 self.provider = Some(provider);
2290 self
2291 }
2292
2293 pub fn with_security_score_range(mut self, min: u8, max: u8) -> Self {
2295 self.min_security_score = Some(min);
2296 self.max_security_score = Some(max);
2297 self
2298 }
2299
2300 pub fn with_limit(mut self, limit: u32) -> Self {
2302 self.limit = Some(limit);
2303 self
2304 }
2305}
2306
2307#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
2313pub struct StorageStats {
2314 pub total_traces: u64,
2316 pub total_spans: u64,
2318 pub storage_size_bytes: u64,
2320 #[schema(value_type = String, format = "date-time")]
2322 pub oldest_trace: Option<DateTime<Utc>>,
2323 #[schema(value_type = String, format = "date-time")]
2325 pub newest_trace: Option<DateTime<Utc>>,
2326}
2327
2328#[async_trait::async_trait]
2336pub trait TraceRepository: Send + Sync {
2337 async fn store_trace(&self, trace: &TraceEvent) -> Result<()>;
2339
2340 async fn store_span(&self, span: &TraceSpan) -> Result<()>;
2342
2343 async fn query_traces(&self, query: &TraceQuery) -> Result<Vec<TraceEvent>>;
2345
2346 async fn query_spans(&self, query: &TraceQuery) -> Result<Vec<TraceSpan>>;
2348
2349 async fn get_trace(&self, tenant_id: TenantId, trace_id: Uuid) -> Result<Option<TraceEvent>>;
2351
2352 async fn get_span(&self, tenant_id: TenantId, span_id: Uuid) -> Result<Option<TraceSpan>>;
2354
2355 async fn delete_traces_before(&self, tenant_id: TenantId, before: DateTime<Utc>)
2357 -> Result<u64>;
2358
2359 async fn get_stats(&self, tenant_id: TenantId) -> Result<StorageStats>;
2361
2362 async fn health_check(&self) -> Result<()>;
2364}
2365
2366#[async_trait::async_trait]
2370pub trait MetadataRepository: Send + Sync {
2371 async fn create_tenant(&self, tenant: &Tenant) -> Result<()>;
2373
2374 async fn get_tenant(&self, id: TenantId) -> Result<Option<Tenant>>;
2376
2377 async fn get_tenant_by_token(&self, token: &str) -> Result<Option<Tenant>>;
2379
2380 async fn update_tenant(&self, tenant: &Tenant) -> Result<()>;
2382
2383 async fn list_tenants(&self) -> Result<Vec<Tenant>>;
2385
2386 async fn delete_tenant(&self, id: TenantId) -> Result<()>;
2388
2389 async fn get_tenant_config(&self, tenant_id: TenantId) -> Result<Option<TenantConfig>>;
2391
2392 async fn upsert_tenant_config(&self, config: &TenantConfig) -> Result<()>;
2394
2395 async fn record_audit_event(&self, event: &AuditEvent) -> Result<()>;
2397
2398 async fn query_audit_events(&self, query: &AuditQuery) -> Result<Vec<AuditEvent>>;
2400
2401 async fn create_api_key(&self, key: &ApiKeyRecord) -> Result<()>;
2405
2406 async fn get_api_key_by_hash(&self, key_hash: &str) -> Result<Option<ApiKeyRecord>>;
2408
2409 async fn list_api_keys(&self, tenant_id: TenantId) -> Result<Vec<ApiKeyRecord>>;
2411
2412 async fn revoke_api_key(&self, key_id: Uuid) -> Result<bool>;
2414
2415 async fn store_report(&self, report: &ComplianceReportRecord) -> Result<()>;
2419
2420 async fn get_report(&self, report_id: Uuid) -> Result<Option<ComplianceReportRecord>>;
2422
2423 async fn list_reports(&self, query: &ReportQuery) -> Result<Vec<ComplianceReportRecord>>;
2425
2426 async fn health_check(&self) -> Result<()>;
2428}
2429
2430#[async_trait::async_trait]
2434pub trait CacheLayer: Send + Sync {
2435 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
2437
2438 async fn set(&self, key: &str, value: &[u8], ttl: Duration) -> Result<()>;
2440
2441 async fn invalidate(&self, key: &str) -> Result<()>;
2443
2444 async fn health_check(&self) -> Result<()>;
2446}
2447
2448pub struct Storage {
2457 pub traces: Arc<dyn TraceRepository>,
2459 pub metadata: Arc<dyn MetadataRepository>,
2461 pub cache: Arc<dyn CacheLayer>,
2463}
2464
2465#[derive(Debug, Clone)]
2471pub struct AnalysisContext {
2472 pub tenant_id: TenantId,
2474 pub trace_id: Uuid,
2476 pub span_id: Uuid,
2478 pub provider: LLMProvider,
2480 pub model_name: String,
2482 pub parameters: HashMap<String, String>,
2484}
2485
2486#[async_trait::async_trait]
2488pub trait SecurityAnalyzer: Send + Sync {
2489 async fn analyze_request(
2491 &self,
2492 prompt: &str,
2493 context: &AnalysisContext,
2494 ) -> Result<Vec<SecurityFinding>>;
2495
2496 async fn analyze_response(
2498 &self,
2499 response: &str,
2500 context: &AnalysisContext,
2501 ) -> Result<Vec<SecurityFinding>>;
2502
2503 async fn analyze_interaction(
2505 &self,
2506 prompt: &str,
2507 response: &str,
2508 context: &AnalysisContext,
2509 ) -> Result<Vec<SecurityFinding>> {
2510 let mut findings = Vec::new();
2511 findings.extend(self.analyze_request(prompt, context).await?);
2512 findings.extend(self.analyze_response(response, context).await?);
2513 Ok(findings)
2514 }
2515
2516 fn name(&self) -> &'static str;
2518
2519 fn version(&self) -> &'static str;
2521
2522 fn supported_finding_types(&self) -> Vec<String>;
2524
2525 async fn health_check(&self) -> Result<()>;
2527}
2528
2529#[cfg(test)]
2534mod tests {
2535 use super::*;
2536
2537 #[test]
2538 fn test_tenant_id_creation() {
2539 let tenant1 = TenantId::new();
2540 let tenant2 = TenantId::new();
2541 assert_ne!(tenant1, tenant2);
2542 }
2543
2544 #[test]
2545 fn test_tenant_id_display() {
2546 let tenant_id = TenantId::new();
2547 let display_str = format!("{}", tenant_id);
2548 let uuid_str = format!("{}", tenant_id.0);
2549 assert_eq!(display_str, uuid_str);
2550 }
2551
2552 #[test]
2553 fn test_security_finding_creation() {
2554 let finding = SecurityFinding::new(
2555 SecuritySeverity::High,
2556 "prompt_injection".to_string(),
2557 "Detected system prompt override attempt".to_string(),
2558 0.95,
2559 );
2560
2561 assert_eq!(finding.severity, SecuritySeverity::High);
2562 assert_eq!(finding.finding_type, "prompt_injection");
2563 assert!(finding.requires_alert);
2564 assert_eq!(finding.confidence_score, 0.95);
2565 assert!(finding.metadata.is_empty());
2566 }
2567
2568 #[test]
2569 fn test_security_finding_with_metadata() {
2570 let finding = SecurityFinding::new(
2571 SecuritySeverity::Medium,
2572 "pii_detection".to_string(),
2573 "Found potential PII".to_string(),
2574 0.8,
2575 )
2576 .with_location("request.messages[0].content".to_string())
2577 .with_metadata("pii_type".to_string(), "email".to_string())
2578 .with_alert_required(true);
2579
2580 assert_eq!(
2581 finding.location,
2582 Some("request.messages[0].content".to_string())
2583 );
2584 assert_eq!(finding.metadata.get("pii_type"), Some(&"email".to_string()));
2585 assert!(finding.requires_alert);
2586 }
2587
2588 #[test]
2589 fn test_trace_span_creation() {
2590 let tenant_id = TenantId::new();
2591 let trace_id = Uuid::new_v4();
2592
2593 let span = TraceSpan::new(
2594 trace_id,
2595 tenant_id,
2596 "chat_completion".to_string(),
2597 LLMProvider::OpenAI,
2598 "gpt-4".to_string(),
2599 "Hello, how are you?".to_string(),
2600 );
2601
2602 assert_eq!(span.trace_id, trace_id);
2603 assert_eq!(span.tenant_id, tenant_id);
2604 assert_eq!(span.operation_name, "chat_completion");
2605 assert_eq!(span.provider, LLMProvider::OpenAI);
2606 assert!(span.response.is_none());
2607 assert!(!span.is_complete());
2608 assert!(!span.is_failed());
2609 }
2610
2611 #[test]
2612 fn test_trace_span_completion() {
2613 let span = TraceSpan::new(
2614 Uuid::new_v4(),
2615 TenantId::new(),
2616 "test_op".to_string(),
2617 LLMProvider::OpenAI,
2618 "gpt-3.5-turbo".to_string(),
2619 "test prompt".to_string(),
2620 );
2621
2622 let completed_span = span.finish_with_response("test response".to_string());
2623
2624 assert_eq!(completed_span.response, Some("test response".to_string()));
2625 assert!(completed_span.is_complete());
2626 assert!(!completed_span.is_failed());
2627 assert!(completed_span.end_time.is_some());
2628 assert!(completed_span.duration_ms.is_some());
2629 }
2630
2631 #[test]
2632 fn test_trace_span_error() {
2633 let span = TraceSpan::new(
2634 Uuid::new_v4(),
2635 TenantId::new(),
2636 "test_op".to_string(),
2637 LLMProvider::OpenAI,
2638 "gpt-4".to_string(),
2639 "test prompt".to_string(),
2640 );
2641
2642 let failed_span = span.finish_with_error("API timeout".to_string(), Some(504));
2643
2644 assert!(failed_span.is_failed());
2645 assert!(failed_span.is_complete());
2646 assert_eq!(failed_span.error_message, Some("API timeout".to_string()));
2647 assert_eq!(failed_span.status_code, Some(504));
2648 }
2649
2650 #[test]
2651 fn test_trace_span_security_findings() {
2652 let mut span = TraceSpan::new(
2653 Uuid::new_v4(),
2654 TenantId::new(),
2655 "test_op".to_string(),
2656 LLMProvider::OpenAI,
2657 "gpt-4".to_string(),
2658 "test prompt".to_string(),
2659 );
2660
2661 let finding = SecurityFinding::new(
2662 SecuritySeverity::Critical,
2663 "prompt_injection".to_string(),
2664 "Malicious prompt detected".to_string(),
2665 0.98,
2666 );
2667
2668 span.add_security_finding(finding);
2669
2670 assert_eq!(span.security_findings.len(), 1);
2671 assert_eq!(span.security_score, Some(95));
2672 }
2673
2674 #[test]
2675 fn test_span_event_creation() {
2676 let event = SpanEvent::new(
2677 "token_received".to_string(),
2678 "First token received from LLM".to_string(),
2679 )
2680 .with_data("token".to_string(), "Hello".to_string())
2681 .with_data("position".to_string(), "0".to_string());
2682
2683 assert_eq!(event.event_type, "token_received");
2684 assert_eq!(event.data.get("token"), Some(&"Hello".to_string()));
2685 assert_eq!(event.data.get("position"), Some(&"0".to_string()));
2686 }
2687
2688 #[test]
2689 fn test_trace_query_creation() {
2690 let tenant_id = TenantId::new();
2691 let provider = LLMProvider::Anthropic;
2692 let start_time = Utc::now() - chrono::Duration::hours(24);
2693 let end_time = Utc::now();
2694
2695 let query = TraceQuery::new(tenant_id)
2696 .with_time_range(start_time, end_time)
2697 .with_provider(provider.clone())
2698 .with_security_score_range(80, 100)
2699 .with_limit(50);
2700
2701 assert_eq!(query.tenant_id, tenant_id);
2702 assert_eq!(query.start_time, Some(start_time));
2703 assert_eq!(query.end_time, Some(end_time));
2704 assert_eq!(query.provider, Some(provider));
2705 assert_eq!(query.min_security_score, Some(80));
2706 assert_eq!(query.max_security_score, Some(100));
2707 assert_eq!(query.limit, Some(50));
2708 }
2709
2710 #[test]
2711 fn test_proxy_config_default() {
2712 let config = ProxyConfig::default();
2713
2714 assert_eq!(config.listen_addr, "0.0.0.0:8080");
2715 assert!(config.enable_security_analysis);
2716 assert!(config.enable_trace_storage);
2717 assert!(config.enable_streaming);
2718 assert_eq!(config.timeout_ms, 30000);
2719 assert_eq!(config.max_connections, 1000);
2720 assert!(!config.enable_tls);
2721 assert_eq!(config.logging.level, "info");
2722 assert_eq!(config.logging.format, "text");
2723 assert!(config.cost_estimation.enabled);
2724 assert!(config.cost_estimation.custom_models.is_empty());
2725 }
2726
2727 #[test]
2728 fn test_storage_config_default() {
2729 let config = StorageConfig::default();
2730 assert_eq!(config.profile, "lite");
2731 assert_eq!(config.database_path, "llmtrace.db");
2732 assert!(config.clickhouse_url.is_none());
2733 assert!(config.clickhouse_database.is_none());
2734 assert!(config.postgres_url.is_none());
2735 assert!(config.redis_url.is_none());
2736 }
2737
2738 #[test]
2739 fn test_logging_config_default() {
2740 let config = LoggingConfig::default();
2741 assert_eq!(config.level, "info");
2742 assert_eq!(config.format, "text");
2743 }
2744
2745 #[test]
2746 fn test_logging_config_serialization() {
2747 let config = LoggingConfig {
2748 level: "debug".to_string(),
2749 format: "json".to_string(),
2750 };
2751 let serialized = serde_json::to_string(&config).unwrap();
2752 let deserialized: LoggingConfig = serde_json::from_str(&serialized).unwrap();
2753 assert_eq!(config.level, deserialized.level);
2754 assert_eq!(config.format, deserialized.format);
2755 }
2756
2757 #[test]
2758 fn test_rate_limit_config_default() {
2759 let config = RateLimitConfig::default();
2760
2761 assert!(config.enabled);
2762 assert_eq!(config.requests_per_second, 100);
2763 assert_eq!(config.burst_size, 200);
2764 assert_eq!(config.window_seconds, 60);
2765 }
2766
2767 #[test]
2768 fn test_circuit_breaker_config_default() {
2769 let config = CircuitBreakerConfig::default();
2770
2771 assert!(config.enabled);
2772 assert_eq!(config.failure_threshold, 10);
2773 assert_eq!(config.recovery_timeout_ms, 30000);
2774 assert_eq!(config.half_open_max_calls, 3);
2775 }
2776
2777 #[test]
2778 fn test_health_check_config_default() {
2779 let config = HealthCheckConfig::default();
2780
2781 assert!(config.enabled);
2782 assert_eq!(config.path, "/health");
2783 assert_eq!(config.interval_seconds, 10);
2784 assert_eq!(config.timeout_ms, 5000);
2785 assert_eq!(config.retries, 3);
2786 }
2787
2788 #[test]
2789 fn test_llm_provider_serialization() {
2790 let providers = vec![
2791 LLMProvider::OpenAI,
2792 LLMProvider::Anthropic,
2793 LLMProvider::VLLm,
2794 LLMProvider::SGLang,
2795 LLMProvider::TGI,
2796 LLMProvider::Ollama,
2797 LLMProvider::AzureOpenAI,
2798 LLMProvider::Bedrock,
2799 LLMProvider::Custom("CustomProvider".to_string()),
2800 ];
2801
2802 for provider in providers {
2803 let serialized = serde_json::to_string(&provider).unwrap();
2804 let deserialized: LLMProvider = serde_json::from_str(&serialized).unwrap();
2805 assert_eq!(provider, deserialized);
2806 }
2807 }
2808
2809 #[test]
2810 fn test_security_severity_ordering() {
2811 assert!(SecuritySeverity::Critical > SecuritySeverity::High);
2812 assert!(SecuritySeverity::High > SecuritySeverity::Medium);
2813 assert!(SecuritySeverity::Medium > SecuritySeverity::Low);
2814 assert!(SecuritySeverity::Low > SecuritySeverity::Info);
2815 }
2816
2817 #[test]
2818 fn test_trace_serialization() {
2819 let tenant_id = TenantId::new();
2820 let trace_id = Uuid::new_v4();
2821
2822 let span = TraceSpan::new(
2823 trace_id,
2824 tenant_id,
2825 "chat_completion".to_string(),
2826 LLMProvider::OpenAI,
2827 "gpt-4".to_string(),
2828 "Hello, world!".to_string(),
2829 );
2830
2831 let trace = TraceEvent {
2832 trace_id,
2833 tenant_id,
2834 spans: vec![span],
2835 created_at: Utc::now(),
2836 };
2837
2838 let serialized = serde_json::to_string(&trace).unwrap();
2839 let deserialized: TraceEvent = serde_json::from_str(&serialized).unwrap();
2840
2841 assert_eq!(trace.trace_id, deserialized.trace_id);
2842 assert_eq!(trace.tenant_id, deserialized.tenant_id);
2843 assert_eq!(trace.spans.len(), deserialized.spans.len());
2844 }
2845
2846 #[test]
2847 fn test_security_finding_serialization() {
2848 let finding = SecurityFinding::new(
2849 SecuritySeverity::High,
2850 "prompt_injection".to_string(),
2851 "Detected system prompt override attempt".to_string(),
2852 0.95,
2853 )
2854 .with_location("request.messages[0]".to_string())
2855 .with_metadata("pattern".to_string(), "ignore_previous".to_string());
2856
2857 let serialized = serde_json::to_string(&finding).unwrap();
2858 let deserialized: SecurityFinding = serde_json::from_str(&serialized).unwrap();
2859
2860 assert_eq!(finding.severity, deserialized.severity);
2861 assert_eq!(finding.finding_type, deserialized.finding_type);
2862 assert_eq!(finding.confidence_score, deserialized.confidence_score);
2863 assert_eq!(finding.location, deserialized.location);
2864 assert_eq!(finding.metadata, deserialized.metadata);
2865 }
2866
2867 #[test]
2868 fn test_trace_span_serialization() {
2869 let mut span = TraceSpan::new(
2870 Uuid::new_v4(),
2871 TenantId::new(),
2872 "embedding".to_string(),
2873 LLMProvider::OpenAI,
2874 "text-embedding-ada-002".to_string(),
2875 "Test embedding text".to_string(),
2876 );
2877
2878 let event = SpanEvent::new(
2879 "embedding_complete".to_string(),
2880 "Embedding calculation completed".to_string(),
2881 );
2882 span.add_event(event);
2883
2884 let finding = SecurityFinding::new(
2885 SecuritySeverity::Low,
2886 "pii_detection".to_string(),
2887 "Potential email address detected".to_string(),
2888 0.7,
2889 );
2890 span.add_security_finding(finding);
2891
2892 let serialized = serde_json::to_string(&span).unwrap();
2893 let deserialized: TraceSpan = serde_json::from_str(&serialized).unwrap();
2894
2895 assert_eq!(span.trace_id, deserialized.trace_id);
2896 assert_eq!(span.operation_name, deserialized.operation_name);
2897 assert_eq!(span.provider, deserialized.provider);
2898 assert_eq!(span.events.len(), deserialized.events.len());
2899 assert_eq!(
2900 span.security_findings.len(),
2901 deserialized.security_findings.len()
2902 );
2903 }
2904
2905 #[test]
2906 fn test_proxy_config_serialization() {
2907 let mut custom_models = HashMap::new();
2908 custom_models.insert(
2909 "my-custom-model".to_string(),
2910 ModelPricingConfig {
2911 input_per_million: 1.0,
2912 output_per_million: 2.0,
2913 },
2914 );
2915
2916 let config = ProxyConfig {
2917 listen_addr: "127.0.0.1:9090".to_string(),
2918 upstream_url: "https://api.anthropic.com".to_string(),
2919 storage: StorageConfig {
2920 profile: "lite".to_string(),
2921 database_path: "test.db".to_string(),
2922 clickhouse_url: None,
2923 clickhouse_database: None,
2924 postgres_url: None,
2925 redis_url: None,
2926 auto_migrate: true,
2927 },
2928 timeout_ms: 60000,
2929 connection_timeout_ms: 10000,
2930 max_connections: 2000,
2931 enable_tls: true,
2932 tls_cert_file: Some("/etc/ssl/cert.pem".to_string()),
2933 tls_key_file: Some("/etc/ssl/key.pem".to_string()),
2934 enable_security_analysis: true,
2935 enable_trace_storage: true,
2936 enable_streaming: true,
2937 max_request_size_bytes: 100 * 1024 * 1024,
2938 security_analysis_timeout_ms: 3000,
2939 trace_storage_timeout_ms: 5000,
2940 rate_limiting: RateLimitConfig::default(),
2941 circuit_breaker: CircuitBreakerConfig::default(),
2942 health_check: HealthCheckConfig::default(),
2943 logging: LoggingConfig {
2944 level: "debug".to_string(),
2945 format: "json".to_string(),
2946 },
2947 cost_estimation: CostEstimationConfig {
2948 enabled: true,
2949 pricing_file: None,
2950 custom_models,
2951 },
2952 alerts: AlertConfig::default(),
2953 cost_caps: CostCapConfig::default(),
2954 security_analysis: SecurityAnalysisConfig {
2955 ml_enabled: true,
2956 ml_model: "custom/model".to_string(),
2957 ml_threshold: 0.9,
2958 ml_cache_dir: "/tmp/models".to_string(),
2959 ml_preload: true,
2960 ml_download_timeout_seconds: 300,
2961 ner_enabled: false,
2962 ner_model: default_ner_model(),
2963 fusion_enabled: false,
2964 fusion_model_path: None,
2965 jailbreak_enabled: true,
2966 jailbreak_threshold: 0.7,
2967 injecguard_enabled: false,
2968 injecguard_model: default_injecguard_model(),
2969 injecguard_threshold: default_injecguard_threshold(),
2970 piguard_enabled: false,
2971 piguard_model: default_piguard_model(),
2972 piguard_threshold: default_piguard_threshold(),
2973 operating_point: OperatingPoint::default(),
2974 over_defence: false,
2975 },
2976 otel_ingest: OtelIngestConfig::default(),
2977 auth: AuthConfig::default(),
2978 grpc: GrpcConfig::default(),
2979 anomaly_detection: AnomalyDetectionConfig::default(),
2980 streaming_analysis: StreamingAnalysisConfig::default(),
2981 pii: PiiConfig {
2982 action: PiiAction::AlertAndRedact,
2983 },
2984 output_safety: OutputSafetyConfig::default(),
2985 shutdown: ShutdownConfig::default(),
2986 enforcement: EnforcementConfig::default(),
2987 };
2988
2989 let serialized = serde_json::to_string(&config).unwrap();
2990 let deserialized: ProxyConfig = serde_json::from_str(&serialized).unwrap();
2991
2992 assert_eq!(config.listen_addr, deserialized.listen_addr);
2993 assert_eq!(config.upstream_url, deserialized.upstream_url);
2994 assert_eq!(config.enable_tls, deserialized.enable_tls);
2995 assert_eq!(config.tls_cert_file, deserialized.tls_cert_file);
2996 assert_eq!(config.storage.profile, deserialized.storage.profile);
2997 assert_eq!(
2998 config.storage.database_path,
2999 deserialized.storage.database_path
3000 );
3001 assert_eq!(config.logging.level, deserialized.logging.level);
3002 assert_eq!(config.logging.format, deserialized.logging.format);
3003 assert_eq!(deserialized.pii.action, PiiAction::AlertAndRedact);
3004 assert!(deserialized.cost_estimation.enabled);
3005 let custom = deserialized
3006 .cost_estimation
3007 .custom_models
3008 .get("my-custom-model")
3009 .unwrap();
3010 assert!((custom.input_per_million - 1.0).abs() < f64::EPSILON);
3011 assert!((custom.output_per_million - 2.0).abs() < f64::EPSILON);
3012 assert!(deserialized.security_analysis.ml_enabled);
3013 assert_eq!(deserialized.security_analysis.ml_model, "custom/model");
3014 assert!(!deserialized.grpc.enabled);
3015 assert_eq!(deserialized.grpc.listen_addr, "0.0.0.0:50051");
3016 }
3017
3018 #[test]
3019 fn test_trace_query_serialization() {
3020 let query = TraceQuery::new(TenantId::new())
3021 .with_time_range(Utc::now() - chrono::Duration::hours(1), Utc::now())
3022 .with_provider(LLMProvider::Anthropic)
3023 .with_limit(100);
3024
3025 let serialized = serde_json::to_string(&query).unwrap();
3026 let deserialized: TraceQuery = serde_json::from_str(&serialized).unwrap();
3027
3028 assert_eq!(query.tenant_id, deserialized.tenant_id);
3029 assert_eq!(query.provider, deserialized.provider);
3030 assert_eq!(query.limit, deserialized.limit);
3031 }
3032
3033 #[test]
3034 fn test_storage_stats_serialization() {
3035 let stats = StorageStats {
3036 total_traces: 12345,
3037 total_spans: 67890,
3038 storage_size_bytes: 1024 * 1024 * 1024, oldest_trace: Some(Utc::now() - chrono::Duration::days(30)),
3040 newest_trace: Some(Utc::now()),
3041 };
3042
3043 let serialized = serde_json::to_string(&stats).unwrap();
3044 let deserialized: StorageStats = serde_json::from_str(&serialized).unwrap();
3045
3046 assert_eq!(stats.total_traces, deserialized.total_traces);
3047 assert_eq!(stats.total_spans, deserialized.total_spans);
3048 assert_eq!(stats.storage_size_bytes, deserialized.storage_size_bytes);
3049 }
3050
3051 #[test]
3052 fn test_tenant_creation() {
3053 let tenant = Tenant {
3054 id: TenantId::new(),
3055 name: "Acme Corp".to_string(),
3056 api_token: "test-token".to_string(),
3057 plan: "pro".to_string(),
3058 created_at: Utc::now(),
3059 config: serde_json::json!({"max_traces_per_day": 10000}),
3060 };
3061
3062 let serialized = serde_json::to_string(&tenant).unwrap();
3063 let deserialized: Tenant = serde_json::from_str(&serialized).unwrap();
3064 assert_eq!(tenant.id, deserialized.id);
3065 assert_eq!(tenant.name, deserialized.name);
3066 assert_eq!(tenant.plan, deserialized.plan);
3067 }
3068
3069 #[test]
3070 fn test_tenant_config_creation() {
3071 let mut thresholds = HashMap::new();
3072 thresholds.insert("alert_min_score".to_string(), 80.0);
3073
3074 let mut flags = HashMap::new();
3075 flags.insert("enable_pii_detection".to_string(), true);
3076
3077 let config = TenantConfig {
3078 tenant_id: TenantId::new(),
3079 security_thresholds: thresholds,
3080 feature_flags: flags,
3081 monitoring_scope: MonitoringScope::Hybrid,
3082 rate_limit_rpm: None,
3083 monthly_budget: None,
3084 };
3085
3086 let serialized = serde_json::to_string(&config).unwrap();
3087 let deserialized: TenantConfig = serde_json::from_str(&serialized).unwrap();
3088 assert_eq!(config.tenant_id, deserialized.tenant_id);
3089 assert_eq!(
3090 config.security_thresholds.get("alert_min_score"),
3091 deserialized.security_thresholds.get("alert_min_score")
3092 );
3093 }
3094
3095 #[test]
3096 fn test_audit_event_creation() {
3097 let event = AuditEvent {
3098 id: Uuid::new_v4(),
3099 tenant_id: TenantId::new(),
3100 event_type: "config_changed".to_string(),
3101 actor: "admin@example.com".to_string(),
3102 resource: "tenant_config".to_string(),
3103 data: serde_json::json!({"field": "enable_pii_detection", "old": false, "new": true}),
3104 timestamp: Utc::now(),
3105 };
3106
3107 let serialized = serde_json::to_string(&event).unwrap();
3108 let deserialized: AuditEvent = serde_json::from_str(&serialized).unwrap();
3109 assert_eq!(event.id, deserialized.id);
3110 assert_eq!(event.event_type, deserialized.event_type);
3111 assert_eq!(event.actor, deserialized.actor);
3112 }
3113
3114 #[test]
3115 fn test_audit_query_builder() {
3116 let tenant = TenantId::new();
3117 let start = Utc::now() - chrono::Duration::hours(1);
3118 let end = Utc::now();
3119
3120 let query = AuditQuery::new(tenant)
3121 .with_event_type("config_changed".to_string())
3122 .with_time_range(start, end)
3123 .with_limit(50);
3124
3125 assert_eq!(query.tenant_id, tenant);
3126 assert_eq!(query.event_type, Some("config_changed".to_string()));
3127 assert_eq!(query.start_time, Some(start));
3128 assert_eq!(query.end_time, Some(end));
3129 assert_eq!(query.limit, Some(50));
3130 }
3131
3132 #[test]
3133 fn test_alert_config_default() {
3134 let config = AlertConfig::default();
3135 assert!(!config.enabled);
3136 assert!(config.webhook_url.is_empty());
3137 assert_eq!(config.min_severity, "High");
3138 assert_eq!(config.min_security_score, 70);
3139 assert_eq!(config.cooldown_seconds, 300);
3140 assert!(config.channels.is_empty());
3141 assert!(config.escalation.is_none());
3142 }
3143
3144 #[test]
3145 fn test_alert_config_serialization() {
3146 let config = AlertConfig {
3147 enabled: true,
3148 webhook_url: "https://hooks.slack.com/services/T00/B00/xxx".to_string(),
3149 min_severity: "Critical".to_string(),
3150 min_security_score: 90,
3151 cooldown_seconds: 600,
3152 channels: Vec::new(),
3153 escalation: None,
3154 };
3155 let serialized = serde_json::to_string(&config).unwrap();
3156 let deserialized: AlertConfig = serde_json::from_str(&serialized).unwrap();
3157 assert_eq!(config.enabled, deserialized.enabled);
3158 assert_eq!(config.webhook_url, deserialized.webhook_url);
3159 assert_eq!(config.min_severity, deserialized.min_severity);
3160 assert_eq!(config.min_security_score, deserialized.min_security_score);
3161 assert_eq!(config.cooldown_seconds, deserialized.cooldown_seconds);
3162 }
3163
3164 #[test]
3165 fn test_alert_config_deserializes_with_defaults() {
3166 let json = r#"{"enabled": true, "webhook_url": "http://example.com"}"#;
3167 let config: AlertConfig = serde_json::from_str(json).unwrap();
3168 assert!(config.enabled);
3169 assert_eq!(config.webhook_url, "http://example.com");
3170 assert_eq!(config.min_severity, "High");
3171 assert_eq!(config.min_security_score, 70);
3172 assert_eq!(config.cooldown_seconds, 300);
3173 assert!(config.channels.is_empty());
3174 }
3175
3176 #[test]
3177 fn test_alert_config_multi_channel() {
3178 let json = r#"{
3179 "enabled": true,
3180 "cooldown_seconds": 120,
3181 "channels": [
3182 {"type": "slack", "url": "https://hooks.slack.com/services/T/B/x", "min_severity": "Medium"},
3183 {"type": "pagerduty", "routing_key": "abc123", "min_severity": "Critical"},
3184 {"type": "webhook", "url": "https://example.com/hook", "min_severity": "High"}
3185 ]
3186 }"#;
3187 let config: AlertConfig = serde_json::from_str(json).unwrap();
3188 assert!(config.enabled);
3189 assert_eq!(config.channels.len(), 3);
3190 assert_eq!(config.channels[0].channel_type, "slack");
3191 assert_eq!(config.channels[0].min_severity, "Medium");
3192 assert_eq!(config.channels[1].channel_type, "pagerduty");
3193 assert_eq!(config.channels[1].routing_key.as_deref(), Some("abc123"));
3194 assert_eq!(config.channels[2].channel_type, "webhook");
3195 }
3196
3197 #[test]
3198 fn test_alert_channel_config_effective_url() {
3199 let cfg = AlertChannelConfig {
3201 channel_type: "webhook".to_string(),
3202 url: Some("https://primary.com".to_string()),
3203 webhook_url: Some("https://fallback.com".to_string()),
3204 routing_key: None,
3205 min_severity: "High".to_string(),
3206 min_security_score: 70,
3207 };
3208 assert_eq!(cfg.effective_url(), Some("https://primary.com"));
3209
3210 let cfg2 = AlertChannelConfig {
3212 url: None,
3213 webhook_url: Some("https://fallback.com".to_string()),
3214 ..cfg.clone()
3215 };
3216 assert_eq!(cfg2.effective_url(), Some("https://fallback.com"));
3217
3218 let cfg3 = AlertChannelConfig {
3220 url: Some(String::new()),
3221 webhook_url: None,
3222 ..cfg
3223 };
3224 assert!(cfg3.effective_url().is_none());
3225 }
3226
3227 #[test]
3228 fn test_proxy_config_default_includes_alerts() {
3229 let config = ProxyConfig::default();
3230 assert!(!config.alerts.enabled);
3231 assert!(config.alerts.webhook_url.is_empty());
3232 assert_eq!(config.alerts.min_severity, "High");
3233 assert!(config.alerts.channels.is_empty());
3234 }
3235
3236 #[test]
3237 fn test_security_severity_display() {
3238 assert_eq!(SecuritySeverity::Info.to_string(), "Info");
3239 assert_eq!(SecuritySeverity::Low.to_string(), "Low");
3240 assert_eq!(SecuritySeverity::Medium.to_string(), "Medium");
3241 assert_eq!(SecuritySeverity::High.to_string(), "High");
3242 assert_eq!(SecuritySeverity::Critical.to_string(), "Critical");
3243 }
3244
3245 #[test]
3246 fn test_security_severity_from_str() {
3247 assert_eq!(
3248 "Info".parse::<SecuritySeverity>().unwrap(),
3249 SecuritySeverity::Info
3250 );
3251 assert_eq!(
3252 "low".parse::<SecuritySeverity>().unwrap(),
3253 SecuritySeverity::Low
3254 );
3255 assert_eq!(
3256 "MEDIUM".parse::<SecuritySeverity>().unwrap(),
3257 SecuritySeverity::Medium
3258 );
3259 assert_eq!(
3260 "High".parse::<SecuritySeverity>().unwrap(),
3261 SecuritySeverity::High
3262 );
3263 assert_eq!(
3264 "critical".parse::<SecuritySeverity>().unwrap(),
3265 SecuritySeverity::Critical
3266 );
3267 assert!("unknown".parse::<SecuritySeverity>().is_err());
3268 }
3269
3270 #[test]
3273 fn test_budget_window_duration_secs() {
3274 assert_eq!(BudgetWindow::Hourly.duration_secs(), 3_600);
3275 assert_eq!(BudgetWindow::Daily.duration_secs(), 86_400);
3276 assert_eq!(BudgetWindow::Weekly.duration_secs(), 604_800);
3277 assert_eq!(BudgetWindow::Monthly.duration_secs(), 2_592_000);
3278 }
3279
3280 #[test]
3281 fn test_budget_window_cache_ttl_exceeds_window() {
3282 for window in [
3283 BudgetWindow::Hourly,
3284 BudgetWindow::Daily,
3285 BudgetWindow::Weekly,
3286 BudgetWindow::Monthly,
3287 ] {
3288 assert!(window.cache_ttl().as_secs() > window.duration_secs());
3289 }
3290 }
3291
3292 #[test]
3293 fn test_budget_window_display() {
3294 assert_eq!(BudgetWindow::Hourly.to_string(), "hourly");
3295 assert_eq!(BudgetWindow::Daily.to_string(), "daily");
3296 assert_eq!(BudgetWindow::Weekly.to_string(), "weekly");
3297 assert_eq!(BudgetWindow::Monthly.to_string(), "monthly");
3298 }
3299
3300 #[test]
3301 fn test_budget_window_serialization() {
3302 let window = BudgetWindow::Daily;
3303 let json = serde_json::to_string(&window).unwrap();
3304 assert_eq!(json, r#""daily""#);
3305 let deser: BudgetWindow = serde_json::from_str(&json).unwrap();
3306 assert_eq!(deser, window);
3307 }
3308
3309 #[test]
3310 fn test_budget_cap_serialization() {
3311 let cap = BudgetCap {
3312 window: BudgetWindow::Hourly,
3313 hard_limit_usd: 10.0,
3314 soft_limit_usd: Some(8.0),
3315 };
3316 let json = serde_json::to_string(&cap).unwrap();
3317 let deser: BudgetCap = serde_json::from_str(&json).unwrap();
3318 assert_eq!(deser.window, BudgetWindow::Hourly);
3319 assert!((deser.hard_limit_usd - 10.0).abs() < f64::EPSILON);
3320 assert!((deser.soft_limit_usd.unwrap() - 8.0).abs() < f64::EPSILON);
3321 }
3322
3323 #[test]
3324 fn test_budget_cap_no_soft_limit() {
3325 let json = r#"{"window":"monthly","hard_limit_usd":100.0}"#;
3326 let cap: BudgetCap = serde_json::from_str(json).unwrap();
3327 assert_eq!(cap.window, BudgetWindow::Monthly);
3328 assert!(cap.soft_limit_usd.is_none());
3329 }
3330
3331 #[test]
3332 fn test_token_cap_serialization() {
3333 let cap = TokenCap {
3334 max_prompt_tokens: Some(4096),
3335 max_completion_tokens: Some(2048),
3336 max_total_tokens: None,
3337 };
3338 let json = serde_json::to_string(&cap).unwrap();
3339 let deser: TokenCap = serde_json::from_str(&json).unwrap();
3340 assert_eq!(deser.max_prompt_tokens, Some(4096));
3341 assert_eq!(deser.max_completion_tokens, Some(2048));
3342 assert!(deser.max_total_tokens.is_none());
3343 }
3344
3345 #[test]
3346 fn test_agent_cost_cap_serialization() {
3347 let agent_cap = AgentCostCap {
3348 agent_id: "agent-007".to_string(),
3349 budget_caps: vec![BudgetCap {
3350 window: BudgetWindow::Daily,
3351 hard_limit_usd: 50.0,
3352 soft_limit_usd: Some(40.0),
3353 }],
3354 token_cap: Some(TokenCap {
3355 max_prompt_tokens: Some(8192),
3356 max_completion_tokens: None,
3357 max_total_tokens: Some(16384),
3358 }),
3359 };
3360 let json = serde_json::to_string(&agent_cap).unwrap();
3361 let deser: AgentCostCap = serde_json::from_str(&json).unwrap();
3362 assert_eq!(deser.agent_id, "agent-007");
3363 assert_eq!(deser.budget_caps.len(), 1);
3364 assert!(deser.token_cap.is_some());
3365 }
3366
3367 #[test]
3368 fn test_cost_cap_config_default() {
3369 let config = CostCapConfig::default();
3370 assert!(!config.enabled);
3371 assert!(config.default_budget_caps.is_empty());
3372 assert!(config.default_token_cap.is_none());
3373 assert!(config.agents.is_empty());
3374 }
3375
3376 #[test]
3377 fn test_cost_cap_config_serialization() {
3378 let config = CostCapConfig {
3379 enabled: true,
3380 default_budget_caps: vec![BudgetCap {
3381 window: BudgetWindow::Daily,
3382 hard_limit_usd: 100.0,
3383 soft_limit_usd: Some(80.0),
3384 }],
3385 default_token_cap: Some(TokenCap {
3386 max_prompt_tokens: Some(4096),
3387 max_completion_tokens: Some(4096),
3388 max_total_tokens: None,
3389 }),
3390 agents: vec![AgentCostCap {
3391 agent_id: "heavy-agent".to_string(),
3392 budget_caps: vec![BudgetCap {
3393 window: BudgetWindow::Daily,
3394 hard_limit_usd: 200.0,
3395 soft_limit_usd: None,
3396 }],
3397 token_cap: None,
3398 }],
3399 };
3400 let json = serde_json::to_string(&config).unwrap();
3401 let deser: CostCapConfig = serde_json::from_str(&json).unwrap();
3402 assert!(deser.enabled);
3403 assert_eq!(deser.default_budget_caps.len(), 1);
3404 assert!(deser.default_token_cap.is_some());
3405 assert_eq!(deser.agents.len(), 1);
3406 assert_eq!(deser.agents[0].agent_id, "heavy-agent");
3407 }
3408
3409 #[test]
3410 fn test_proxy_config_default_includes_cost_caps() {
3411 let config = ProxyConfig::default();
3412 assert!(!config.cost_caps.enabled);
3413 assert!(config.cost_caps.default_budget_caps.is_empty());
3414 }
3415
3416 #[test]
3417 fn test_proxy_config_default_includes_security_analysis() {
3418 let config = ProxyConfig::default();
3419 assert!(config.security_analysis.ml_enabled);
3420 assert_eq!(
3421 config.security_analysis.ml_model,
3422 "protectai/deberta-v3-base-prompt-injection-v2"
3423 );
3424 assert!((config.security_analysis.ml_threshold - 0.8).abs() < f64::EPSILON);
3425 assert_eq!(
3426 config.security_analysis.ml_cache_dir,
3427 "~/.cache/llmtrace/models"
3428 );
3429 assert!(config.security_analysis.ml_preload);
3430 assert_eq!(config.security_analysis.ml_download_timeout_seconds, 300);
3431 assert!(!config.security_analysis.ner_enabled);
3432 assert_eq!(config.security_analysis.ner_model, "dslim/bert-base-NER");
3433 assert!(!config.security_analysis.fusion_enabled);
3434 assert!(config.security_analysis.fusion_model_path.is_none());
3435 assert!(config.security_analysis.jailbreak_enabled);
3436 assert!((config.security_analysis.jailbreak_threshold - 0.7).abs() < f32::EPSILON);
3437 }
3438
3439 #[test]
3440 fn test_security_analysis_config_default() {
3441 let config = SecurityAnalysisConfig::default();
3442 assert!(config.ml_enabled);
3443 assert_eq!(
3444 config.ml_model,
3445 "protectai/deberta-v3-base-prompt-injection-v2"
3446 );
3447 assert!((config.ml_threshold - 0.8).abs() < f64::EPSILON);
3448 assert_eq!(config.ml_cache_dir, "~/.cache/llmtrace/models");
3449 assert!(config.ml_preload);
3450 assert_eq!(config.ml_download_timeout_seconds, 300);
3451 assert!(!config.ner_enabled);
3452 assert_eq!(config.ner_model, "dslim/bert-base-NER");
3453 assert!(!config.fusion_enabled);
3454 assert!(config.fusion_model_path.is_none());
3455 assert!(config.jailbreak_enabled);
3456 assert!((config.jailbreak_threshold - 0.7).abs() < f32::EPSILON);
3457 }
3458
3459 #[test]
3460 fn test_security_analysis_config_serialization() {
3461 let config = SecurityAnalysisConfig {
3462 ml_enabled: true,
3463 ml_model: "custom/model".to_string(),
3464 ml_threshold: 0.9,
3465 ml_cache_dir: "/tmp/models".to_string(),
3466 ml_preload: false,
3467 ml_download_timeout_seconds: 600,
3468 ner_enabled: true,
3469 ner_model: "dslim/bert-base-NER".to_string(),
3470 fusion_enabled: false,
3471 fusion_model_path: None,
3472 jailbreak_enabled: true,
3473 jailbreak_threshold: 0.7,
3474 injecguard_enabled: false,
3475 injecguard_model: default_injecguard_model(),
3476 injecguard_threshold: default_injecguard_threshold(),
3477 piguard_enabled: false,
3478 piguard_model: default_piguard_model(),
3479 piguard_threshold: default_piguard_threshold(),
3480 operating_point: OperatingPoint::default(),
3481 over_defence: false,
3482 };
3483 let json = serde_json::to_string(&config).unwrap();
3484 let deserialized: SecurityAnalysisConfig = serde_json::from_str(&json).unwrap();
3485 assert!(deserialized.ml_enabled);
3486 assert_eq!(deserialized.ml_model, "custom/model");
3487 assert!((deserialized.ml_threshold - 0.9).abs() < f64::EPSILON);
3488 assert_eq!(deserialized.ml_cache_dir, "/tmp/models");
3489 assert!(!deserialized.ml_preload);
3490 assert_eq!(deserialized.ml_download_timeout_seconds, 600);
3491 assert!(deserialized.ner_enabled);
3492 assert_eq!(deserialized.ner_model, "dslim/bert-base-NER");
3493 }
3494
3495 #[test]
3500 fn test_agent_action_type_display() {
3501 assert_eq!(AgentActionType::ToolCall.to_string(), "tool_call");
3502 assert_eq!(
3503 AgentActionType::SkillInvocation.to_string(),
3504 "skill_invocation"
3505 );
3506 assert_eq!(
3507 AgentActionType::CommandExecution.to_string(),
3508 "command_execution"
3509 );
3510 assert_eq!(AgentActionType::WebAccess.to_string(), "web_access");
3511 assert_eq!(AgentActionType::FileAccess.to_string(), "file_access");
3512 }
3513
3514 #[test]
3515 fn test_agent_action_type_serialization() {
3516 let types = vec![
3517 AgentActionType::ToolCall,
3518 AgentActionType::SkillInvocation,
3519 AgentActionType::CommandExecution,
3520 AgentActionType::WebAccess,
3521 AgentActionType::FileAccess,
3522 ];
3523 for action_type in types {
3524 let json = serde_json::to_string(&action_type).unwrap();
3525 let deser: AgentActionType = serde_json::from_str(&json).unwrap();
3526 assert_eq!(action_type, deser);
3527 }
3528 }
3529
3530 #[test]
3531 fn test_agent_action_creation() {
3532 let action = AgentAction::new(AgentActionType::ToolCall, "get_weather".to_string());
3533 assert_eq!(action.action_type, AgentActionType::ToolCall);
3534 assert_eq!(action.name, "get_weather");
3535 assert!(action.success);
3536 assert!(action.arguments.is_none());
3537 assert!(action.result.is_none());
3538 }
3539
3540 #[test]
3541 fn test_agent_action_builder() {
3542 let action = AgentAction::new(AgentActionType::CommandExecution, "ls -la".to_string())
3543 .with_arguments("-la /tmp".to_string())
3544 .with_result("file1\nfile2".to_string())
3545 .with_duration_ms(150)
3546 .with_exit_code(0)
3547 .with_metadata("cwd".to_string(), "/home".to_string());
3548
3549 assert_eq!(action.action_type, AgentActionType::CommandExecution);
3550 assert_eq!(action.arguments, Some("-la /tmp".to_string()));
3551 assert_eq!(action.result, Some("file1\nfile2".to_string()));
3552 assert_eq!(action.duration_ms, Some(150));
3553 assert_eq!(action.exit_code, Some(0));
3554 assert!(action.success);
3555 assert_eq!(action.metadata.get("cwd"), Some(&"/home".to_string()));
3556 }
3557
3558 #[test]
3559 fn test_agent_action_web_access() {
3560 let action = AgentAction::new(
3561 AgentActionType::WebAccess,
3562 "https://api.example.com".to_string(),
3563 )
3564 .with_http("GET".to_string(), 200)
3565 .with_duration_ms(500);
3566
3567 assert_eq!(action.http_method, Some("GET".to_string()));
3568 assert_eq!(action.http_status, Some(200));
3569 }
3570
3571 #[test]
3572 fn test_agent_action_file_access() {
3573 let action = AgentAction::new(AgentActionType::FileAccess, "/etc/passwd".to_string())
3574 .with_file_operation("read".to_string());
3575
3576 assert_eq!(action.file_operation, Some("read".to_string()));
3577 }
3578
3579 #[test]
3580 fn test_agent_action_failure() {
3581 let action = AgentAction::new(AgentActionType::CommandExecution, "rm -rf /".to_string())
3582 .with_failure()
3583 .with_exit_code(1);
3584
3585 assert!(!action.success);
3586 assert_eq!(action.exit_code, Some(1));
3587 }
3588
3589 #[test]
3590 fn test_agent_action_result_truncation() {
3591 let long_result = "x".repeat(AGENT_ACTION_RESULT_MAX_BYTES + 100);
3592 let action = AgentAction::new(AgentActionType::ToolCall, "big_output".to_string())
3593 .with_result(long_result);
3594
3595 assert_eq!(
3596 action.result.as_ref().unwrap().len(),
3597 AGENT_ACTION_RESULT_MAX_BYTES
3598 );
3599 }
3600
3601 #[test]
3602 fn test_agent_action_serialization() {
3603 let action = AgentAction::new(AgentActionType::ToolCall, "get_weather".to_string())
3604 .with_arguments(r#"{"location": "London"}"#.to_string())
3605 .with_result(r#"{"temp": 15}"#.to_string())
3606 .with_duration_ms(200);
3607
3608 let json = serde_json::to_string(&action).unwrap();
3609 let deser: AgentAction = serde_json::from_str(&json).unwrap();
3610
3611 assert_eq!(deser.action_type, AgentActionType::ToolCall);
3612 assert_eq!(deser.name, "get_weather");
3613 assert_eq!(deser.arguments, action.arguments);
3614 assert_eq!(deser.result, action.result);
3615 assert_eq!(deser.duration_ms, Some(200));
3616 assert!(deser.success);
3617 }
3618
3619 #[test]
3620 fn test_trace_span_agent_actions() {
3621 let mut span = TraceSpan::new(
3622 Uuid::new_v4(),
3623 TenantId::new(),
3624 "chat_completion".to_string(),
3625 LLMProvider::OpenAI,
3626 "gpt-4".to_string(),
3627 "test".to_string(),
3628 );
3629
3630 assert!(span.agent_actions.is_empty());
3631 assert!(!span.has_tool_calls());
3632 assert!(!span.has_web_access());
3633 assert!(!span.has_commands());
3634
3635 span.add_agent_action(AgentAction::new(
3636 AgentActionType::ToolCall,
3637 "search".to_string(),
3638 ));
3639 span.add_agent_action(AgentAction::new(
3640 AgentActionType::WebAccess,
3641 "https://example.com".to_string(),
3642 ));
3643 span.add_agent_action(AgentAction::new(
3644 AgentActionType::CommandExecution,
3645 "ls".to_string(),
3646 ));
3647 span.add_agent_action(AgentAction::new(
3648 AgentActionType::FileAccess,
3649 "/tmp/file.txt".to_string(),
3650 ));
3651
3652 assert_eq!(span.agent_actions.len(), 4);
3653 assert!(span.has_tool_calls());
3654 assert!(span.has_web_access());
3655 assert!(span.has_commands());
3656 assert_eq!(span.tool_calls().len(), 1);
3657 assert_eq!(span.web_accesses().len(), 1);
3658 assert_eq!(span.commands().len(), 1);
3659 }
3660
3661 #[test]
3662 fn test_trace_span_with_agent_actions_serialization() {
3663 let mut span = TraceSpan::new(
3664 Uuid::new_v4(),
3665 TenantId::new(),
3666 "chat_completion".to_string(),
3667 LLMProvider::OpenAI,
3668 "gpt-4".to_string(),
3669 "test prompt".to_string(),
3670 );
3671 span.add_agent_action(
3672 AgentAction::new(AgentActionType::ToolCall, "calculate".to_string())
3673 .with_arguments(r#"{"expr": "2+2"}"#.to_string())
3674 .with_result("4".to_string()),
3675 );
3676
3677 let json = serde_json::to_string(&span).unwrap();
3678 let deser: TraceSpan = serde_json::from_str(&json).unwrap();
3679
3680 assert_eq!(deser.agent_actions.len(), 1);
3681 assert_eq!(deser.agent_actions[0].name, "calculate");
3682 assert_eq!(
3683 deser.agent_actions[0].action_type,
3684 AgentActionType::ToolCall
3685 );
3686 }
3687
3688 #[test]
3689 fn test_trace_span_deserialize_without_agent_actions() {
3690 let json = r#"{
3692 "trace_id": "00000000-0000-0000-0000-000000000001",
3693 "span_id": "00000000-0000-0000-0000-000000000002",
3694 "parent_span_id": null,
3695 "tenant_id": "00000000-0000-0000-0000-000000000003",
3696 "operation_name": "test",
3697 "start_time": "2024-01-01T00:00:00Z",
3698 "end_time": null,
3699 "provider": "OpenAI",
3700 "model_name": "gpt-4",
3701 "prompt": "hello",
3702 "response": null,
3703 "prompt_tokens": null,
3704 "completion_tokens": null,
3705 "total_tokens": null,
3706 "time_to_first_token_ms": null,
3707 "duration_ms": null,
3708 "status_code": null,
3709 "error_message": null,
3710 "estimated_cost_usd": null,
3711 "security_score": null,
3712 "security_findings": [],
3713 "tags": {},
3714 "events": []
3715 }"#;
3716 let span: TraceSpan = serde_json::from_str(json).unwrap();
3717 assert!(span.agent_actions.is_empty());
3718 }
3719
3720 #[test]
3725 fn test_api_key_role_has_permission() {
3726 assert!(ApiKeyRole::Admin.has_permission(ApiKeyRole::Admin));
3728 assert!(ApiKeyRole::Admin.has_permission(ApiKeyRole::Operator));
3729 assert!(ApiKeyRole::Admin.has_permission(ApiKeyRole::Viewer));
3730
3731 assert!(!ApiKeyRole::Operator.has_permission(ApiKeyRole::Admin));
3733 assert!(ApiKeyRole::Operator.has_permission(ApiKeyRole::Operator));
3734 assert!(ApiKeyRole::Operator.has_permission(ApiKeyRole::Viewer));
3735
3736 assert!(!ApiKeyRole::Viewer.has_permission(ApiKeyRole::Admin));
3738 assert!(!ApiKeyRole::Viewer.has_permission(ApiKeyRole::Operator));
3739 assert!(ApiKeyRole::Viewer.has_permission(ApiKeyRole::Viewer));
3740 }
3741
3742 #[test]
3743 fn test_api_key_role_roundtrip() {
3744 for role in &[ApiKeyRole::Admin, ApiKeyRole::Operator, ApiKeyRole::Viewer] {
3745 let s = role.to_string();
3746 let parsed: ApiKeyRole = s.parse().unwrap();
3747 assert_eq!(*role, parsed);
3748 }
3749 }
3750
3751 #[test]
3752 fn test_api_key_role_serialization() {
3753 let role = ApiKeyRole::Operator;
3754 let json = serde_json::to_string(&role).unwrap();
3755 assert_eq!(json, r#""operator""#);
3756 let deserialized: ApiKeyRole = serde_json::from_str(&json).unwrap();
3757 assert_eq!(deserialized, ApiKeyRole::Operator);
3758 }
3759
3760 #[test]
3761 fn test_api_key_record_serialization_skips_hash() {
3762 let record = ApiKeyRecord {
3763 id: Uuid::new_v4(),
3764 tenant_id: TenantId::new(),
3765 name: "test-key".to_string(),
3766 key_hash: "abc123".to_string(),
3767 key_prefix: "llmt_ab…".to_string(),
3768 role: ApiKeyRole::Viewer,
3769 created_at: Utc::now(),
3770 revoked_at: None,
3771 };
3772 let json = serde_json::to_string(&record).unwrap();
3773 assert!(!json.contains("abc123"));
3775 assert!(json.contains("llmt_ab"));
3776 }
3777
3778 #[test]
3779 fn test_auth_config_default() {
3780 let cfg = AuthConfig::default();
3781 assert!(!cfg.enabled);
3782 assert!(cfg.admin_key.is_none());
3783 }
3784
3785 #[test]
3786 fn test_auth_config_serialization() {
3787 let cfg = AuthConfig {
3788 enabled: true,
3789 admin_key: Some("my-secret".to_string()),
3790 };
3791 let json = serde_json::to_string(&cfg).unwrap();
3792 let deser: AuthConfig = serde_json::from_str(&json).unwrap();
3793 assert!(deser.enabled);
3794 assert_eq!(deser.admin_key.as_deref(), Some("my-secret"));
3795 }
3796
3797 #[test]
3798 fn test_proxy_config_includes_auth() {
3799 let config = ProxyConfig::default();
3800 assert!(!config.auth.enabled);
3801 assert!(config.auth.admin_key.is_none());
3802 }
3803
3804 #[test]
3809 fn test_pii_action_default() {
3810 assert_eq!(PiiAction::default(), PiiAction::AlertOnly);
3811 }
3812
3813 #[test]
3814 fn test_pii_action_display() {
3815 assert_eq!(PiiAction::AlertOnly.to_string(), "alert_only");
3816 assert_eq!(PiiAction::AlertAndRedact.to_string(), "alert_and_redact");
3817 assert_eq!(PiiAction::RedactSilent.to_string(), "redact_silent");
3818 }
3819
3820 #[test]
3821 fn test_pii_action_serialization() {
3822 let action = PiiAction::AlertAndRedact;
3823 let json = serde_json::to_string(&action).unwrap();
3824 assert_eq!(json, r#""alert_and_redact""#);
3825 let deser: PiiAction = serde_json::from_str(&json).unwrap();
3826 assert_eq!(deser, PiiAction::AlertAndRedact);
3827 }
3828
3829 #[test]
3830 fn test_pii_config_default() {
3831 let config = PiiConfig::default();
3832 assert_eq!(config.action, PiiAction::AlertOnly);
3833 }
3834
3835 #[test]
3836 fn test_pii_config_serialization() {
3837 let config = PiiConfig {
3838 action: PiiAction::RedactSilent,
3839 };
3840 let json = serde_json::to_string(&config).unwrap();
3841 let deser: PiiConfig = serde_json::from_str(&json).unwrap();
3842 assert_eq!(deser.action, PiiAction::RedactSilent);
3843 }
3844
3845 #[test]
3846 fn test_pii_config_deserializes_with_defaults() {
3847 let json = r#"{}"#;
3848 let config: PiiConfig = serde_json::from_str(json).unwrap();
3849 assert_eq!(config.action, PiiAction::AlertOnly);
3850 }
3851
3852 #[test]
3853 fn test_proxy_config_default_includes_pii() {
3854 let config = ProxyConfig::default();
3855 assert_eq!(config.pii.action, PiiAction::AlertOnly);
3856 }
3857}