1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CommandRequest {
9 pub input: String,
11
12 pub shell: ShellType,
14
15 pub safety_level: SafetyLevel,
17
18 pub context: Option<String>,
20
21 pub backend_preference: Option<String>,
23}
24
25impl CommandRequest {
26 pub fn new(input: impl Into<String>, shell: ShellType) -> Self {
28 let input = input.into();
29 let trimmed = input.trim().to_string();
30
31 Self {
32 input: trimmed,
33 shell,
34 safety_level: SafetyLevel::default(),
35 context: None,
36 backend_preference: None,
37 }
38 }
39
40 pub fn with_safety(mut self, level: SafetyLevel) -> Self {
42 self.safety_level = level;
43 self
44 }
45
46 pub fn with_context(mut self, ctx: impl Into<String>) -> Self {
48 self.context = Some(ctx.into());
49 self
50 }
51
52 pub fn with_backend(mut self, backend: impl Into<String>) -> Self {
54 self.backend_preference = Some(backend.into());
55 self
56 }
57
58 pub fn validate(&self) -> Result<(), String> {
60 if self.input.is_empty() {
61 return Err("Input cannot be empty".to_string());
62 }
63 Ok(())
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct GeneratedCommand {
70 pub command: String,
72
73 pub explanation: String,
75
76 pub safety_level: RiskLevel,
78
79 pub estimated_impact: String,
81
82 pub alternatives: Vec<String>,
84
85 pub backend_used: String,
87
88 pub generation_time_ms: u64,
90
91 pub confidence_score: f64,
93}
94
95impl GeneratedCommand {
96 pub fn validate(&self) -> Result<(), String> {
98 if self.command.is_empty() {
99 return Err("Command cannot be empty".to_string());
100 }
101 if self.explanation.is_empty() {
102 return Err("Explanation cannot be empty".to_string());
103 }
104 if !(0.0..=1.0).contains(&self.confidence_score) {
105 return Err(format!(
106 "Confidence score must be between 0.0 and 1.0, got {}",
107 self.confidence_score
108 ));
109 }
110 Ok(())
111 }
112}
113
114impl std::fmt::Display for GeneratedCommand {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 use colored::Colorize;
117
118 writeln!(f, "{}", "Generated Command:".bold())?;
119 writeln!(f, " {}", self.command.bright_cyan().bold())?;
120 writeln!(f)?;
121 writeln!(f, "{}", "Explanation:".bold())?;
122 writeln!(f, " {}", self.explanation)?;
123 writeln!(f)?;
124 writeln!(f, "{} {}", "Risk Level:".bold(), self.safety_level)?;
125 writeln!(f, "{} {}", "Backend:".bold(), self.backend_used)?;
126 writeln!(
127 f,
128 "{} {:.0}%",
129 "Confidence:".bold(),
130 self.confidence_score * 100.0
131 )?;
132
133 if !self.alternatives.is_empty() {
134 writeln!(f)?;
135 writeln!(f, "{}", "Alternatives:".bold())?;
136 for alt in &self.alternatives {
137 writeln!(f, " • {}", alt.dimmed())?;
138 }
139 }
140
141 Ok(())
142 }
143}
144
145#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
146#[serde(rename_all = "lowercase")]
147pub enum RiskLevel {
148 Safe,
149 Moderate,
150 High,
151 Critical,
152}
153
154impl RiskLevel {
155 pub fn requires_confirmation(&self, safety_level: SafetyLevel) -> bool {
157 match safety_level {
158 SafetyLevel::Strict => matches!(self, Self::Moderate | Self::High | Self::Critical),
159 SafetyLevel::Moderate => matches!(self, Self::High | Self::Critical),
160 SafetyLevel::Permissive => matches!(self, Self::Critical),
161 }
162 }
163
164 pub fn is_blocked(&self, safety_level: SafetyLevel) -> bool {
166 match safety_level {
167 SafetyLevel::Strict => matches!(self, Self::High | Self::Critical),
168 SafetyLevel::Moderate => matches!(self, Self::Critical),
169 SafetyLevel::Permissive => false,
170 }
171 }
172}
173
174impl std::fmt::Display for RiskLevel {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 use colored::Colorize;
177 match self {
178 Self::Safe => write!(f, "{}", "Safe".green()),
179 Self::Moderate => write!(f, "{}", "Moderate".yellow()),
180 Self::High => write!(f, "{}", "High".bright_red()),
181 Self::Critical => write!(f, "{}", "Critical".red().bold()),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "lowercase")]
188pub enum SafetyLevel {
189 Strict,
191 Moderate,
193 Permissive,
195}
196
197impl std::str::FromStr for SafetyLevel {
198 type Err = String;
199
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 match s.to_lowercase().as_str() {
202 "strict" => Ok(SafetyLevel::Strict),
203 "moderate" => Ok(SafetyLevel::Moderate),
204 "permissive" => Ok(SafetyLevel::Permissive),
205 _ => Err(format!(
206 "Invalid safety level '{}'. Valid values: strict, moderate, permissive",
207 s
208 )),
209 }
210 }
211}
212
213impl Default for SafetyLevel {
214 fn default() -> Self {
215 Self::Moderate
216 }
217}
218
219impl std::fmt::Display for SafetyLevel {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 match self {
222 Self::Strict => write!(f, "strict"),
223 Self::Moderate => write!(f, "moderate"),
224 Self::Permissive => write!(f, "permissive"),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "lowercase")]
231pub enum BackendType {
232 Mock,
234 Embedded,
236 Ollama,
238 VLlm,
240 Mlx,
242}
243
244impl std::str::FromStr for BackendType {
245 type Err = String;
246
247 fn from_str(s: &str) -> Result<Self, Self::Err> {
248 match s.to_lowercase().as_str() {
249 "mock" => Ok(Self::Mock),
250 "embedded" => Ok(Self::Embedded),
251 "ollama" => Ok(Self::Ollama),
252 "vllm" => Ok(Self::VLlm),
253 "mlx" => Ok(Self::Mlx),
254 _ => Err(format!("Unknown backend type: {}", s)),
255 }
256 }
257}
258
259impl std::fmt::Display for BackendType {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Self::Mock => write!(f, "mock"),
263 Self::Embedded => write!(f, "embedded"),
264 Self::Ollama => write!(f, "ollama"),
265 Self::VLlm => write!(f, "vllm"),
266 Self::Mlx => write!(f, "mlx"),
267 }
268 }
269}
270
271#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "lowercase")]
273pub enum ShellType {
274 Bash,
275 Zsh,
276 Fish,
277 Sh,
278 PowerShell,
279 Cmd,
280 Unknown,
281}
282
283impl ShellType {
284 pub fn detect() -> Self {
286 if let Ok(shell) = std::env::var("SHELL") {
288 if shell.contains("bash") {
289 return Self::Bash;
290 } else if shell.contains("zsh") {
291 return Self::Zsh;
292 } else if shell.contains("fish") {
293 return Self::Fish;
294 } else if shell.ends_with("/sh") {
295 return Self::Sh;
296 }
297 }
298
299 #[cfg(target_os = "windows")]
301 {
302 if std::env::var("PSModulePath").is_ok() {
303 return Self::PowerShell;
304 }
305 return Self::Cmd;
306 }
307
308 Self::Unknown
309 }
310
311 pub fn is_posix(&self) -> bool {
313 matches!(self, Self::Bash | Self::Zsh | Self::Fish | Self::Sh)
314 }
315
316 pub fn is_windows(&self) -> bool {
318 matches!(self, Self::PowerShell | Self::Cmd)
319 }
320}
321
322impl Default for ShellType {
323 fn default() -> Self {
324 Self::detect()
325 }
326}
327
328impl std::str::FromStr for ShellType {
329 type Err = String;
330
331 fn from_str(s: &str) -> Result<Self, Self::Err> {
332 match s.to_lowercase().as_str() {
333 "bash" => Ok(Self::Bash),
334 "zsh" => Ok(Self::Zsh),
335 "fish" => Ok(Self::Fish),
336 "sh" => Ok(Self::Sh),
337 "powershell" | "pwsh" => Ok(Self::PowerShell),
338 "cmd" => Ok(Self::Cmd),
339 _ => Ok(Self::Unknown),
340 }
341 }
342}
343
344impl std::fmt::Display for ShellType {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 match self {
347 Self::Bash => write!(f, "bash"),
348 Self::Zsh => write!(f, "zsh"),
349 Self::Fish => write!(f, "fish"),
350 Self::Sh => write!(f, "sh"),
351 Self::PowerShell => write!(f, "powershell"),
352 Self::Cmd => write!(f, "cmd"),
353 Self::Unknown => write!(f, "unknown"),
354 }
355 }
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct BackendInfo {
361 pub backend_type: BackendType,
363
364 pub model_name: String,
366
367 pub supports_streaming: bool,
369
370 pub max_tokens: u32,
372
373 pub typical_latency_ms: u64,
375
376 pub memory_usage_mb: u64,
378
379 pub version: String,
381}
382
383impl BackendInfo {
384 pub fn validate(&self) -> Result<(), String> {
386 if self.model_name.is_empty() {
387 return Err("Model name cannot be empty".to_string());
388 }
389 if self.max_tokens == 0 {
390 return Err("Max tokens must be positive".to_string());
391 }
392 if self.version.is_empty() {
393 return Err("Version cannot be empty".to_string());
394 }
395 Ok(())
396 }
397}
398
399use chrono::{DateTime, Utc};
406use std::collections::HashMap;
407use std::path::PathBuf;
408
409#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
411#[serde(rename_all = "lowercase")]
412pub enum Platform {
413 Linux,
414 MacOS,
415 Windows,
416}
417
418impl Platform {
419 pub fn detect() -> Self {
421 #[cfg(target_os = "linux")]
422 return Platform::Linux;
423
424 #[cfg(target_os = "macos")]
425 return Platform::MacOS;
426
427 #[cfg(target_os = "windows")]
428 return Platform::Windows;
429
430 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
431 compile_error!("Unsupported platform");
432 }
433
434 pub fn is_posix(&self) -> bool {
436 matches!(self, Platform::Linux | Platform::MacOS)
437 }
438}
439
440impl std::fmt::Display for Platform {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 match self {
443 Platform::Linux => write!(f, "Linux"),
444 Platform::MacOS => write!(f, "macOS"),
445 Platform::Windows => write!(f, "Windows"),
446 }
447 }
448}
449
450#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
452#[serde(rename_all = "lowercase")]
453pub enum LogLevel {
454 Debug,
455 Info,
456 Warn,
457 Error,
458}
459
460impl LogLevel {
461 pub fn to_tracing_level(&self) -> tracing::Level {
463 match self {
464 LogLevel::Debug => tracing::Level::DEBUG,
465 LogLevel::Info => tracing::Level::INFO,
466 LogLevel::Warn => tracing::Level::WARN,
467 LogLevel::Error => tracing::Level::ERROR,
468 }
469 }
470}
471
472impl std::str::FromStr for LogLevel {
473 type Err = String;
474
475 fn from_str(s: &str) -> Result<Self, Self::Err> {
476 match s.to_lowercase().as_str() {
477 "debug" => Ok(LogLevel::Debug),
478 "info" => Ok(LogLevel::Info),
479 "warn" | "warning" => Ok(LogLevel::Warn),
480 "error" | "err" => Ok(LogLevel::Error),
481 _ => Err(format!(
482 "Invalid log level '{}'. Valid options: debug, info, warn, error",
483 s
484 )),
485 }
486 }
487}
488
489impl std::fmt::Display for LogLevel {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 match self {
492 LogLevel::Debug => write!(f, "DEBUG"),
493 LogLevel::Info => write!(f, "INFO"),
494 LogLevel::Warn => write!(f, "WARN"),
495 LogLevel::Error => write!(f, "ERROR"),
496 }
497 }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct CachedModel {
503 pub model_id: String,
504 pub path: PathBuf,
505 pub checksum: String,
506 pub size_bytes: u64,
507 pub downloaded_at: DateTime<Utc>,
508 pub last_accessed: DateTime<Utc>,
509 pub version: Option<String>,
510}
511
512impl CachedModel {
513 pub fn validate(&self) -> Result<(), String> {
515 if self.model_id.is_empty() {
516 return Err("Model ID cannot be empty".to_string());
517 }
518 if self.checksum.len() != 64 {
519 return Err(format!(
520 "Checksum must be 64 characters (SHA256 hex), got {}",
521 self.checksum.len()
522 ));
523 }
524 if !self.checksum.chars().all(|c| c.is_ascii_hexdigit()) {
525 return Err("Checksum must be valid hexadecimal".to_string());
526 }
527 Ok(())
528 }
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct CacheManifest {
534 pub version: String,
535 pub models: HashMap<String, CachedModel>,
536 pub total_size_bytes: u64,
537 pub max_cache_size_bytes: u64,
538 pub last_updated: DateTime<Utc>,
539}
540
541impl CacheManifest {
542 pub fn new(max_size_gb: u64) -> Self {
544 Self {
545 version: "1.0.0".to_string(),
546 models: HashMap::new(),
547 total_size_bytes: 0,
548 max_cache_size_bytes: max_size_gb * 1024 * 1024 * 1024,
549 last_updated: Utc::now(),
550 }
551 }
552
553 pub fn add_model(&mut self, model: CachedModel) {
555 self.total_size_bytes += model.size_bytes;
556 self.models.insert(model.model_id.clone(), model);
557 self.last_updated = Utc::now();
558 }
559
560 pub fn remove_model(&mut self, model_id: &str) -> Option<CachedModel> {
562 if let Some(model) = self.models.remove(model_id) {
563 self.total_size_bytes = self.total_size_bytes.saturating_sub(model.size_bytes);
564 self.last_updated = Utc::now();
565 Some(model)
566 } else {
567 None
568 }
569 }
570
571 pub fn get_model(&self, model_id: &str) -> Option<&CachedModel> {
573 self.models.get(model_id)
574 }
575
576 pub fn cleanup_lru(&mut self) -> Vec<String> {
578 let mut removed = Vec::new();
579
580 while self.total_size_bytes > self.max_cache_size_bytes && !self.models.is_empty() {
581 let lru_model_id = self
583 .models
584 .iter()
585 .min_by_key(|(_, model)| model.last_accessed)
586 .map(|(id, _)| id.clone());
587
588 if let Some(model_id) = lru_model_id {
589 self.remove_model(&model_id);
590 removed.push(model_id);
591 } else {
592 break;
593 }
594 }
595
596 removed
597 }
598
599 pub fn validate_integrity(&self) -> (Vec<String>, Vec<String>, Vec<String>) {
601 let mut valid = Vec::new();
602 let mut corrupted = Vec::new();
603 let mut missing = Vec::new();
604
605 for (model_id, model) in &self.models {
606 if !model.path.exists() {
607 missing.push(model_id.clone());
608 } else if model.validate().is_err() {
609 corrupted.push(model_id.clone());
610 } else {
611 valid.push(model_id.clone());
612 }
613 }
614
615 (valid, corrupted, missing)
616 }
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct UserConfiguration {
622 pub default_shell: Option<ShellType>,
623 pub safety_level: SafetyLevel,
624 pub default_model: Option<String>,
625 pub log_level: LogLevel,
626 pub cache_max_size_gb: u64,
627 pub log_rotation_days: u32,
628}
629
630impl Default for UserConfiguration {
631 fn default() -> Self {
632 Self {
633 default_shell: None, safety_level: SafetyLevel::Moderate,
635 default_model: None,
636 log_level: LogLevel::Info,
637 cache_max_size_gb: 10,
638 log_rotation_days: 7,
639 }
640 }
641}
642
643impl UserConfiguration {
644 pub fn builder() -> UserConfigurationBuilder {
646 UserConfigurationBuilder::new()
647 }
648
649 pub fn validate(&self) -> Result<(), String> {
651 if self.cache_max_size_gb < 1 || self.cache_max_size_gb > 1000 {
652 return Err(format!(
653 "cache_max_size_gb must be between 1 and 1000, got {}",
654 self.cache_max_size_gb
655 ));
656 }
657 if self.log_rotation_days < 1 || self.log_rotation_days > 365 {
658 return Err(format!(
659 "log_rotation_days must be between 1 and 365, got {}",
660 self.log_rotation_days
661 ));
662 }
663 Ok(())
664 }
665}
666
667pub struct UserConfigurationBuilder {
669 default_shell: Option<ShellType>,
670 safety_level: SafetyLevel,
671 default_model: Option<String>,
672 log_level: LogLevel,
673 cache_max_size_gb: u64,
674 log_rotation_days: u32,
675}
676
677impl Default for UserConfigurationBuilder {
678 fn default() -> Self {
679 Self::new()
680 }
681}
682
683impl UserConfigurationBuilder {
684 pub fn new() -> Self {
685 let defaults = UserConfiguration::default();
686 Self {
687 default_shell: defaults.default_shell,
688 safety_level: defaults.safety_level,
689 default_model: defaults.default_model,
690 log_level: defaults.log_level,
691 cache_max_size_gb: defaults.cache_max_size_gb,
692 log_rotation_days: defaults.log_rotation_days,
693 }
694 }
695
696 pub fn default_shell(mut self, shell: ShellType) -> Self {
697 self.default_shell = Some(shell);
698 self
699 }
700
701 pub fn safety_level(mut self, level: SafetyLevel) -> Self {
702 self.safety_level = level;
703 self
704 }
705
706 pub fn default_model(mut self, model: impl Into<String>) -> Self {
707 self.default_model = Some(model.into());
708 self
709 }
710
711 pub fn log_level(mut self, level: LogLevel) -> Self {
712 self.log_level = level;
713 self
714 }
715
716 pub fn cache_max_size_gb(mut self, size: u64) -> Self {
717 self.cache_max_size_gb = size;
718 self
719 }
720
721 pub fn log_rotation_days(mut self, days: u32) -> Self {
722 self.log_rotation_days = days;
723 self
724 }
725
726 pub fn build(self) -> Result<UserConfiguration, String> {
727 let config = UserConfiguration {
728 default_shell: self.default_shell,
729 safety_level: self.safety_level,
730 default_model: self.default_model,
731 log_level: self.log_level,
732 cache_max_size_gb: self.cache_max_size_gb,
733 log_rotation_days: self.log_rotation_days,
734 };
735 config.validate()?;
736 Ok(config)
737 }
738}
739
740pub struct ConfigSchema {
742 pub known_sections: Vec<String>,
743 pub known_keys: HashMap<String, String>,
744 pub deprecated_keys: HashMap<String, String>,
745}
746
747impl ConfigSchema {
748 pub fn new() -> Self {
749 let mut known_keys = HashMap::new();
750 known_keys.insert(
751 "general.safety_level".to_string(),
752 "SafetyLevel enum".to_string(),
753 );
754 known_keys.insert(
755 "general.default_shell".to_string(),
756 "ShellType enum".to_string(),
757 );
758 known_keys.insert("general.default_model".to_string(), "String".to_string());
759 known_keys.insert("logging.log_level".to_string(), "LogLevel enum".to_string());
760 known_keys.insert("logging.log_rotation_days".to_string(), "u32".to_string());
761 known_keys.insert("cache.max_size_gb".to_string(), "u64".to_string());
762
763 Self {
764 known_sections: vec![
765 "general".to_string(),
766 "logging".to_string(),
767 "cache".to_string(),
768 ],
769 known_keys,
770 deprecated_keys: HashMap::new(),
771 }
772 }
773
774 pub fn validate(&self, _config: &UserConfiguration) -> Result<(), String> {
775 Ok(())
777 }
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize)]
782pub struct ExecutionContext {
783 pub current_dir: PathBuf,
784 pub shell_type: ShellType,
785 pub platform: Platform,
786 pub environment_vars: HashMap<String, String>,
787 pub username: String,
788 pub hostname: String,
789 pub captured_at: DateTime<Utc>,
790}
791
792impl ExecutionContext {
793 pub fn new(
795 current_dir: PathBuf,
796 shell_type: ShellType,
797 platform: Platform,
798 ) -> Result<Self, String> {
799 if !current_dir.is_absolute() {
800 return Err("Current directory must be absolute path".to_string());
801 }
802
803 let environment_vars = Self::filter_env_vars();
805
806 Ok(Self {
807 current_dir,
808 shell_type,
809 platform,
810 environment_vars,
811 username: std::env::var("USER")
812 .or_else(|_| std::env::var("USERNAME"))
813 .unwrap_or_else(|_| "unknown".to_string()),
814 hostname: std::env::var("HOSTNAME")
815 .or_else(|_| std::env::var("COMPUTERNAME"))
816 .unwrap_or_else(|_| "unknown".to_string()),
817 captured_at: Utc::now(),
818 })
819 }
820
821 fn filter_env_vars() -> HashMap<String, String> {
823 let sensitive_patterns = [
824 "API_KEY",
825 "TOKEN",
826 "SECRET",
827 "PASSWORD",
828 "PASSWD",
829 "CREDENTIAL",
830 "AUTH",
831 "PRIVATE",
832 "KEY",
833 ];
834
835 std::env::vars()
836 .filter(|(key, value)| {
837 !key.is_empty()
839 && !value.is_empty()
840 && !sensitive_patterns
841 .iter()
842 .any(|pattern| key.to_uppercase().contains(pattern))
843 })
844 .collect()
845 }
846
847 pub fn to_prompt_context(&self) -> String {
849 format!(
850 "Current directory: {}\nShell: {}\nPlatform: {}\nUser: {}@{}",
851 self.current_dir.display(),
852 self.shell_type,
853 self.platform,
854 self.username,
855 self.hostname
856 )
857 }
858
859 pub fn has_env_var(&self, key: &str) -> bool {
861 self.environment_vars.contains_key(key)
862 }
863
864 pub fn get_env_var(&self, key: &str) -> Option<&str> {
866 self.environment_vars.get(key).map(|s| s.as_str())
867 }
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct LogEntry {
873 pub timestamp: DateTime<Utc>,
874 pub level: LogLevel,
875 pub target: String,
876 pub message: String,
877 pub operation_id: Option<String>,
878 pub metadata: HashMap<String, serde_json::Value>,
879 pub duration_ms: Option<u64>,
880}
881
882impl LogEntry {
883 pub fn new(level: LogLevel, target: impl Into<String>, message: impl Into<String>) -> Self {
885 Self {
886 timestamp: Utc::now(),
887 level,
888 target: target.into(),
889 message: message.into(),
890 operation_id: None,
891 metadata: HashMap::new(),
892 duration_ms: None,
893 }
894 }
895
896 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
898 self.metadata.insert(key.into(), value);
899 self
900 }
901
902 pub fn with_operation_id(mut self, id: impl Into<String>) -> Self {
904 self.operation_id = Some(id.into());
905 self
906 }
907
908 pub fn with_duration(mut self, duration_ms: u64) -> Self {
910 self.duration_ms = Some(duration_ms);
911 self
912 }
913}