1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::env;
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SemanticConfig {
12 #[serde(default = "default_enabled")]
14 pub enabled: bool,
15
16 #[serde(default = "default_provider")]
18 pub provider: String,
19
20 #[serde(default)]
22 pub model: Option<String>,
23
24 #[serde(default)]
26 pub auto_execute: bool,
27
28 #[serde(default = "default_agentic_enabled")]
30 pub agentic_enabled: bool,
31
32 #[serde(default = "default_max_iterations")]
34 pub max_iterations: usize,
35
36 #[serde(default = "default_max_tools")]
38 pub max_tools_per_phase: usize,
39
40 #[serde(default = "default_evaluation_enabled")]
42 pub evaluation_enabled: bool,
43
44 #[serde(default = "default_strictness")]
46 pub evaluation_strictness: f32,
47
48 #[serde(default = "default_timeout_seconds")]
50 pub timeout_seconds: u64,
51}
52
53fn default_enabled() -> bool {
54 true
55}
56
57fn default_provider() -> String {
58 "openai".to_string()
59}
60
61fn default_agentic_enabled() -> bool {
62 false }
64
65fn default_max_iterations() -> usize {
66 2
67}
68
69fn default_max_tools() -> usize {
70 5
71}
72
73fn default_evaluation_enabled() -> bool {
74 true
75}
76
77fn default_strictness() -> f32 {
78 0.5
79}
80
81fn default_timeout_seconds() -> u64 {
82 30
83}
84
85impl Default for SemanticConfig {
86 fn default() -> Self {
87 Self {
88 enabled: true,
89 provider: "openai".to_string(),
90 model: None,
91 auto_execute: false,
92 agentic_enabled: false,
93 max_iterations: 2,
94 max_tools_per_phase: 5,
95 evaluation_enabled: true,
96 evaluation_strictness: 0.5,
97 timeout_seconds: 30,
98 }
99 }
100}
101
102fn apply_env_overrides(mut config: SemanticConfig) -> SemanticConfig {
110 if let Ok(provider) = env::var("REFLEX_PROVIDER") {
111 if !provider.is_empty() {
112 log::debug!(
113 "Overriding provider from REFLEX_PROVIDER env var: {}",
114 provider
115 );
116 config.provider = provider;
117 }
118 }
119
120 if let Ok(model) = env::var("REFLEX_MODEL") {
121 if !model.is_empty() {
122 log::debug!("Overriding model from REFLEX_MODEL env var: {}", model);
123 config.model = Some(model);
124 }
125 }
126
127 if let Ok(val) = env::var("REFLEX_LLM_TIMEOUT_SECONDS") {
128 match val.trim().parse::<u64>() {
129 Ok(secs) if secs > 0 => {
130 log::debug!(
131 "Overriding LLM timeout from REFLEX_LLM_TIMEOUT_SECONDS: {}s",
132 secs
133 );
134 config.timeout_seconds = secs;
135 }
136 _ => log::warn!(
137 "REFLEX_LLM_TIMEOUT_SECONDS is invalid (must be a positive integer): {}",
138 val
139 ),
140 }
141 }
142
143 config
144}
145
146pub fn load_config(_cache_dir: &Path) -> Result<SemanticConfig> {
154 let home = match dirs::home_dir() {
156 Some(h) => h,
157 None => {
158 log::debug!("Could not determine home directory, using defaults");
159 return Ok(apply_env_overrides(SemanticConfig::default()));
160 }
161 };
162
163 let config_path = home.join(".reflex").join("config.toml");
164
165 if !config_path.exists() {
166 log::debug!("No ~/.reflex/config.toml found, using default semantic config");
167 return Ok(apply_env_overrides(SemanticConfig::default()));
168 }
169
170 let config_str =
171 std::fs::read_to_string(&config_path).context("Failed to read ~/.reflex/config.toml")?;
172
173 let toml_value: toml::Value =
174 toml::from_str(&config_str).context("Failed to parse ~/.reflex/config.toml")?;
175
176 let known_sections = ["semantic", "credentials", "index", "search", "performance"];
178 if let Some(table) = toml_value.as_table() {
179 for key in table.keys() {
180 if !known_sections.contains(&key.as_str()) {
181 eprintln!(
182 "[warn] ~/.reflex/config.toml: unknown section '[{}]' — ignored",
183 key
184 );
185 }
186 }
187 }
188
189 let known_semantic_keys = ["provider", "model", "auto_execute"];
191 if let Some(toml::Value::Table(sem_table)) = toml_value.get("semantic") {
192 for key in sem_table.keys() {
193 if !known_semantic_keys.contains(&key.as_str()) {
194 eprintln!(
195 "[warn] ~/.reflex/config.toml: unknown key '[semantic].{}' — ignored",
196 key
197 );
198 }
199 }
200 }
201
202 if let Some(semantic_table) = toml_value.get("semantic") {
204 let config: SemanticConfig = semantic_table
205 .clone()
206 .try_into()
207 .context("Failed to parse [semantic] section in ~/.reflex/config.toml")?;
208 log::debug!(
209 "Loaded semantic config from ~/.reflex/config.toml: provider={}",
210 config.provider
211 );
212 Ok(apply_env_overrides(config))
213 } else {
214 log::debug!("No [semantic] section in ~/.reflex/config.toml, using defaults");
215 Ok(apply_env_overrides(SemanticConfig::default()))
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221struct UserConfig {
222 #[serde(default)]
223 credentials: Option<Credentials>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227struct Credentials {
228 #[serde(default)]
229 openai_api_key: Option<String>,
230 #[serde(default)]
231 anthropic_api_key: Option<String>,
232 #[serde(default)]
233 openrouter_api_key: Option<String>,
234 #[serde(default)]
235 openai_compatible_api_key: Option<String>,
236 #[serde(default)]
237 openai_model: Option<String>,
238 #[serde(default)]
239 anthropic_model: Option<String>,
240 #[serde(default)]
241 openrouter_model: Option<String>,
242 #[serde(default)]
243 openai_compatible_model: Option<String>,
244 #[serde(default)]
245 openrouter_sort: Option<String>,
246 #[serde(default)]
247 openai_compatible_base_url: Option<String>,
248}
249
250fn load_user_config() -> Result<Option<UserConfig>> {
252 let home = match dirs::home_dir() {
253 Some(h) => h,
254 None => {
255 log::debug!("Could not determine home directory");
256 return Ok(None);
257 }
258 };
259
260 let config_path = home.join(".reflex").join("config.toml");
261
262 if !config_path.exists() {
263 log::debug!("No user config found at ~/.reflex/config.toml");
264 return Ok(None);
265 }
266
267 let config_str =
268 std::fs::read_to_string(&config_path).context("Failed to read ~/.reflex/config.toml")?;
269
270 let config: UserConfig =
271 toml::from_str(&config_str).context("Failed to parse ~/.reflex/config.toml")?;
272
273 Ok(Some(config))
274}
275
276pub fn get_api_key(provider: &str) -> Result<String> {
284 let provider_lc = provider.to_lowercase();
285 let is_openai_compatible =
286 provider_lc == "openai-compatible" || provider_lc == "openai_compatible";
287
288 if let Ok(Some(user_config)) = load_user_config() {
290 if let Some(credentials) = &user_config.credentials {
291 let key = match provider_lc.as_str() {
293 "openai" => credentials.openai_api_key.as_ref(),
294 "anthropic" => credentials.anthropic_api_key.as_ref(),
295 "openrouter" => credentials.openrouter_api_key.as_ref(),
296 "openai-compatible" | "openai_compatible" => {
297 credentials.openai_compatible_api_key.as_ref()
298 }
299 _ => None,
300 };
301
302 if let Some(api_key) = key {
303 log::debug!("Using {} API key from ~/.reflex/config.toml", provider);
304 return Ok(api_key.clone());
305 }
306 }
307 }
308
309 if let Ok(key) = env::var("REFLEX_AI_API_KEY") {
311 if !key.is_empty() {
312 log::debug!(
313 "Using API key from REFLEX_AI_API_KEY env var for provider '{}'",
314 provider
315 );
316 return Ok(key);
317 }
318 }
319
320 let env_var = match provider_lc.as_str() {
322 "openai" => "OPENAI_API_KEY",
323 "anthropic" => "ANTHROPIC_API_KEY",
324 "openrouter" => "OPENROUTER_API_KEY",
325 "openai-compatible" | "openai_compatible" => "OPENAI_COMPATIBLE_API_KEY",
326 _ => anyhow::bail!("Unknown provider: {}", provider),
327 };
328
329 if let Ok(key) = env::var(env_var) {
330 return Ok(key);
331 }
332
333 if is_openai_compatible {
337 log::debug!(
338 "No API key configured for openai-compatible; sending requests without auth header"
339 );
340 return Ok(String::new());
341 }
342
343 Err(anyhow::anyhow!(
344 "API key not found for provider '{}'.\n\
345 \n\
346 Either:\n\
347 1. Run 'rfx llm config' to set up your API key interactively\n\
348 2. Set REFLEX_AI_API_KEY (works with any provider)\n\
349 3. Set the {} environment variable\n\
350 \n\
351 Example: export REFLEX_AI_API_KEY=sk-...",
352 provider,
353 env_var
354 ))
355}
356
357pub fn is_any_api_key_configured() -> bool {
366 if let Ok(Some(user_config)) = load_user_config() {
368 if let Some(credentials) = &user_config.credentials {
369 if credentials.openai_api_key.is_some()
371 || credentials.anthropic_api_key.is_some()
372 || credentials.openrouter_api_key.is_some()
373 || credentials.openai_compatible_api_key.is_some()
374 || credentials.openai_compatible_base_url.is_some()
377 {
378 log::debug!("Found provider credential in ~/.reflex/config.toml");
379 return true;
380 }
381 }
382 }
383
384 if let Ok(key) = env::var("REFLEX_AI_API_KEY") {
386 if !key.is_empty() {
387 log::debug!("Found REFLEX_AI_API_KEY env var");
388 return true;
389 }
390 }
391
392 let env_vars = [
394 "OPENAI_API_KEY",
395 "ANTHROPIC_API_KEY",
396 "OPENROUTER_API_KEY",
397 "OPENAI_COMPATIBLE_API_KEY",
398 "OPENAI_COMPATIBLE_BASE_URL",
399 ];
400
401 for env_var in &env_vars {
402 if env::var(env_var).is_ok() {
403 log::debug!("Found {} environment variable", env_var);
404 return true;
405 }
406 }
407
408 log::debug!("No provider credentials found in config or environment variables");
409 false
410}
411
412pub fn get_user_model(provider: &str) -> Option<String> {
417 if let Ok(Some(user_config)) = load_user_config() {
418 if let Some(credentials) = &user_config.credentials {
419 let model = match provider.to_lowercase().as_str() {
420 "openai" => credentials.openai_model.as_ref(),
421 "anthropic" => credentials.anthropic_model.as_ref(),
422 "openrouter" => credentials.openrouter_model.as_ref(),
423 "openai-compatible" | "openai_compatible" => {
424 credentials.openai_compatible_model.as_ref()
425 }
426 _ => None,
427 };
428
429 if let Some(model_name) = model {
430 log::debug!(
431 "Using {} model from ~/.reflex/config.toml: {}",
432 provider,
433 model_name
434 );
435 return Some(model_name.clone());
436 }
437 }
438 }
439
440 let provider_lc = provider.to_lowercase();
442 if provider_lc == "openai-compatible" || provider_lc == "openai_compatible" {
443 if let Ok(model) = env::var("OPENAI_COMPATIBLE_MODEL") {
444 if !model.is_empty() {
445 log::debug!(
446 "Using openai-compatible model from OPENAI_COMPATIBLE_MODEL env var: {}",
447 model
448 );
449 return Some(model);
450 }
451 }
452 }
453
454 None
455}
456
457pub fn resolve_model(config: &SemanticConfig, override_model: Option<&str>) -> Option<String> {
471 resolve_model_for(&config.provider, config.model.as_deref(), override_model)
472}
473
474pub fn resolve_model_for(
480 provider: &str,
481 project_model: Option<&str>,
482 override_model: Option<&str>,
483) -> Option<String> {
484 override_model
485 .map(String::from)
486 .or_else(|| project_model.map(String::from))
487 .or_else(|| get_user_model(provider))
488}
489
490pub fn save_user_provider(provider: &str, model: Option<&str>) -> Result<()> {
495 let home = dirs::home_dir().context("Cannot find home directory")?;
496 let config_dir = home.join(".reflex");
497 let config_path = config_dir.join("config.toml");
498
499 std::fs::create_dir_all(&config_dir).context("Failed to create ~/.reflex directory")?;
501
502 let mut config: toml::Value = if config_path.exists() {
504 let content = std::fs::read_to_string(&config_path)
505 .context("Failed to read ~/.reflex/config.toml")?;
506 toml::from_str(&content).context("Failed to parse ~/.reflex/config.toml")?
507 } else {
508 toml::Value::Table(toml::map::Map::new())
509 };
510
511 let credentials = config
513 .as_table_mut()
514 .context("Config root is not a table")?
515 .entry("credentials")
516 .or_insert(toml::Value::Table(toml::map::Map::new()))
517 .as_table_mut()
518 .context("[credentials] is not a table")?;
519
520 if let Some(m) = model {
522 let key = format!("{}_model", provider.to_lowercase());
523 credentials.insert(key, toml::Value::String(m.to_string()));
524 log::info!("Saved {} model: {}", provider, m);
525 }
526
527 let toml_str = toml::to_string_pretty(&config).context("Failed to serialize config to TOML")?;
529 std::fs::write(&config_path, toml_str).context("Failed to write ~/.reflex/config.toml")?;
530
531 Ok(())
532}
533
534pub fn get_provider_options(provider: &str) -> Option<HashMap<String, String>> {
539 let provider_lc = provider.to_lowercase();
540
541 match provider_lc.as_str() {
542 "openrouter" => {
543 if let Ok(Some(user_config)) = load_user_config() {
544 if let Some(credentials) = &user_config.credentials {
545 if let Some(sort) = &credentials.openrouter_sort {
546 let mut opts = HashMap::new();
547 opts.insert("sort".to_string(), sort.clone());
548 return Some(opts);
549 }
550 }
551 }
552 None
553 }
554 "openai-compatible" | "openai_compatible" => {
555 let base_url = load_user_config()
557 .ok()
558 .flatten()
559 .and_then(|cfg| cfg.credentials)
560 .and_then(|c| c.openai_compatible_base_url)
561 .or_else(|| env::var("OPENAI_COMPATIBLE_BASE_URL").ok())
562 .filter(|s| !s.is_empty());
563
564 base_url.map(|url| {
565 let mut opts = HashMap::new();
566 opts.insert("base_url".to_string(), url);
567 opts
568 })
569 }
570 _ => None,
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use std::sync::{Mutex, MutexGuard};
578 use tempfile::TempDir;
579
580 static ENV_LOCK: Mutex<()> = Mutex::new(());
588
589 fn env_guard() -> MutexGuard<'static, ()> {
593 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
594 }
595
596 #[test]
597 fn test_default_config() {
598 let config = SemanticConfig::default();
599 assert_eq!(config.enabled, true);
600 assert_eq!(config.provider, "openai");
601 assert_eq!(config.model, None);
602 assert_eq!(config.auto_execute, false);
603 }
604
605 #[test]
606 fn test_load_config_no_file() {
607 let _g = env_guard();
608 let temp = TempDir::new().unwrap();
609
610 unsafe {
612 env::set_var("HOME", temp.path());
613 }
614 let config = load_config(temp.path()).unwrap();
615 unsafe {
616 env::remove_var("HOME");
617 }
618
619 assert_eq!(config.provider, "openai");
621 assert_eq!(config.enabled, true);
622 }
623
624 #[test]
625 fn test_load_config_with_semantic_section() {
626 let _g = env_guard();
627 let temp = TempDir::new().unwrap();
628 let reflex_dir = temp.path().join(".reflex");
629 std::fs::create_dir_all(&reflex_dir).unwrap();
630 let config_path = reflex_dir.join("config.toml");
631
632 std::fs::write(
633 &config_path,
634 r#"
635[semantic]
636enabled = true
637provider = "anthropic"
638model = "claude-3-5-sonnet-20241022"
639auto_execute = true
640 "#,
641 )
642 .unwrap();
643
644 unsafe {
646 env::set_var("HOME", temp.path());
647 }
648 let config = load_config(temp.path()).unwrap();
649 unsafe {
650 env::remove_var("HOME");
651 }
652
653 assert_eq!(config.enabled, true);
654 assert_eq!(config.provider, "anthropic");
655 assert_eq!(config.model, Some("claude-3-5-sonnet-20241022".to_string()));
656 assert_eq!(config.auto_execute, true);
657 }
658
659 #[test]
660 fn test_load_config_without_semantic_section() {
661 let _g = env_guard();
662 let temp = TempDir::new().unwrap();
663 let reflex_dir = temp.path().join(".reflex");
664 std::fs::create_dir_all(&reflex_dir).unwrap();
665 let config_path = reflex_dir.join("config.toml");
666
667 std::fs::write(
668 &config_path,
669 r#"
670[index]
671languages = []
672 "#,
673 )
674 .unwrap();
675
676 unsafe {
678 env::set_var("HOME", temp.path());
679 }
680 let config = load_config(temp.path()).unwrap();
681 unsafe {
682 env::remove_var("HOME");
683 }
684
685 assert_eq!(config.provider, "openai");
687 }
688
689 #[test]
690 fn test_get_api_key_env_var() {
691 let _g = env_guard();
692 let temp = TempDir::new().unwrap();
693
694 unsafe {
696 env::set_var("HOME", temp.path());
697 env::set_var("OPENAI_API_KEY", "test-key-123");
698 }
699
700 let key = get_api_key("openai").unwrap();
701 assert_eq!(key, "test-key-123");
702
703 unsafe {
704 env::remove_var("OPENAI_API_KEY");
705 env::remove_var("HOME");
706 }
707 }
708
709 #[test]
710 fn test_get_api_key_missing() {
711 let _g = env_guard();
712 let temp = TempDir::new().unwrap();
713
714 unsafe {
716 env::set_var("HOME", temp.path());
717 env::remove_var("OPENROUTER_API_KEY");
718 env::remove_var("REFLEX_AI_API_KEY");
719 }
720
721 let result = get_api_key("openrouter");
722 assert!(result.is_err());
723 assert!(
724 result
725 .unwrap_err()
726 .to_string()
727 .contains("OPENROUTER_API_KEY")
728 );
729
730 unsafe {
731 env::remove_var("HOME");
732 }
733 }
734
735 #[test]
736 fn test_get_api_key_unknown_provider() {
737 let _g = env_guard();
738 let result = get_api_key("unknown");
739 assert!(result.is_err());
740 assert!(result.unwrap_err().to_string().contains("Unknown provider"));
741 }
742
743 #[test]
744 fn test_env_override_provider() {
745 let _g = env_guard();
746 let temp = TempDir::new().unwrap();
747
748 unsafe {
749 env::set_var("HOME", temp.path());
750 env::set_var("REFLEX_PROVIDER", "openrouter");
751 }
752
753 let config = load_config(temp.path()).unwrap();
754
755 unsafe {
756 env::remove_var("REFLEX_PROVIDER");
757 env::remove_var("HOME");
758 }
759
760 assert_eq!(config.provider, "openrouter");
761 }
762
763 #[test]
764 fn test_env_override_model() {
765 let _g = env_guard();
766 let temp = TempDir::new().unwrap();
767
768 unsafe {
769 env::set_var("HOME", temp.path());
770 env::set_var("REFLEX_MODEL", "google/gemini-2.5-flash");
771 }
772
773 let config = load_config(temp.path()).unwrap();
774
775 unsafe {
776 env::remove_var("REFLEX_MODEL");
777 env::remove_var("HOME");
778 }
779
780 assert_eq!(config.model, Some("google/gemini-2.5-flash".to_string()));
781 assert_eq!(config.provider, "openai");
783 }
784
785 #[test]
786 fn test_get_api_key_generic_env_var() {
787 let _g = env_guard();
788 let temp = TempDir::new().unwrap();
789
790 unsafe {
791 env::set_var("HOME", temp.path());
792 env::remove_var("OPENROUTER_API_KEY");
793 env::set_var("REFLEX_AI_API_KEY", "generic-key-456");
794 }
795
796 let key = get_api_key("openrouter").unwrap();
797 assert_eq!(key, "generic-key-456");
798
799 unsafe {
800 env::remove_var("REFLEX_AI_API_KEY");
801 env::remove_var("HOME");
802 }
803 }
804
805 #[test]
806 fn test_get_api_key_openai_compatible_returns_empty_when_unset() {
807 let _g = env_guard();
808 let temp = TempDir::new().unwrap();
809
810 unsafe {
811 env::set_var("HOME", temp.path());
812 env::remove_var("OPENAI_COMPATIBLE_API_KEY");
813 env::remove_var("REFLEX_AI_API_KEY");
814 }
815
816 let key = get_api_key("openai-compatible").unwrap();
818 assert_eq!(key, "");
819
820 unsafe {
821 env::remove_var("HOME");
822 }
823 }
824
825 #[test]
826 fn test_get_provider_options_openai_compatible_from_config() {
827 let _g = env_guard();
828 let temp = TempDir::new().unwrap();
829 let reflex_dir = temp.path().join(".reflex");
830 std::fs::create_dir_all(&reflex_dir).unwrap();
831 let config_path = reflex_dir.join("config.toml");
832
833 std::fs::write(
834 &config_path,
835 r#"
836[credentials]
837openai_compatible_base_url = "http://localhost:1234/v1"
838openai_compatible_model = "qwen2.5-coder"
839 "#,
840 )
841 .unwrap();
842
843 unsafe {
844 env::set_var("HOME", temp.path());
845 env::remove_var("OPENAI_COMPATIBLE_BASE_URL");
846 }
847
848 let opts = get_provider_options("openai-compatible");
849 let model = get_user_model("openai-compatible");
850
851 unsafe {
852 env::remove_var("HOME");
853 }
854
855 let opts = opts.expect("base_url should be discovered from config");
856 assert_eq!(
857 opts.get("base_url").map(|s| s.as_str()),
858 Some("http://localhost:1234/v1")
859 );
860 assert_eq!(model, Some("qwen2.5-coder".to_string()));
861 }
862
863 #[test]
864 fn test_get_provider_options_openai_compatible_from_env() {
865 let _g = env_guard();
866 let temp = TempDir::new().unwrap();
867
868 unsafe {
869 env::set_var("HOME", temp.path());
870 env::set_var("OPENAI_COMPATIBLE_BASE_URL", "http://localhost:11434/v1");
871 }
872
873 let opts = get_provider_options("openai-compatible");
874
875 unsafe {
876 env::remove_var("OPENAI_COMPATIBLE_BASE_URL");
877 env::remove_var("HOME");
878 }
879
880 let opts = opts.expect("base_url should be discovered from env var");
881 assert_eq!(
882 opts.get("base_url").map(|s| s.as_str()),
883 Some("http://localhost:11434/v1")
884 );
885 }
886
887 fn config_with(provider: &str, project_model: Option<&str>) -> SemanticConfig {
888 SemanticConfig {
889 provider: provider.to_string(),
890 model: project_model.map(String::from),
891 ..SemanticConfig::default()
892 }
893 }
894
895 #[test]
896 fn resolve_model_prefers_override() {
897 let config = config_with("openai", Some("gpt-4o"));
898 let resolved = resolve_model(&config, Some("gpt-4o-2024-08-06"));
899 assert_eq!(resolved.as_deref(), Some("gpt-4o-2024-08-06"));
900 }
901
902 #[test]
903 fn resolve_model_falls_back_to_project_config() {
904 let config = config_with("openai", Some("gpt-4o"));
905 let resolved = resolve_model(&config, None);
906 assert_eq!(resolved.as_deref(), Some("gpt-4o"));
907 }
908
909 #[test]
910 fn resolve_model_returns_none_when_unset() {
911 let _g = env_guard();
912 let temp = TempDir::new().unwrap();
915 unsafe {
916 env::set_var("HOME", temp.path());
917 }
918
919 let config = config_with("openai", None);
920 let resolved = resolve_model(&config, None);
921
922 unsafe {
923 env::remove_var("HOME");
924 }
925
926 assert_eq!(resolved, None);
927 }
928
929 #[test]
930 fn resolve_model_for_openai_compatible_reads_user_config() {
931 let _g = env_guard();
932 let temp = TempDir::new().unwrap();
936 let reflex_dir = temp.path().join(".reflex");
937 std::fs::create_dir_all(&reflex_dir).unwrap();
938 std::fs::write(
939 reflex_dir.join("config.toml"),
940 r#"
941[credentials]
942openai_compatible_model = "gpt-oss:20b-cloud"
943 "#,
944 )
945 .unwrap();
946
947 unsafe {
948 env::set_var("HOME", temp.path());
949 }
950
951 let resolved = resolve_model_for("openai-compatible", None, None);
952
953 unsafe {
954 env::remove_var("HOME");
955 }
956
957 assert_eq!(resolved.as_deref(), Some("gpt-oss:20b-cloud"));
958 }
959
960 #[test]
961 fn resolve_model_for_override_beats_user_config() {
962 let _g = env_guard();
963 let temp = TempDir::new().unwrap();
964 let reflex_dir = temp.path().join(".reflex");
965 std::fs::create_dir_all(&reflex_dir).unwrap();
966 std::fs::write(
967 reflex_dir.join("config.toml"),
968 r#"
969[credentials]
970openrouter_model = "anthropic/claude-opus-4"
971 "#,
972 )
973 .unwrap();
974
975 unsafe {
976 env::set_var("HOME", temp.path());
977 }
978
979 let resolved = resolve_model_for("openrouter", None, Some("openai/gpt-4o"));
980
981 unsafe {
982 env::remove_var("HOME");
983 }
984
985 assert_eq!(resolved.as_deref(), Some("openai/gpt-4o"));
986 }
987}