Skip to main content

nexus_memory_core/
config.rs

1//! Configuration types for Nexus Memory System
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// LLM provider configuration for agent operations
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LlmConfig {
9    /// LLM provider name (openai, anthropic, gemini, openrouter, groq, zai, minimax, mistral)
10    pub provider: String,
11    /// Model name (e.g., "gpt-4o-mini", "claude-sonnet-4-20250514")
12    pub model: String,
13    /// API key environment variable name (e.g., "OPENAI_API_KEY")
14    pub api_key_env: String,
15    /// Base URL override (optional)
16    pub base_url: Option<String>,
17    /// Request timeout in seconds
18    pub timeout_secs: u64,
19    /// Maximum tokens to generate
20    pub max_tokens: u32,
21    /// Temperature for generation (0.0 to 1.0)
22    pub temperature: f32,
23}
24
25impl Default for LlmConfig {
26    fn default() -> Self {
27        Self {
28            provider: "openai".to_string(),
29            model: "gpt-4o-mini".to_string(),
30            api_key_env: "OPENAI_API_KEY".to_string(),
31            base_url: None,
32            timeout_secs: 60,
33            max_tokens: 4096,
34            temperature: 0.3,
35        }
36    }
37}
38
39/// Always-on agent configuration
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct AgentConfig {
42    /// Whether the always-on agent is enabled
43    pub enabled: bool,
44    /// Namespace name for agent-generated memories
45    pub namespace: String,
46    /// Directory to watch for new files
47    pub inbox_dir: String,
48    /// File scan interval in seconds
49    pub scan_interval_secs: u64,
50    /// Consolidation interval in minutes
51    pub consolidation_interval_mins: u64,
52    /// Maximum memories to consolidate per run
53    pub consolidation_batch_size: usize,
54    /// Maximum memories to include in query context
55    pub query_context_limit: usize,
56}
57
58impl Default for AgentConfig {
59    fn default() -> Self {
60        Self {
61            enabled: false,
62            namespace: "nexus-agent".to_string(),
63            inbox_dir: "./inbox".to_string(),
64            scan_interval_secs: 5,
65            consolidation_interval_mins: 30,
66            consolidation_batch_size: 10,
67            query_context_limit: 50,
68        }
69    }
70}
71
72/// Cognition and runtime orchestration configuration
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CognitionConfig {
75    /// Enable session-scoped runtime orchestration for hook-driven agent sessions
76    pub auto_runtime_enabled: bool,
77    /// Enable derivation of explicit observations from raw memories
78    pub derive_enabled: bool,
79    /// Enable session digest generation
80    pub digest_enabled: bool,
81    /// Enable reflective/dream processing
82    pub reflect_enabled: bool,
83    /// Enable low-signal activity distillation into higher-level summaries
84    pub activity_distill_enabled: bool,
85    /// Whether to trigger a bounded dream pass when a session ends
86    pub dream_on_session_end: bool,
87    /// Whether compact/checkpoint events should trigger a bounded flush
88    pub checkpoint_flush_enabled: bool,
89    /// Idle timeout, in seconds, for session-scoped runtime state
90    pub runtime_idle_timeout_secs: u64,
91    /// Maximum number of cognition jobs to claim in one worker batch
92    pub max_job_batch: usize,
93    /// Lease TTL, in seconds, for claimed cognition jobs
94    pub lease_ttl_secs: u64,
95    /// Maximum memories to include when building a working representation
96    pub representation_max_items: usize,
97    /// Target token budget for short digests
98    pub digest_short_target_tokens: usize,
99    /// Target token budget for long digests
100    pub digest_long_target_tokens: usize,
101    /// Timeout, in seconds, for direct hook-enrichment attempts
102    pub direct_enrichment_timeout_secs: u64,
103    /// Minimum number of events required before activity distillation runs
104    pub activity_distill_min_events: usize,
105    /// Maximum number of events to include in one activity distillation batch
106    pub activity_distill_max_events: usize,
107    /// Whether raw memories should be included by default in retrieval surfaces
108    pub include_raw_by_default: bool,
109    /// Timeout, in seconds, for best-effort dream work during session shutdown
110    pub session_end_dream_timeout_secs: u64,
111    /// Maximum retry-buffer artifacts to process during a bounded flush
112    pub retry_buffer_drain_limit: usize,
113    /// Enable belief revision when contradictions are detected (reduce confidence on sources)
114    pub contradiction_belief_revision_enabled: bool,
115    /// How much to reduce confidence on each contradicted memory (0.0–1.0)
116    pub contradiction_confidence_penalty: f32,
117    /// Enable memory aging decay in ranking blend
118    pub memory_decay_enabled: bool,
119    /// Days before a memory starts decaying in ranking score
120    pub memory_decay_age_days: u64,
121    /// Days that a recent access resets the decay clock
122    pub memory_decay_access_boost_days: u64,
123    /// Enable adaptive dream interval in the periodic supervisor loop
124    pub adaptive_dream_enabled: bool,
125    /// Floor (minimum) for adaptive dream interval in seconds
126    pub adaptive_dream_min_interval_secs: u64,
127    /// Ceiling (maximum) for adaptive dream interval in seconds
128    pub adaptive_dream_max_interval_secs: u64,
129}
130
131impl Default for CognitionConfig {
132    fn default() -> Self {
133        Self {
134            auto_runtime_enabled: true,
135            derive_enabled: true,
136            digest_enabled: true,
137            reflect_enabled: true,
138            activity_distill_enabled: true,
139            dream_on_session_end: true,
140            checkpoint_flush_enabled: true,
141            runtime_idle_timeout_secs: 900,
142            max_job_batch: 8,
143            lease_ttl_secs: 120,
144            representation_max_items: 24,
145            digest_short_target_tokens: 600,
146            digest_long_target_tokens: 1800,
147            direct_enrichment_timeout_secs: 8,
148            activity_distill_min_events: 8,
149            activity_distill_max_events: 60,
150            include_raw_by_default: false,
151            session_end_dream_timeout_secs: 8,
152            retry_buffer_drain_limit: 8,
153            contradiction_belief_revision_enabled: true,
154            contradiction_confidence_penalty: 0.15,
155            memory_decay_enabled: true,
156            memory_decay_age_days: 90,
157            memory_decay_access_boost_days: 30,
158            adaptive_dream_enabled: true,
159            adaptive_dream_min_interval_secs: 60,
160            adaptive_dream_max_interval_secs: 1800,
161        }
162    }
163}
164
165/// Main configuration for Nexus
166#[derive(Debug, Clone, Serialize, Deserialize, Default)]
167pub struct Config {
168    /// Database configuration
169    pub database: DatabaseConfig,
170
171    /// Server configuration
172    pub server: ServerConfig,
173
174    /// Embedding configuration
175    pub embedding: EmbeddingConfig,
176
177    /// Sync configuration
178    pub sync: SyncConfig,
179
180    /// LLM configuration
181    pub llm: LlmConfig,
182
183    /// Agent configuration
184    pub agent: AgentConfig,
185
186    /// Cognition/runtime configuration
187    pub cognition: CognitionConfig,
188}
189
190impl Config {
191    /// Load configuration from environment variables
192    pub fn from_env() -> crate::Result<Self> {
193        let mut config = Self::default();
194
195        if let Ok(path) = std::env::var("NEXUS_DATABASE_PATH") {
196            config.database.path = PathBuf::from(path);
197        }
198
199        if let Ok(host) = std::env::var("NEXUS_HOST") {
200            config.server.host = host;
201        }
202
203        if let Ok(port) = std::env::var("NEXUS_PORT") {
204            config.server.port = port.parse().unwrap_or(8768);
205        }
206
207        if let Ok(enabled) = std::env::var("NEXUS_EMBEDDINGS_ENABLED") {
208            config.embedding.enabled = enabled.parse().unwrap_or(false);
209        }
210
211        if let Ok(backend) = std::env::var("NEXUS_EMBEDDING_BACKEND") {
212            config.embedding.backend = backend;
213        }
214
215        if let Ok(provider) = std::env::var("NEXUS_EMBEDDING_PROVIDER") {
216            config.embedding.provider = provider;
217        }
218
219        if let Ok(model) = std::env::var("NEXUS_EMBEDDING_MODEL") {
220            config.embedding.model = model;
221        }
222
223        if let Ok(key_env) = std::env::var("NEXUS_EMBEDDING_API_KEY_ENV") {
224            config.embedding.api_key_env = key_env;
225        }
226
227        if let Ok(base_url) = std::env::var("NEXUS_EMBEDDING_BASE_URL") {
228            config.embedding.base_url = Some(base_url);
229        }
230
231        if let Ok(dimension) = std::env::var("NEXUS_EMBEDDING_DIMENSION") {
232            config.embedding.dimension = dimension
233                .parse()
234                .unwrap_or(EmbeddingConfig::default().dimension);
235        }
236
237        if let Ok(timeout) = std::env::var("NEXUS_EMBEDDING_TIMEOUT_SECS") {
238            config.embedding.timeout_secs = timeout
239                .parse()
240                .unwrap_or(EmbeddingConfig::default().timeout_secs);
241        }
242
243        if let Ok(model_path) = std::env::var("NEXUS_EMBEDDING_MODEL_PATH") {
244            config.embedding.local_model_path = Some(model_path);
245        }
246
247        if let Ok(tokenizer_path) = std::env::var("NEXUS_TOKENIZER_PATH") {
248            config.embedding.local_tokenizer_path = Some(tokenizer_path);
249        }
250
251        if let Ok(policy) = std::env::var("NEXUS_SYNC_POLICY") {
252            config.sync.policy = policy;
253        }
254
255        // LLM configuration
256        if let Ok(provider) = std::env::var("NEXUS_LLM_PROVIDER") {
257            config.llm.provider = provider;
258        }
259        if let Ok(model) = std::env::var("NEXUS_LLM_MODEL") {
260            config.llm.model = model;
261        }
262        if let Ok(key_env) = std::env::var("NEXUS_LLM_API_KEY_ENV") {
263            config.llm.api_key_env = key_env;
264        }
265        if let Ok(base_url) = std::env::var("NEXUS_LLM_BASE_URL") {
266            config.llm.base_url = Some(base_url);
267        }
268
269        // Agent configuration
270        if let Ok(enabled) = std::env::var("NEXUS_AGENT_ENABLED") {
271            config.agent.enabled = enabled.parse().unwrap_or(false);
272        }
273        if let Ok(namespace) = std::env::var("NEXUS_AGENT_NAMESPACE") {
274            config.agent.namespace = namespace;
275        }
276        if let Ok(inbox) = std::env::var("NEXUS_AGENT_INBOX_DIR") {
277            config.agent.inbox_dir = inbox;
278        }
279        if let Ok(interval) = std::env::var("NEXUS_AGENT_CONSOLIDATION_INTERVAL_MINS") {
280            config.agent.consolidation_interval_mins = interval
281                .parse()
282                .unwrap_or(AgentConfig::default().consolidation_interval_mins);
283        } else if let Ok(interval) = std::env::var("NEXUS_AGENT_CONSOLIDATION_INTERVAL") {
284            // Backward compat: old name without unit suffix
285            config.agent.consolidation_interval_mins = interval
286                .parse()
287                .unwrap_or(AgentConfig::default().consolidation_interval_mins);
288        }
289        if let Ok(interval) = std::env::var("NEXUS_AGENT_SCAN_INTERVAL_SECS") {
290            config.agent.scan_interval_secs = interval
291                .parse()
292                .unwrap_or(AgentConfig::default().scan_interval_secs);
293        } else if let Ok(interval) = std::env::var("NEXUS_AGENT_SCAN_INTERVAL") {
294            // Backward compat: old name without unit suffix
295            config.agent.scan_interval_secs = interval
296                .parse()
297                .unwrap_or(AgentConfig::default().scan_interval_secs);
298        }
299
300        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_AUTO_RUNTIME_ENABLED") {
301            config.cognition.auto_runtime_enabled = enabled.parse().unwrap_or(true);
302        }
303        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DERIVE_ENABLED") {
304            config.cognition.derive_enabled = enabled.parse().unwrap_or(true);
305        }
306        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DIGEST_ENABLED") {
307            config.cognition.digest_enabled = enabled.parse().unwrap_or(true);
308        }
309        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_REFLECT_ENABLED") {
310            config.cognition.reflect_enabled = enabled.parse().unwrap_or(true);
311        }
312        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_ENABLED") {
313            config.cognition.activity_distill_enabled = enabled.parse().unwrap_or(true);
314        }
315        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_DREAM_ON_SESSION_END") {
316            config.cognition.dream_on_session_end = enabled.parse().unwrap_or(true);
317        }
318        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_CHECKPOINT_FLUSH_ENABLED") {
319            config.cognition.checkpoint_flush_enabled = enabled.parse().unwrap_or(true);
320        }
321        if let Ok(timeout) = std::env::var("NEXUS_COGNITION_RUNTIME_IDLE_TIMEOUT_SECS") {
322            config.cognition.runtime_idle_timeout_secs = timeout
323                .parse()
324                .unwrap_or(CognitionConfig::default().runtime_idle_timeout_secs);
325        }
326        if let Ok(batch) = std::env::var("NEXUS_COGNITION_MAX_JOB_BATCH") {
327            config.cognition.max_job_batch = batch
328                .parse()
329                .unwrap_or(CognitionConfig::default().max_job_batch);
330        }
331        if let Ok(ttl) = std::env::var("NEXUS_COGNITION_LEASE_TTL_SECS") {
332            config.cognition.lease_ttl_secs = ttl
333                .parse()
334                .unwrap_or(CognitionConfig::default().lease_ttl_secs);
335        }
336        if let Ok(items) = std::env::var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS") {
337            config.cognition.representation_max_items = items
338                .parse()
339                .unwrap_or(CognitionConfig::default().representation_max_items);
340        }
341        if let Ok(tokens) = std::env::var("NEXUS_COGNITION_DIGEST_SHORT_TARGET_TOKENS") {
342            config.cognition.digest_short_target_tokens = tokens
343                .parse()
344                .unwrap_or(CognitionConfig::default().digest_short_target_tokens);
345        }
346        if let Ok(tokens) = std::env::var("NEXUS_COGNITION_DIGEST_LONG_TARGET_TOKENS") {
347            config.cognition.digest_long_target_tokens = tokens
348                .parse()
349                .unwrap_or(CognitionConfig::default().digest_long_target_tokens);
350        }
351        if let Ok(timeout) = std::env::var("NEXUS_COGNITION_DIRECT_ENRICHMENT_TIMEOUT_SECS") {
352            config.cognition.direct_enrichment_timeout_secs = timeout
353                .parse()
354                .unwrap_or(CognitionConfig::default().direct_enrichment_timeout_secs);
355        }
356        if let Ok(events) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_MIN_EVENTS") {
357            config.cognition.activity_distill_min_events = events
358                .parse()
359                .unwrap_or(CognitionConfig::default().activity_distill_min_events);
360        }
361        if let Ok(events) = std::env::var("NEXUS_COGNITION_ACTIVITY_DISTILL_MAX_EVENTS") {
362            config.cognition.activity_distill_max_events = events
363                .parse()
364                .unwrap_or(CognitionConfig::default().activity_distill_max_events);
365        }
366        if let Ok(include_raw) = std::env::var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT") {
367            config.cognition.include_raw_by_default = include_raw.parse().unwrap_or(false);
368        }
369        if let Ok(timeout) = std::env::var("NEXUS_COGNITION_SESSION_END_DREAM_TIMEOUT_SECS") {
370            config.cognition.session_end_dream_timeout_secs = timeout
371                .parse()
372                .unwrap_or(CognitionConfig::default().session_end_dream_timeout_secs);
373        }
374        if let Ok(limit) = std::env::var("NEXUS_COGNITION_RETRY_BUFFER_DRAIN_LIMIT") {
375            config.cognition.retry_buffer_drain_limit = limit
376                .parse()
377                .unwrap_or(CognitionConfig::default().retry_buffer_drain_limit);
378        }
379        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_CONTRADICTION_BELIEF_REVISION_ENABLED")
380        {
381            config.cognition.contradiction_belief_revision_enabled =
382                enabled.parse().unwrap_or(true);
383        }
384        if let Ok(penalty) = std::env::var("NEXUS_COGNITION_CONTRADICTION_CONFIDENCE_PENALTY") {
385            config.cognition.contradiction_confidence_penalty = penalty
386                .parse()
387                .unwrap_or(CognitionConfig::default().contradiction_confidence_penalty);
388        }
389        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_ENABLED") {
390            config.cognition.memory_decay_enabled = enabled.parse().unwrap_or(true);
391        }
392        if let Ok(days) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_AGE_DAYS") {
393            config.cognition.memory_decay_age_days = days
394                .parse()
395                .unwrap_or(CognitionConfig::default().memory_decay_age_days);
396        }
397        if let Ok(days) = std::env::var("NEXUS_COGNITION_MEMORY_DECAY_ACCESS_BOOST_DAYS") {
398            config.cognition.memory_decay_access_boost_days = days
399                .parse()
400                .unwrap_or(CognitionConfig::default().memory_decay_access_boost_days);
401        }
402        if let Ok(enabled) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_ENABLED") {
403            config.cognition.adaptive_dream_enabled = enabled.parse().unwrap_or(true);
404        }
405        if let Ok(secs) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_MIN_INTERVAL_SECS") {
406            config.cognition.adaptive_dream_min_interval_secs = secs
407                .parse()
408                .unwrap_or(CognitionConfig::default().adaptive_dream_min_interval_secs);
409        }
410        if let Ok(secs) = std::env::var("NEXUS_COGNITION_ADAPTIVE_DREAM_MAX_INTERVAL_SECS") {
411            config.cognition.adaptive_dream_max_interval_secs = secs
412                .parse()
413                .unwrap_or(CognitionConfig::default().adaptive_dream_max_interval_secs);
414        }
415
416        Ok(config)
417    }
418
419    /// Get the database URL
420    pub fn database_url(&self) -> String {
421        format!("sqlite:{}", self.database.path.display())
422    }
423}
424
425/// Database configuration
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct DatabaseConfig {
428    /// Path to SQLite database file
429    pub path: PathBuf,
430
431    /// Enable foreign key constraints
432    pub foreign_keys: bool,
433
434    /// Connection pool size
435    pub pool_size: u32,
436}
437
438impl Default for DatabaseConfig {
439    fn default() -> Self {
440        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
441        let base_path = PathBuf::from(home).join(".nexus");
442
443        Self {
444            path: base_path.join("nexus.db"),
445            foreign_keys: true,
446            pool_size: 5,
447        }
448    }
449}
450
451/// Server configuration
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct ServerConfig {
454    /// Server host
455    pub host: String,
456
457    /// Server port
458    pub port: u16,
459
460    /// Web dashboard port
461    pub web_port: u16,
462
463    /// Transport type (stdio, web)
464    pub transport: String,
465}
466
467impl Default for ServerConfig {
468    fn default() -> Self {
469        Self {
470            host: "127.0.0.1".to_string(),
471            port: 8768,
472            web_port: 8768,
473            transport: "stdio".to_string(),
474        }
475    }
476}
477
478/// Embedding configuration
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct EmbeddingConfig {
481    /// Enable embeddings
482    pub enabled: bool,
483
484    /// Embedding backend (`local` or `openai-compatible`)
485    pub backend: String,
486
487    /// Embedding provider/profile (`inherit`, `openai`, `gemini`, `openrouter`,
488    /// `vllm`, `lmstudio`, `llamacpp`, `custom`, etc.)
489    pub provider: String,
490
491    /// Embedding model name
492    pub model: String,
493
494    /// API key environment variable name for remote embedding providers
495    pub api_key_env: String,
496
497    /// Base URL override for remote or local OpenAI-compatible runtimes
498    pub base_url: Option<String>,
499
500    /// Embedding dimension
501    pub dimension: usize,
502
503    /// Request timeout for remote embedding providers
504    pub timeout_secs: u64,
505
506    /// Path to a local ONNX embedding model
507    pub local_model_path: Option<String>,
508
509    /// Path to the tokenizer directory for local ONNX embeddings
510    pub local_tokenizer_path: Option<String>,
511}
512
513impl Default for EmbeddingConfig {
514    fn default() -> Self {
515        Self {
516            enabled: false,
517            backend: "local".to_string(),
518            provider: "local".to_string(),
519            model: "all-MiniLM-L6-v2".to_string(),
520            api_key_env: "OPENAI_API_KEY".to_string(),
521            base_url: None,
522            dimension: 384,
523            timeout_secs: 60,
524            local_model_path: Some("models/all-MiniLM-L6-v2.onnx".to_string()),
525            local_tokenizer_path: Some("models/all-MiniLM-L6-v2-tokenizer".to_string()),
526        }
527    }
528}
529
530/// Sync configuration for cross-agent synchronization
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct SyncConfig {
533    /// Sync policy (manual, auto, aggressive)
534    pub policy: String,
535
536    /// Sync interval in seconds (for auto policy)
537    pub interval_secs: u64,
538}
539
540impl Default for SyncConfig {
541    fn default() -> Self {
542        Self {
543            policy: "manual".to_string(),
544            interval_secs: 300,
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use serial_test::serial;
553
554    #[test]
555    fn test_default_config() {
556        let config = Config::default();
557        assert!(!config.embedding.enabled);
558        assert_eq!(config.embedding.backend, "local");
559        assert_eq!(config.embedding.provider, "local");
560        assert_eq!(config.embedding.dimension, 384);
561        assert_eq!(config.server.port, 8768);
562    }
563
564    #[test]
565    fn test_database_url() {
566        let config = Config::default();
567        let url = config.database_url();
568        assert!(url.starts_with("sqlite:"));
569    }
570
571    #[test]
572    fn test_cognition_config_defaults() {
573        let config = Config::default();
574        assert!(config.cognition.derive_enabled);
575        assert!(config.cognition.digest_enabled);
576        assert!(config.cognition.reflect_enabled);
577        assert!(config.cognition.activity_distill_enabled);
578        assert_eq!(config.cognition.representation_max_items, 24);
579        assert!(!config.cognition.include_raw_by_default);
580        assert!(config.cognition.contradiction_belief_revision_enabled);
581        assert!((config.cognition.contradiction_confidence_penalty - 0.15).abs() < f32::EPSILON);
582        assert!(config.cognition.memory_decay_enabled);
583        assert_eq!(config.cognition.memory_decay_age_days, 90);
584        assert!(config.cognition.adaptive_dream_enabled);
585        assert_eq!(config.cognition.adaptive_dream_min_interval_secs, 60);
586        assert_eq!(config.cognition.adaptive_dream_max_interval_secs, 1800);
587    }
588
589    #[test]
590    #[serial]
591    fn test_cognition_config_from_env() {
592        std::env::set_var("NEXUS_COGNITION_DERIVE_ENABLED", "false");
593        std::env::set_var("NEXUS_COGNITION_MAX_JOB_BATCH", "16");
594        std::env::set_var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS", "42");
595        std::env::set_var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT", "true");
596
597        let config = Config::from_env().expect("config from env");
598        assert!(!config.cognition.derive_enabled);
599        assert_eq!(config.cognition.max_job_batch, 16);
600        assert_eq!(config.cognition.representation_max_items, 42);
601        assert!(config.cognition.include_raw_by_default);
602
603        std::env::remove_var("NEXUS_COGNITION_DERIVE_ENABLED");
604        std::env::remove_var("NEXUS_COGNITION_MAX_JOB_BATCH");
605        std::env::remove_var("NEXUS_COGNITION_REPRESENTATION_MAX_ITEMS");
606        std::env::remove_var("NEXUS_COGNITION_INCLUDE_RAW_BY_DEFAULT");
607    }
608
609    #[test]
610    #[serial]
611    fn test_embedding_config_from_env() {
612        std::env::set_var("NEXUS_EMBEDDINGS_ENABLED", "true");
613        std::env::set_var("NEXUS_EMBEDDING_BACKEND", "openai-compatible");
614        std::env::set_var("NEXUS_EMBEDDING_PROVIDER", "inherit");
615        std::env::set_var("NEXUS_EMBEDDING_MODEL", "text-embedding-004");
616        std::env::set_var("NEXUS_EMBEDDING_API_KEY_ENV", "GEMINI_API_KEY");
617        std::env::set_var(
618            "NEXUS_EMBEDDING_BASE_URL",
619            "https://generativelanguage.googleapis.com/v1beta/openai",
620        );
621        std::env::set_var("NEXUS_EMBEDDING_TIMEOUT_SECS", "45");
622
623        let config = Config::from_env().expect("config from env");
624        assert!(config.embedding.enabled);
625        assert_eq!(config.embedding.backend, "openai-compatible");
626        assert_eq!(config.embedding.provider, "inherit");
627        assert_eq!(config.embedding.model, "text-embedding-004");
628        assert_eq!(config.embedding.api_key_env, "GEMINI_API_KEY");
629        assert_eq!(
630            config.embedding.base_url.as_deref(),
631            Some("https://generativelanguage.googleapis.com/v1beta/openai")
632        );
633        assert_eq!(config.embedding.timeout_secs, 45);
634
635        std::env::remove_var("NEXUS_EMBEDDINGS_ENABLED");
636        std::env::remove_var("NEXUS_EMBEDDING_BACKEND");
637        std::env::remove_var("NEXUS_EMBEDDING_PROVIDER");
638        std::env::remove_var("NEXUS_EMBEDDING_MODEL");
639        std::env::remove_var("NEXUS_EMBEDDING_API_KEY_ENV");
640        std::env::remove_var("NEXUS_EMBEDDING_BASE_URL");
641        std::env::remove_var("NEXUS_EMBEDDING_TIMEOUT_SECS");
642    }
643}