bamboo_engine/
resolved_defaults.rs1use std::sync::Arc;
21
22use bamboo_domain::reasoning::ReasoningEffort;
23use bamboo_llm::{Config, ProviderRegistry};
24
25use crate::config::GoldConfig;
26use crate::ModelRoster;
27
28#[derive(Clone)]
31pub struct ResolvedDefaultRunConfig {
32 pub model_roster: ModelRoster,
36 pub reasoning_effort: Option<ReasoningEffort>,
37 pub gold_config: Option<GoldConfig>,
38 pub system_prompt: String,
40 pub base_system_prompt: String,
41 pub workspace_path: Option<String>,
42}
43
44pub fn resolve_default_run_config(
48 config_snapshot: &Config,
49 provider_registry: &Arc<ProviderRegistry>,
50) -> ResolvedDefaultRunConfig {
51 let model = config_snapshot.get_model().unwrap_or_default();
52 let provider_name = Some(config_snapshot.effective_default_provider().to_string());
53 let provider_type = provider_name.as_deref().and_then(|name| {
54 crate::model_config_helper::resolve_provider_type(config_snapshot, name, provider_registry)
55 });
56 let capability_provider_name = provider_name
57 .as_deref()
58 .unwrap_or(config_snapshot.effective_default_provider());
59 let areas = crate::model_areas::resolve_global_area_models(
62 config_snapshot,
63 capability_provider_name,
64 provider_registry,
65 );
66 let reasoning_effort = config_snapshot.get_reasoning_effort();
67 let base_system_prompt = crate::prompt_defaults::read_global_default_system_prompt_template();
68 let workspace_path = config_snapshot
69 .get_default_work_area_path()
70 .map(|path| bamboo_config::paths::path_to_display_string(&path));
71 let system_prompt = crate::context::assemble_system_prompt(
72 &base_system_prompt,
73 None,
74 workspace_path.as_deref(),
75 );
76 let model_roster = ModelRoster::from_areas(Some(model), provider_name, provider_type, areas);
77
78 ResolvedDefaultRunConfig {
79 model_roster,
80 reasoning_effort,
81 gold_config: crate::model_config_helper::resolve_gold_config(config_snapshot, None),
82 system_prompt,
83 base_system_prompt,
84 workspace_path,
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 macro_rules! test_config {
93 (@assign $config:ident, providers, $value:expr) => { *$config.providers_mut() = $value; };
94 (@assign $config:ident, memory, $value:expr) => { *$config.memory_mut() = $value; };
95 (@assign $config:ident, subagents, $value:expr) => { *$config.subagents_mut() = $value; };
96 (@assign $config:ident, $field:ident, $value:expr) => { $config.$field = $value; };
97 ($($field:ident: $value:expr),* $(,)?) => {{
98 let mut config = Config::default();
99 $(test_config!(@assign config, $field, $value);)*
100 config
101 }};
102 }
103 use bamboo_agent_core::tools::ToolSchema;
104 use bamboo_agent_core::Message;
105 use bamboo_config::{DefaultsConfig, FeatureFlags, OpenAIConfig, ProviderConfigs};
106 use bamboo_domain::ProviderModelRef;
107 use bamboo_llm::{LLMError, LLMProvider, LLMStream};
108 use std::collections::HashMap;
109
110 struct NoopProvider;
111
112 #[async_trait::async_trait]
113 impl LLMProvider for NoopProvider {
114 async fn chat_stream(
115 &self,
116 _messages: &[Message],
117 _tools: &[ToolSchema],
118 _max_output_tokens: Option<u32>,
119 _model: &str,
120 ) -> Result<LLMStream, LLMError> {
121 Err(LLMError::Api("noop".to_string()))
122 }
123 }
124
125 fn test_registry() -> Arc<ProviderRegistry> {
126 let mut providers: HashMap<String, Arc<dyn LLMProvider>> = HashMap::new();
127 providers.insert("openai".to_string(), Arc::new(NoopProvider));
128 Arc::new(ProviderRegistry::new(providers, "openai".to_string()))
129 }
130
131 fn config_with_area_defaults() -> Config {
132 let defaults = DefaultsConfig {
133 chat: ProviderModelRef::new("openai", "gpt-chat"),
134 fast: Some(ProviderModelRef::new("openai", "gpt-fast")),
135 task_summary: Some(ProviderModelRef::new("openai", "gpt-summary")),
136 vision: None,
137 memory_background: Some(ProviderModelRef::new("openai", "gpt-memory")),
138 planning: None,
139 search: None,
140 code_review: None,
141 sub_agent: None,
142 subagent_models: HashMap::new(),
143 };
144 test_config! {
145 provider: "openai".to_string(),
146 features: FeatureFlags {
147 provider_model_ref: true,
148 ..Default::default()
149 },
150 providers: ProviderConfigs {
151 openai: Some(OpenAIConfig {
152 api_key: "test-secret-key".to_string(),
153 model: Some("gpt-chat".to_string()),
154 ..Default::default()
155 }),
156 ..Default::default()
157 },
158 defaults: Some(defaults),
159 }
160 }
161
162 #[test]
165 fn resolve_default_run_config_resolves_distinct_area_models() {
166 let config = config_with_area_defaults();
167 let registry = test_registry();
168
169 let resolved = resolve_default_run_config(&config, ®istry);
170
171 assert_eq!(resolved.model_roster.model.as_deref(), Some("gpt-chat"));
172 assert_eq!(
173 resolved.model_roster.fast_model().as_deref(),
174 Some("gpt-fast")
175 );
176 assert_eq!(
177 resolved.model_roster.background_model().as_deref(),
178 Some("gpt-memory")
179 );
180 assert_eq!(
181 resolved.model_roster.summarization_model().as_deref(),
182 Some("gpt-summary")
183 );
184 }
185
186 #[test]
187 fn resolve_default_run_config_never_leaks_api_keys() {
188 let config = config_with_area_defaults();
189 let registry = test_registry();
190
191 let resolved = resolve_default_run_config(&config, ®istry);
192
193 assert!(!resolved.system_prompt.contains("test-secret-key"));
194 assert!(!resolved.base_system_prompt.contains("test-secret-key"));
195 let gold_json =
196 serde_json::to_string(&resolved.gold_config).unwrap_or_else(|_| "null".to_string());
197 assert!(!gold_json.contains("test-secret-key"));
198 }
199}