use actix_web::{web, HttpResponse, Responder};
use serde::Serialize;
use bamboo_domain::reasoning::ReasoningEffort;
use bamboo_engine::config::GoldConfig;
use crate::app_state::AppState;
#[derive(Debug, Serialize)]
pub struct ExecuteDefaultsResponse {
pub model: Option<String>,
pub provider: Option<String>,
pub provider_type: Option<String>,
pub reasoning_effort: Option<ReasoningEffort>,
pub system_prompt: String,
pub base_system_prompt: String,
pub workspace_path: Option<String>,
pub gold_config: Option<GoldConfig>,
pub fast_model: Option<String>,
pub background_model: Option<String>,
pub summarization_model: Option<String>,
}
pub async fn handler(state: web::Data<AppState>) -> impl Responder {
let config_snapshot = state.config.read().await.clone();
let resolved = bamboo_engine::resolved_defaults::resolve_default_run_config(
&config_snapshot,
&state.provider_registry,
);
HttpResponse::Ok().json(ExecuteDefaultsResponse {
model: resolved.model_roster.model.clone(),
provider: resolved.model_roster.provider_name.clone(),
provider_type: resolved.model_roster.provider_type.clone(),
reasoning_effort: resolved.reasoning_effort,
system_prompt: resolved.system_prompt,
base_system_prompt: resolved.base_system_prompt,
workspace_path: resolved.workspace_path,
gold_config: resolved.gold_config,
fast_model: resolved.model_roster.fast_model(),
background_model: resolved.model_roster.background_model(),
summarization_model: resolved.model_roster.summarization_model(),
})
}
#[cfg(test)]
mod tests {
use actix_web::{test, web, App};
use serde_json::Value;
use tempfile::tempdir;
use crate::routes::configure_routes;
use crate::AppState;
async fn new_state() -> web::Data<AppState> {
let temp_dir = tempdir().expect("tempdir");
bamboo_config::paths::init_bamboo_dir(temp_dir.path().to_path_buf());
web::Data::new(
AppState::new(temp_dir.path().to_path_buf())
.await
.expect("app state"),
)
}
#[actix_web::test]
async fn defaults_endpoint_returns_resolved_area_models_not_raw_config_echo() {
let state = new_state().await;
{
let mut config = state.config.write().await;
config.features.provider_model_ref = true;
config.provider = "openai".to_string();
config.providers_mut().openai = Some(bamboo_config::OpenAIConfig {
api_key: "test-key".to_string(),
..Default::default()
});
config.defaults = Some(bamboo_config::DefaultsConfig {
chat: bamboo_domain::ProviderModelRef::new("openai", "gpt-chat"),
fast: Some(bamboo_domain::ProviderModelRef::new("openai", "gpt-fast")),
task_summary: Some(bamboo_domain::ProviderModelRef::new(
"openai",
"gpt-summary",
)),
vision: None,
memory_background: Some(bamboo_domain::ProviderModelRef::new(
"openai",
"gpt-memory",
)),
planning: None,
search: None,
code_review: None,
sub_agent: None,
subagent_models: Default::default(),
});
state
.provider_registry
.reload_from_config(&config, state.app_data_dir.clone())
.await
.expect("provider registry should reload with the openai provider registered");
}
let app = test::init_service(
App::new()
.app_data(state.clone())
.configure(configure_routes),
)
.await;
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri("/api/v1/execute/defaults")
.to_request(),
)
.await;
assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
let body: Value = test::read_body_json(resp).await;
assert_eq!(body["model"], "gpt-chat");
assert_eq!(body["fast_model"], "gpt-fast");
assert_eq!(body["background_model"], "gpt-memory");
assert_eq!(body["summarization_model"], "gpt-summary");
}
#[actix_web::test]
async fn defaults_endpoint_never_leaks_api_keys() {
let state = new_state().await;
{
let mut config = state.config.write().await;
config.provider = "openai".to_string();
config.providers_mut().openai = Some(bamboo_config::OpenAIConfig {
api_key: "sk-super-secret-value".to_string(),
model: Some("gpt-4o".to_string()),
..Default::default()
});
}
let app = test::init_service(
App::new()
.app_data(state.clone())
.configure(configure_routes),
)
.await;
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri("/api/v1/execute/defaults")
.to_request(),
)
.await;
assert_eq!(resp.status(), actix_web::http::StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8_lossy(&body);
assert!(!body_str.contains("sk-super-secret-value"));
}
}