use crate::cli::{ReasoningEffort, ReasoningSummary, WebSearchMode};
use agent_client_protocol::schema::v1::{SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption};
pub const MODEL_CONFIG_OPTION_ID: &str = "model";
pub const WEBSEARCH_CONFIG_OPTION_ID: &str = "websearch";
pub const REASONING_EFFORT_CONFIG_OPTION_ID: &str = "reasoning_effort";
pub const REASONING_SUMMARY_CONFIG_OPTION_ID: &str = "reasoning_summary";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigOptionSpec {
pub id: &'static str,
pub label: &'static str,
pub mode: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigOptionValue {
Model(String),
WebSearch(WebSearchMode),
ReasoningEffort(ReasoningEffort),
ReasoningSummary(ReasoningSummary),
}
impl ConfigOptionValue {
pub fn as_persisted_value(&self) -> String {
match self {
Self::Model(model) => model.clone(),
Self::WebSearch(mode) => mode.label().to_string(),
Self::ReasoningEffort(effort) => effort.label().to_string(),
Self::ReasoningSummary(summary) => summary.label().to_string(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ConfigOptionError {
UnknownOption { id: String },
InvalidValue { id: String, value: String, reason: String },
}
impl core::fmt::Display for ConfigOptionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownOption { id } => write!(f, "unknown config option `{id}`"),
Self::InvalidValue { id, value, reason } => {
write!(f, "invalid value `{value}` for config option `{id}`: {reason}")
}
}
}
}
impl std::error::Error for ConfigOptionError {}
pub fn initial_config_option_ids() -> &'static [&'static str; 4] {
&[
MODEL_CONFIG_OPTION_ID,
WEBSEARCH_CONFIG_OPTION_ID,
REASONING_EFFORT_CONFIG_OPTION_ID,
REASONING_SUMMARY_CONFIG_OPTION_ID,
]
}
pub fn initial_config_options() -> &'static [ConfigOptionSpec] {
const OPTIONS: [ConfigOptionSpec; 4] = [
ConfigOptionSpec { id: MODEL_CONFIG_OPTION_ID, label: "Model", mode: "string" },
ConfigOptionSpec { id: WEBSEARCH_CONFIG_OPTION_ID, label: "Web Search", mode: "enum" },
ConfigOptionSpec { id: REASONING_EFFORT_CONFIG_OPTION_ID, label: "Reasoning Effort", mode: "enum" },
ConfigOptionSpec { id: REASONING_SUMMARY_CONFIG_OPTION_ID, label: "Reasoning Summary", mode: "enum" },
];
&OPTIONS
}
pub fn acp_config_options(
model: &str, websearch: WebSearchMode, effort: ReasoningEffort, summary: ReasoningSummary,
) -> Vec<SessionConfigOption> {
vec![
model_config_option(model),
websearch_config_option(websearch),
reasoning_effort_config_option(model, effort),
reasoning_summary_config_option(summary),
]
}
pub fn validate_config_option(option_id: &str, value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
match option_id {
MODEL_CONFIG_OPTION_ID => validate_model(value),
WEBSEARCH_CONFIG_OPTION_ID => validate_websearch(value),
REASONING_EFFORT_CONFIG_OPTION_ID => validate_reasoning_effort(value),
REASONING_SUMMARY_CONFIG_OPTION_ID => validate_reasoning_summary(value),
_ => Err(ConfigOptionError::UnknownOption { id: option_id.to_string() }),
}
}
fn validate_model(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ConfigOptionError::InvalidValue {
id: MODEL_CONFIG_OPTION_ID.to_string(),
value: value.to_string(),
reason: "model may not be empty".to_string(),
});
}
Ok(ConfigOptionValue::Model(trimmed.to_string()))
}
fn validate_websearch(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
let mode = match value.to_lowercase().as_str() {
"duckduckgo" => WebSearchMode::DuckDuckGo,
"searxng" => WebSearchMode::Searxng,
"none" => WebSearchMode::None,
other => {
return Err(ConfigOptionError::InvalidValue {
id: WEBSEARCH_CONFIG_OPTION_ID.to_string(),
value: other.to_string(),
reason: "must be one of duckduckgo/searxng/none".to_string(),
});
}
};
Ok(ConfigOptionValue::WebSearch(mode))
}
fn validate_reasoning_effort(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
ReasoningEffort::parse(value)
.map(ConfigOptionValue::ReasoningEffort)
.ok_or_else(|| ConfigOptionError::InvalidValue {
id: REASONING_EFFORT_CONFIG_OPTION_ID.to_string(),
value: value.to_string(),
reason: "must be one of auto/on/none/minimal/low/medium/high/xhigh/max".to_string(),
})
}
fn validate_reasoning_summary(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
ReasoningSummary::parse(value)
.map(ConfigOptionValue::ReasoningSummary)
.ok_or_else(|| ConfigOptionError::InvalidValue {
id: REASONING_SUMMARY_CONFIG_OPTION_ID.to_string(),
value: value.to_string(),
reason: "must be one of off/auto".to_string(),
})
}
fn model_config_option(model: &str) -> SessionConfigOption {
SessionConfigOption::select(
MODEL_CONFIG_OPTION_ID,
"Model",
model.to_string(),
vec![SessionConfigSelectOption::new(model.to_string(), model.to_string())],
)
.description("Provider model for future prompt turns")
.category(SessionConfigOptionCategory::Model)
}
fn websearch_config_option(websearch: WebSearchMode) -> SessionConfigOption {
let current = websearch.label();
SessionConfigOption::select(
WEBSEARCH_CONFIG_OPTION_ID,
"Web Search",
current,
vec![
SessionConfigSelectOption::new("duckduckgo", "DuckDuckGo"),
SessionConfigSelectOption::new("searxng", "SearXNG"),
SessionConfigSelectOption::new("none", "None"),
],
)
.description("Web search mode for future prompt turns")
.category(SessionConfigOptionCategory::ModelConfig)
}
fn reasoning_effort_config_option(model: &str, effort: ReasoningEffort) -> SessionConfigOption {
let options: Vec<SessionConfigSelectOption> = crate::providers::reasoning_options(model)
.into_iter()
.map(|option| SessionConfigSelectOption::new(option.label(), option.display_label()))
.collect();
SessionConfigOption::select(
REASONING_EFFORT_CONFIG_OPTION_ID,
"Reasoning Effort",
effort.label(),
options,
)
.description("Reasoning control supported by the selected model for future prompt turns")
.category(SessionConfigOptionCategory::ModelConfig)
}
fn reasoning_summary_config_option(summary: ReasoningSummary) -> SessionConfigOption {
SessionConfigOption::select(
REASONING_SUMMARY_CONFIG_OPTION_ID,
"Reasoning Summary",
summary.label(),
vec![
SessionConfigSelectOption::new("off", "Off"),
SessionConfigSelectOption::new("auto", "Auto"),
],
)
.description("Whether ChatGPT Codex reasoning summaries are shown for future prompt turns")
.category(SessionConfigOptionCategory::ModelConfig)
}