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!("Overriding provider from REFLEX_PROVIDER env var: {}", provider);
113 config.provider = provider;
114 }
115 }
116
117 if let Ok(model) = env::var("REFLEX_MODEL") {
118 if !model.is_empty() {
119 log::debug!("Overriding model from REFLEX_MODEL env var: {}", model);
120 config.model = Some(model);
121 }
122 }
123
124 if let Ok(val) = env::var("REFLEX_LLM_TIMEOUT_SECONDS") {
125 match val.trim().parse::<u64>() {
126 Ok(secs) if secs > 0 => {
127 log::debug!("Overriding LLM timeout from REFLEX_LLM_TIMEOUT_SECONDS: {}s", secs);
128 config.timeout_seconds = secs;
129 }
130 _ => log::warn!("REFLEX_LLM_TIMEOUT_SECONDS is invalid (must be a positive integer): {}", val),
131 }
132 }
133
134 config
135}
136
137pub fn load_config(_cache_dir: &Path) -> Result<SemanticConfig> {
145 let home = match dirs::home_dir() {
147 Some(h) => h,
148 None => {
149 log::debug!("Could not determine home directory, using defaults");
150 return Ok(apply_env_overrides(SemanticConfig::default()));
151 }
152 };
153
154 let config_path = home.join(".reflex").join("config.toml");
155
156 if !config_path.exists() {
157 log::debug!("No ~/.reflex/config.toml found, using default semantic config");
158 return Ok(apply_env_overrides(SemanticConfig::default()));
159 }
160
161 let config_str = std::fs::read_to_string(&config_path)
162 .context("Failed to read ~/.reflex/config.toml")?;
163
164 let toml_value: toml::Value = toml::from_str(&config_str)
165 .context("Failed to parse ~/.reflex/config.toml")?;
166
167 if let Some(semantic_table) = toml_value.get("semantic") {
169 let config: SemanticConfig = semantic_table.clone().try_into()
170 .context("Failed to parse [semantic] section in ~/.reflex/config.toml")?;
171 log::debug!("Loaded semantic config from ~/.reflex/config.toml: provider={}", config.provider);
172 Ok(apply_env_overrides(config))
173 } else {
174 log::debug!("No [semantic] section in ~/.reflex/config.toml, using defaults");
175 Ok(apply_env_overrides(SemanticConfig::default()))
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181struct UserConfig {
182 #[serde(default)]
183 credentials: Option<Credentials>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187struct Credentials {
188 #[serde(default)]
189 openai_api_key: Option<String>,
190 #[serde(default)]
191 anthropic_api_key: Option<String>,
192 #[serde(default)]
193 openrouter_api_key: Option<String>,
194 #[serde(default)]
195 openai_compatible_api_key: Option<String>,
196 #[serde(default)]
197 openai_model: Option<String>,
198 #[serde(default)]
199 anthropic_model: Option<String>,
200 #[serde(default)]
201 openrouter_model: Option<String>,
202 #[serde(default)]
203 openai_compatible_model: Option<String>,
204 #[serde(default)]
205 openrouter_sort: Option<String>,
206 #[serde(default)]
207 openai_compatible_base_url: Option<String>,
208}
209
210fn load_user_config() -> Result<Option<UserConfig>> {
212 let home = match dirs::home_dir() {
213 Some(h) => h,
214 None => {
215 log::debug!("Could not determine home directory");
216 return Ok(None);
217 }
218 };
219
220 let config_path = home.join(".reflex").join("config.toml");
221
222 if !config_path.exists() {
223 log::debug!("No user config found at ~/.reflex/config.toml");
224 return Ok(None);
225 }
226
227 let config_str = std::fs::read_to_string(&config_path)
228 .context("Failed to read ~/.reflex/config.toml")?;
229
230 let config: UserConfig = toml::from_str(&config_str)
231 .context("Failed to parse ~/.reflex/config.toml")?;
232
233 Ok(Some(config))
234}
235
236pub fn get_api_key(provider: &str) -> Result<String> {
244 let provider_lc = provider.to_lowercase();
245 let is_openai_compatible =
246 provider_lc == "openai-compatible" || provider_lc == "openai_compatible";
247
248 if let Ok(Some(user_config)) = load_user_config() {
250 if let Some(credentials) = &user_config.credentials {
251 let key = match provider_lc.as_str() {
253 "openai" => credentials.openai_api_key.as_ref(),
254 "anthropic" => credentials.anthropic_api_key.as_ref(),
255 "openrouter" => credentials.openrouter_api_key.as_ref(),
256 "openai-compatible" | "openai_compatible" => {
257 credentials.openai_compatible_api_key.as_ref()
258 }
259 _ => None,
260 };
261
262 if let Some(api_key) = key {
263 log::debug!("Using {} API key from ~/.reflex/config.toml", provider);
264 return Ok(api_key.clone());
265 }
266 }
267 }
268
269 if let Ok(key) = env::var("REFLEX_AI_API_KEY") {
271 if !key.is_empty() {
272 log::debug!("Using API key from REFLEX_AI_API_KEY env var for provider '{}'", provider);
273 return Ok(key);
274 }
275 }
276
277 let env_var = match provider_lc.as_str() {
279 "openai" => "OPENAI_API_KEY",
280 "anthropic" => "ANTHROPIC_API_KEY",
281 "openrouter" => "OPENROUTER_API_KEY",
282 "openai-compatible" | "openai_compatible" => "OPENAI_COMPATIBLE_API_KEY",
283 _ => anyhow::bail!("Unknown provider: {}", provider),
284 };
285
286 if let Ok(key) = env::var(env_var) {
287 return Ok(key);
288 }
289
290 if is_openai_compatible {
294 log::debug!("No API key configured for openai-compatible; sending requests without auth header");
295 return Ok(String::new());
296 }
297
298 Err(anyhow::anyhow!(
299 "API key not found for provider '{}'.\n\
300 \n\
301 Either:\n\
302 1. Run 'rfx llm config' to set up your API key interactively\n\
303 2. Set REFLEX_AI_API_KEY (works with any provider)\n\
304 3. Set the {} environment variable\n\
305 \n\
306 Example: export REFLEX_AI_API_KEY=sk-...",
307 provider, env_var
308 ))
309}
310
311pub fn is_any_api_key_configured() -> bool {
320 if let Ok(Some(user_config)) = load_user_config() {
322 if let Some(credentials) = &user_config.credentials {
323 if credentials.openai_api_key.is_some()
325 || credentials.anthropic_api_key.is_some()
326 || credentials.openrouter_api_key.is_some()
327 || credentials.openai_compatible_api_key.is_some()
328 || credentials.openai_compatible_base_url.is_some()
331 {
332 log::debug!("Found provider credential in ~/.reflex/config.toml");
333 return true;
334 }
335 }
336 }
337
338 if let Ok(key) = env::var("REFLEX_AI_API_KEY") {
340 if !key.is_empty() {
341 log::debug!("Found REFLEX_AI_API_KEY env var");
342 return true;
343 }
344 }
345
346 let env_vars = [
348 "OPENAI_API_KEY",
349 "ANTHROPIC_API_KEY",
350 "OPENROUTER_API_KEY",
351 "OPENAI_COMPATIBLE_API_KEY",
352 "OPENAI_COMPATIBLE_BASE_URL",
353 ];
354
355 for env_var in &env_vars {
356 if env::var(env_var).is_ok() {
357 log::debug!("Found {} environment variable", env_var);
358 return true;
359 }
360 }
361
362 log::debug!("No provider credentials found in config or environment variables");
363 false
364}
365
366pub fn get_user_model(provider: &str) -> Option<String> {
371 if let Ok(Some(user_config)) = load_user_config() {
372 if let Some(credentials) = &user_config.credentials {
373 let model = match provider.to_lowercase().as_str() {
374 "openai" => credentials.openai_model.as_ref(),
375 "anthropic" => credentials.anthropic_model.as_ref(),
376 "openrouter" => credentials.openrouter_model.as_ref(),
377 "openai-compatible" | "openai_compatible" => {
378 credentials.openai_compatible_model.as_ref()
379 }
380 _ => None,
381 };
382
383 if let Some(model_name) = model {
384 log::debug!("Using {} model from ~/.reflex/config.toml: {}", provider, model_name);
385 return Some(model_name.clone());
386 }
387 }
388 }
389
390 None
391}
392
393pub fn resolve_model(
407 config: &SemanticConfig,
408 override_model: Option<&str>,
409) -> Option<String> {
410 resolve_model_for(&config.provider, config.model.as_deref(), override_model)
411}
412
413pub fn resolve_model_for(
419 provider: &str,
420 project_model: Option<&str>,
421 override_model: Option<&str>,
422) -> Option<String> {
423 override_model
424 .map(String::from)
425 .or_else(|| project_model.map(String::from))
426 .or_else(|| get_user_model(provider))
427}
428
429pub fn save_user_provider(provider: &str, model: Option<&str>) -> Result<()> {
434 let home = dirs::home_dir().context("Cannot find home directory")?;
435 let config_dir = home.join(".reflex");
436 let config_path = config_dir.join("config.toml");
437
438 std::fs::create_dir_all(&config_dir)
440 .context("Failed to create ~/.reflex directory")?;
441
442 let mut config: toml::Value = if config_path.exists() {
444 let content = std::fs::read_to_string(&config_path)
445 .context("Failed to read ~/.reflex/config.toml")?;
446 toml::from_str(&content)
447 .context("Failed to parse ~/.reflex/config.toml")?
448 } else {
449 toml::Value::Table(toml::map::Map::new())
450 };
451
452 let credentials = config
454 .as_table_mut()
455 .context("Config root is not a table")?
456 .entry("credentials")
457 .or_insert(toml::Value::Table(toml::map::Map::new()))
458 .as_table_mut()
459 .context("[credentials] is not a table")?;
460
461 if let Some(m) = model {
463 let key = format!("{}_model", provider.to_lowercase());
464 credentials.insert(key, toml::Value::String(m.to_string()));
465 log::info!("Saved {} model: {}", provider, m);
466 }
467
468 let toml_str = toml::to_string_pretty(&config)
470 .context("Failed to serialize config to TOML")?;
471 std::fs::write(&config_path, toml_str)
472 .context("Failed to write ~/.reflex/config.toml")?;
473
474 Ok(())
475}
476
477pub fn get_provider_options(provider: &str) -> Option<HashMap<String, String>> {
482 let provider_lc = provider.to_lowercase();
483
484 match provider_lc.as_str() {
485 "openrouter" => {
486 if let Ok(Some(user_config)) = load_user_config() {
487 if let Some(credentials) = &user_config.credentials {
488 if let Some(sort) = &credentials.openrouter_sort {
489 let mut opts = HashMap::new();
490 opts.insert("sort".to_string(), sort.clone());
491 return Some(opts);
492 }
493 }
494 }
495 None
496 }
497 "openai-compatible" | "openai_compatible" => {
498 let base_url = load_user_config()
500 .ok()
501 .flatten()
502 .and_then(|cfg| cfg.credentials)
503 .and_then(|c| c.openai_compatible_base_url)
504 .or_else(|| env::var("OPENAI_COMPATIBLE_BASE_URL").ok())
505 .filter(|s| !s.is_empty());
506
507 base_url.map(|url| {
508 let mut opts = HashMap::new();
509 opts.insert("base_url".to_string(), url);
510 opts
511 })
512 }
513 _ => None,
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use std::sync::{Mutex, MutexGuard};
521 use tempfile::TempDir;
522
523 static ENV_LOCK: Mutex<()> = Mutex::new(());
531
532 fn env_guard() -> MutexGuard<'static, ()> {
536 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
537 }
538
539 #[test]
540 fn test_default_config() {
541 let config = SemanticConfig::default();
542 assert_eq!(config.enabled, true);
543 assert_eq!(config.provider, "openai");
544 assert_eq!(config.model, None);
545 assert_eq!(config.auto_execute, false);
546 }
547
548 #[test]
549 fn test_load_config_no_file() {
550 let _g = env_guard();
551 let temp = TempDir::new().unwrap();
552
553 unsafe {
555 env::set_var("HOME", temp.path());
556 }
557 let config = load_config(temp.path()).unwrap();
558 unsafe {
559 env::remove_var("HOME");
560 }
561
562 assert_eq!(config.provider, "openai");
564 assert_eq!(config.enabled, true);
565 }
566
567 #[test]
568 fn test_load_config_with_semantic_section() {
569 let _g = env_guard();
570 let temp = TempDir::new().unwrap();
571 let reflex_dir = temp.path().join(".reflex");
572 std::fs::create_dir_all(&reflex_dir).unwrap();
573 let config_path = reflex_dir.join("config.toml");
574
575 std::fs::write(
576 &config_path,
577 r#"
578[semantic]
579enabled = true
580provider = "anthropic"
581model = "claude-3-5-sonnet-20241022"
582auto_execute = true
583 "#,
584 )
585 .unwrap();
586
587 unsafe {
589 env::set_var("HOME", temp.path());
590 }
591 let config = load_config(temp.path()).unwrap();
592 unsafe {
593 env::remove_var("HOME");
594 }
595
596 assert_eq!(config.enabled, true);
597 assert_eq!(config.provider, "anthropic");
598 assert_eq!(config.model, Some("claude-3-5-sonnet-20241022".to_string()));
599 assert_eq!(config.auto_execute, true);
600 }
601
602 #[test]
603 fn test_load_config_without_semantic_section() {
604 let _g = env_guard();
605 let temp = TempDir::new().unwrap();
606 let reflex_dir = temp.path().join(".reflex");
607 std::fs::create_dir_all(&reflex_dir).unwrap();
608 let config_path = reflex_dir.join("config.toml");
609
610 std::fs::write(
611 &config_path,
612 r#"
613[index]
614languages = []
615 "#,
616 )
617 .unwrap();
618
619 unsafe {
621 env::set_var("HOME", temp.path());
622 }
623 let config = load_config(temp.path()).unwrap();
624 unsafe {
625 env::remove_var("HOME");
626 }
627
628 assert_eq!(config.provider, "openai");
630 }
631
632 #[test]
633 fn test_get_api_key_env_var() {
634 let _g = env_guard();
635 let temp = TempDir::new().unwrap();
636
637 unsafe {
639 env::set_var("HOME", temp.path());
640 env::set_var("OPENAI_API_KEY", "test-key-123");
641 }
642
643 let key = get_api_key("openai").unwrap();
644 assert_eq!(key, "test-key-123");
645
646 unsafe {
647 env::remove_var("OPENAI_API_KEY");
648 env::remove_var("HOME");
649 }
650 }
651
652 #[test]
653 fn test_get_api_key_missing() {
654 let _g = env_guard();
655 let temp = TempDir::new().unwrap();
656
657 unsafe {
659 env::set_var("HOME", temp.path());
660 env::remove_var("OPENROUTER_API_KEY");
661 env::remove_var("REFLEX_AI_API_KEY");
662 }
663
664 let result = get_api_key("openrouter");
665 assert!(result.is_err());
666 assert!(result.unwrap_err().to_string().contains("OPENROUTER_API_KEY"));
667
668 unsafe {
669 env::remove_var("HOME");
670 }
671 }
672
673 #[test]
674 fn test_get_api_key_unknown_provider() {
675 let _g = env_guard();
676 let result = get_api_key("unknown");
677 assert!(result.is_err());
678 assert!(result.unwrap_err().to_string().contains("Unknown provider"));
679 }
680
681 #[test]
682 fn test_env_override_provider() {
683 let _g = env_guard();
684 let temp = TempDir::new().unwrap();
685
686 unsafe {
687 env::set_var("HOME", temp.path());
688 env::set_var("REFLEX_PROVIDER", "openrouter");
689 }
690
691 let config = load_config(temp.path()).unwrap();
692
693 unsafe {
694 env::remove_var("REFLEX_PROVIDER");
695 env::remove_var("HOME");
696 }
697
698 assert_eq!(config.provider, "openrouter");
699 }
700
701 #[test]
702 fn test_env_override_model() {
703 let _g = env_guard();
704 let temp = TempDir::new().unwrap();
705
706 unsafe {
707 env::set_var("HOME", temp.path());
708 env::set_var("REFLEX_MODEL", "google/gemini-2.5-flash");
709 }
710
711 let config = load_config(temp.path()).unwrap();
712
713 unsafe {
714 env::remove_var("REFLEX_MODEL");
715 env::remove_var("HOME");
716 }
717
718 assert_eq!(config.model, Some("google/gemini-2.5-flash".to_string()));
719 assert_eq!(config.provider, "openai");
721 }
722
723 #[test]
724 fn test_get_api_key_generic_env_var() {
725 let _g = env_guard();
726 let temp = TempDir::new().unwrap();
727
728 unsafe {
729 env::set_var("HOME", temp.path());
730 env::remove_var("OPENROUTER_API_KEY");
731 env::set_var("REFLEX_AI_API_KEY", "generic-key-456");
732 }
733
734 let key = get_api_key("openrouter").unwrap();
735 assert_eq!(key, "generic-key-456");
736
737 unsafe {
738 env::remove_var("REFLEX_AI_API_KEY");
739 env::remove_var("HOME");
740 }
741 }
742
743 #[test]
744 fn test_get_api_key_openai_compatible_returns_empty_when_unset() {
745 let _g = env_guard();
746 let temp = TempDir::new().unwrap();
747
748 unsafe {
749 env::set_var("HOME", temp.path());
750 env::remove_var("OPENAI_COMPATIBLE_API_KEY");
751 env::remove_var("REFLEX_AI_API_KEY");
752 }
753
754 let key = get_api_key("openai-compatible").unwrap();
756 assert_eq!(key, "");
757
758 unsafe {
759 env::remove_var("HOME");
760 }
761 }
762
763 #[test]
764 fn test_get_provider_options_openai_compatible_from_config() {
765 let _g = env_guard();
766 let temp = TempDir::new().unwrap();
767 let reflex_dir = temp.path().join(".reflex");
768 std::fs::create_dir_all(&reflex_dir).unwrap();
769 let config_path = reflex_dir.join("config.toml");
770
771 std::fs::write(
772 &config_path,
773 r#"
774[credentials]
775openai_compatible_base_url = "http://localhost:1234/v1"
776openai_compatible_model = "qwen2.5-coder"
777 "#,
778 )
779 .unwrap();
780
781 unsafe {
782 env::set_var("HOME", temp.path());
783 env::remove_var("OPENAI_COMPATIBLE_BASE_URL");
784 }
785
786 let opts = get_provider_options("openai-compatible");
787 let model = get_user_model("openai-compatible");
788
789 unsafe {
790 env::remove_var("HOME");
791 }
792
793 let opts = opts.expect("base_url should be discovered from config");
794 assert_eq!(
795 opts.get("base_url").map(|s| s.as_str()),
796 Some("http://localhost:1234/v1")
797 );
798 assert_eq!(model, Some("qwen2.5-coder".to_string()));
799 }
800
801 #[test]
802 fn test_get_provider_options_openai_compatible_from_env() {
803 let _g = env_guard();
804 let temp = TempDir::new().unwrap();
805
806 unsafe {
807 env::set_var("HOME", temp.path());
808 env::set_var("OPENAI_COMPATIBLE_BASE_URL", "http://localhost:11434/v1");
809 }
810
811 let opts = get_provider_options("openai-compatible");
812
813 unsafe {
814 env::remove_var("OPENAI_COMPATIBLE_BASE_URL");
815 env::remove_var("HOME");
816 }
817
818 let opts = opts.expect("base_url should be discovered from env var");
819 assert_eq!(
820 opts.get("base_url").map(|s| s.as_str()),
821 Some("http://localhost:11434/v1")
822 );
823 }
824
825 fn config_with(provider: &str, project_model: Option<&str>) -> SemanticConfig {
826 SemanticConfig {
827 provider: provider.to_string(),
828 model: project_model.map(String::from),
829 ..SemanticConfig::default()
830 }
831 }
832
833 #[test]
834 fn resolve_model_prefers_override() {
835 let config = config_with("openai", Some("gpt-4o"));
836 let resolved = resolve_model(&config, Some("gpt-4o-2024-08-06"));
837 assert_eq!(resolved.as_deref(), Some("gpt-4o-2024-08-06"));
838 }
839
840 #[test]
841 fn resolve_model_falls_back_to_project_config() {
842 let config = config_with("openai", Some("gpt-4o"));
843 let resolved = resolve_model(&config, None);
844 assert_eq!(resolved.as_deref(), Some("gpt-4o"));
845 }
846
847 #[test]
848 fn resolve_model_returns_none_when_unset() {
849 let _g = env_guard();
850 let temp = TempDir::new().unwrap();
853 unsafe {
854 env::set_var("HOME", temp.path());
855 }
856
857 let config = config_with("openai", None);
858 let resolved = resolve_model(&config, None);
859
860 unsafe {
861 env::remove_var("HOME");
862 }
863
864 assert_eq!(resolved, None);
865 }
866
867 #[test]
868 fn resolve_model_for_openai_compatible_reads_user_config() {
869 let _g = env_guard();
870 let temp = TempDir::new().unwrap();
874 let reflex_dir = temp.path().join(".reflex");
875 std::fs::create_dir_all(&reflex_dir).unwrap();
876 std::fs::write(
877 reflex_dir.join("config.toml"),
878 r#"
879[credentials]
880openai_compatible_model = "gpt-oss:20b-cloud"
881 "#,
882 )
883 .unwrap();
884
885 unsafe {
886 env::set_var("HOME", temp.path());
887 }
888
889 let resolved = resolve_model_for("openai-compatible", None, None);
890
891 unsafe {
892 env::remove_var("HOME");
893 }
894
895 assert_eq!(resolved.as_deref(), Some("gpt-oss:20b-cloud"));
896 }
897
898 #[test]
899 fn resolve_model_for_override_beats_user_config() {
900 let _g = env_guard();
901 let temp = TempDir::new().unwrap();
902 let reflex_dir = temp.path().join(".reflex");
903 std::fs::create_dir_all(&reflex_dir).unwrap();
904 std::fs::write(
905 reflex_dir.join("config.toml"),
906 r#"
907[credentials]
908openrouter_model = "anthropic/claude-opus-4"
909 "#,
910 )
911 .unwrap();
912
913 unsafe {
914 env::set_var("HOME", temp.path());
915 }
916
917 let resolved = resolve_model_for("openrouter", None, Some("openai/gpt-4o"));
918
919 unsafe {
920 env::remove_var("HOME");
921 }
922
923 assert_eq!(resolved.as_deref(), Some("openai/gpt-4o"));
924 }
925}