use std::sync::Arc;
use bamboo_domain::reasoning::ReasoningEffort;
use bamboo_domain::ProviderModelRef;
use bamboo_infrastructure::{Config, ProviderRegistry, ResolvedModel};
use crate::model_config_helper::{
resolve_background_model, resolve_fast_model, resolve_subagent_model, resolve_task_summary_model,
resolve_vision_model,
};
pub struct GlobalAreaModels {
pub fast: Option<ResolvedModel>,
pub fast_ref: Option<ProviderModelRef>,
pub background: Option<ResolvedModel>,
pub background_ref: Option<ProviderModelRef>,
pub summarization: Option<ResolvedModel>,
pub summarization_ref: Option<ProviderModelRef>,
}
pub fn resolve_global_area_models(
config: &Config,
provider_name: &str,
provider_registry: &Arc<ProviderRegistry>,
) -> GlobalAreaModels {
let defaults = config.defaults.as_ref();
GlobalAreaModels {
fast: resolve_fast_model(config, provider_name, provider_registry),
fast_ref: defaults.and_then(|d| d.fast.clone()),
background: resolve_background_model(config, provider_name, provider_registry),
background_ref: defaults.and_then(|d| d.memory_background.clone()),
summarization: resolve_task_summary_model(config, provider_name, provider_registry),
summarization_ref: defaults.and_then(|d| d.task_summary.clone()),
}
}
pub fn resolve_global_vision_model(
config: &Config,
provider_name: &str,
provider_registry: &Arc<ProviderRegistry>,
) -> Option<ResolvedModel> {
resolve_vision_model(config, provider_name, provider_registry)
}
pub fn resolve_global_subagent_model(
config: &Config,
provider_name: &str,
provider_registry: &Arc<ProviderRegistry>,
subagent_type: &str,
) -> Option<ResolvedModel> {
resolve_subagent_model(config, provider_name, provider_registry, subagent_type)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningEffortSource {
Session,
Request,
ProviderDefault,
None,
}
impl ReasoningEffortSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Session => "session",
Self::Request => "request",
Self::ProviderDefault => "provider_default",
Self::None => "none",
}
}
}
pub fn resolve_effective_reasoning_effort(
session_effort: Option<ReasoningEffort>,
request_effort: Option<ReasoningEffort>,
provider_default: Option<ReasoningEffort>,
) -> (Option<ReasoningEffort>, ReasoningEffortSource) {
if let Some(effort) = session_effort {
(Some(effort), ReasoningEffortSource::Session)
} else if let Some(effort) = request_effort {
(Some(effort), ReasoningEffortSource::Request)
} else if let Some(effort) = provider_default {
(Some(effort), ReasoningEffortSource::ProviderDefault)
} else {
(None, ReasoningEffortSource::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bamboo_agent_core::tools::ToolSchema;
use bamboo_agent_core::Message;
use bamboo_domain::{Session, DEFAULT_REASONING_EFFORT};
use bamboo_infrastructure::{
DefaultsConfig, FeatureFlags, LLMError, LLMProvider, LLMStream, OpenAIConfig,
ProviderConfigs,
};
use std::collections::HashMap;
struct NoopProvider;
#[async_trait::async_trait]
impl LLMProvider for NoopProvider {
async fn chat_stream(
&self,
_messages: &[Message],
_tools: &[ToolSchema],
_max_output_tokens: Option<u32>,
_model: &str,
) -> Result<LLMStream, LLMError> {
Err(LLMError::Api("noop".to_string()))
}
}
fn test_registry() -> Arc<ProviderRegistry> {
let mut providers: HashMap<String, Arc<dyn LLMProvider>> = HashMap::new();
providers.insert("openai".to_string(), Arc::new(NoopProvider));
Arc::new(ProviderRegistry::new(providers, "openai".to_string()))
}
fn defaults_with_all_areas() -> DefaultsConfig {
DefaultsConfig {
chat: ProviderModelRef::new("openai", "gpt-chat"),
fast: Some(ProviderModelRef::new("openai", "gpt-fast")),
task_summary: Some(ProviderModelRef::new("openai", "gpt-summary")),
vision: Some(ProviderModelRef::new("openai", "gpt-vision")),
memory_background: Some(ProviderModelRef::new("openai", "gpt-memory")),
planning: None,
search: None,
code_review: None,
sub_agent: Some(ProviderModelRef::new("openai", "gpt-sub")),
subagent_models: HashMap::new(),
}
}
fn config_with_defaults(defaults: DefaultsConfig) -> Config {
Config {
provider: "openai".to_string(),
features: FeatureFlags {
provider_model_ref: true,
..Default::default()
},
defaults: Some(defaults),
..Config::default()
}
}
#[test]
fn global_area_models_read_each_area_from_its_own_default() {
let config = config_with_defaults(defaults_with_all_areas());
let areas = resolve_global_area_models(&config, "openai", &test_registry());
assert_eq!(areas.fast.as_ref().map(|m| m.model_name.as_str()), Some("gpt-fast"));
assert_eq!(
areas.summarization.as_ref().map(|m| m.model_name.as_str()),
Some("gpt-summary")
);
assert_eq!(
areas.background.as_ref().map(|m| m.model_name.as_str()),
Some("gpt-memory")
);
assert_eq!(areas.fast_ref, Some(ProviderModelRef::new("openai", "gpt-fast")));
assert_eq!(
areas.summarization_ref,
Some(ProviderModelRef::new("openai", "gpt-summary"))
);
assert_eq!(
areas.background_ref,
Some(ProviderModelRef::new("openai", "gpt-memory"))
);
}
#[test]
fn global_area_models_are_independent_of_any_session() {
let config = config_with_defaults(defaults_with_all_areas());
let registry = test_registry();
let before = resolve_global_area_models(&config, "openai", ®istry);
let mut session = Session::new("s1", "some-exotic-session-model");
session.model_ref = Some(ProviderModelRef::new("openai", "some-exotic-session-model"));
session.reasoning_effort = Some(ReasoningEffort::Max);
let _ = &session;
let after = resolve_global_area_models(&config, "openai", ®istry);
assert_eq!(
before.fast.as_ref().map(|m| m.model_name.clone()),
after.fast.as_ref().map(|m| m.model_name.clone())
);
assert_eq!(
before.background.as_ref().map(|m| m.model_name.clone()),
after.background.as_ref().map(|m| m.model_name.clone())
);
assert_eq!(
before.summarization.as_ref().map(|m| m.model_name.clone()),
after.summarization.as_ref().map(|m| m.model_name.clone())
);
assert_ne!(
after.fast.as_ref().map(|m| m.model_name.as_str()),
Some("some-exotic-session-model")
);
}
#[test]
fn vision_model_is_global_from_defaults() {
let config = config_with_defaults(defaults_with_all_areas());
let vision = resolve_global_vision_model(&config, "openai", &test_registry());
assert_eq!(vision.as_ref().map(|m| m.model_name.as_str()), Some("gpt-vision"));
}
#[test]
fn subagent_model_is_global_from_defaults() {
let config = config_with_defaults(defaults_with_all_areas());
let sub = resolve_global_subagent_model(&config, "openai", &test_registry(), "coder");
assert_eq!(sub.as_ref().map(|m| m.model_name.as_str()), Some("gpt-sub"));
}
#[test]
fn background_falls_back_to_fast_when_memory_background_unset() {
let mut defaults = defaults_with_all_areas();
defaults.memory_background = None;
let config = config_with_defaults(defaults);
let areas = resolve_global_area_models(&config, "openai", &test_registry());
assert_eq!(
areas.background.as_ref().map(|m| m.model_name.as_str()),
Some("gpt-fast")
);
}
#[test]
fn legacy_mode_resolves_fast_from_provider_config() {
let config = Config {
provider: "openai".to_string(),
features: FeatureFlags {
provider_model_ref: false,
..Default::default()
},
defaults: None,
providers: ProviderConfigs {
openai: Some(OpenAIConfig {
api_key: "test".to_string(),
api_key_encrypted: None,
base_url: None,
model: Some("gpt-4o".to_string()),
fast_model: Some("gpt-4o-mini".to_string()),
vision_model: None,
reasoning_effort: None,
responses_only_models: vec![],
request_overrides: None,
extra: Default::default(),
}),
..ProviderConfigs::default()
},
..Config::default()
};
let areas = resolve_global_area_models(&config, "openai", &test_registry());
assert_eq!(areas.fast.as_ref().map(|m| m.model_name.as_str()), Some("gpt-4o-mini"));
}
#[test]
fn reasoning_prefers_session_then_request_then_provider() {
assert_eq!(
resolve_effective_reasoning_effort(
Some(ReasoningEffort::Max),
Some(ReasoningEffort::High),
Some(ReasoningEffort::Low),
),
(Some(ReasoningEffort::Max), ReasoningEffortSource::Session)
);
assert_eq!(
resolve_effective_reasoning_effort(
None,
Some(ReasoningEffort::High),
Some(ReasoningEffort::Low),
),
(Some(ReasoningEffort::High), ReasoningEffortSource::Request)
);
assert_eq!(
resolve_effective_reasoning_effort(None, None, Some(ReasoningEffort::Low)),
(Some(ReasoningEffort::Low), ReasoningEffortSource::ProviderDefault)
);
}
#[test]
fn reasoning_none_when_nothing_configured() {
let (effort, source) = resolve_effective_reasoning_effort(None, None, None);
assert_eq!(effort, None);
assert_eq!(source, ReasoningEffortSource::None);
}
#[test]
fn canonical_default_is_medium_and_used_as_terminal() {
assert_eq!(DEFAULT_REASONING_EFFORT, ReasoningEffort::Medium);
let (effort, _) = resolve_effective_reasoning_effort(None, None, None);
assert_eq!(effort.unwrap_or(DEFAULT_REASONING_EFFORT), ReasoningEffort::Medium);
}
}