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