1use crate::cli::{ReasoningEffort, ReasoningSummary, WebSearchMode};
4use agent_client_protocol::schema::v1::{SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption};
5
6pub const MODEL_CONFIG_OPTION_ID: &str = "model";
8
9pub const WEBSEARCH_CONFIG_OPTION_ID: &str = "websearch";
11pub const REASONING_EFFORT_CONFIG_OPTION_ID: &str = "reasoning_effort";
13pub const REASONING_SUMMARY_CONFIG_OPTION_ID: &str = "reasoning_summary";
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ConfigOptionSpec {
19 pub id: &'static str,
21 pub label: &'static str,
23 pub mode: &'static str,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum ConfigOptionValue {
30 Model(String),
32 WebSearch(WebSearchMode),
34 ReasoningEffort(ReasoningEffort),
36 ReasoningSummary(ReasoningSummary),
38}
39
40impl ConfigOptionValue {
41 pub fn as_persisted_value(&self) -> String {
43 match self {
44 Self::Model(model) => model.clone(),
45 Self::WebSearch(mode) => mode.label().to_string(),
46 Self::ReasoningEffort(effort) => effort.label().to_string(),
47 Self::ReasoningSummary(summary) => summary.label().to_string(),
48 }
49 }
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum ConfigOptionError {
55 UnknownOption { id: String },
57 InvalidValue { id: String, value: String, reason: String },
59}
60
61impl core::fmt::Display for ConfigOptionError {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 match self {
64 Self::UnknownOption { id } => write!(f, "unknown config option `{id}`"),
65 Self::InvalidValue { id, value, reason } => {
66 write!(f, "invalid value `{value}` for config option `{id}`: {reason}")
67 }
68 }
69 }
70}
71
72impl std::error::Error for ConfigOptionError {}
73
74pub fn initial_config_option_ids() -> &'static [&'static str; 4] {
78 &[
79 MODEL_CONFIG_OPTION_ID,
80 WEBSEARCH_CONFIG_OPTION_ID,
81 REASONING_EFFORT_CONFIG_OPTION_ID,
82 REASONING_SUMMARY_CONFIG_OPTION_ID,
83 ]
84}
85
86pub fn initial_config_options() -> &'static [ConfigOptionSpec] {
88 const OPTIONS: [ConfigOptionSpec; 4] = [
89 ConfigOptionSpec { id: MODEL_CONFIG_OPTION_ID, label: "Model", mode: "string" },
90 ConfigOptionSpec { id: WEBSEARCH_CONFIG_OPTION_ID, label: "Web Search", mode: "enum" },
91 ConfigOptionSpec { id: REASONING_EFFORT_CONFIG_OPTION_ID, label: "Reasoning Effort", mode: "enum" },
92 ConfigOptionSpec { id: REASONING_SUMMARY_CONFIG_OPTION_ID, label: "Reasoning Summary", mode: "enum" },
93 ];
94 &OPTIONS
95}
96
97pub fn acp_config_options(
99 model: &str, websearch: WebSearchMode, effort: ReasoningEffort, summary: ReasoningSummary,
100) -> Vec<SessionConfigOption> {
101 vec![
102 model_config_option(model),
103 websearch_config_option(websearch),
104 reasoning_effort_config_option(model, effort),
105 reasoning_summary_config_option(summary),
106 ]
107}
108
109pub fn validate_config_option(option_id: &str, value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
111 match option_id {
112 MODEL_CONFIG_OPTION_ID => validate_model(value),
113 WEBSEARCH_CONFIG_OPTION_ID => validate_websearch(value),
114 REASONING_EFFORT_CONFIG_OPTION_ID => validate_reasoning_effort(value),
115 REASONING_SUMMARY_CONFIG_OPTION_ID => validate_reasoning_summary(value),
116 _ => Err(ConfigOptionError::UnknownOption { id: option_id.to_string() }),
117 }
118}
119
120fn validate_model(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
121 let trimmed = value.trim();
122 if trimmed.is_empty() {
123 return Err(ConfigOptionError::InvalidValue {
124 id: MODEL_CONFIG_OPTION_ID.to_string(),
125 value: value.to_string(),
126 reason: "model may not be empty".to_string(),
127 });
128 }
129 Ok(ConfigOptionValue::Model(trimmed.to_string()))
130}
131
132fn validate_websearch(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
133 let mode = match value.to_lowercase().as_str() {
134 "duckduckgo" => WebSearchMode::DuckDuckGo,
135 "searxng" => WebSearchMode::Searxng,
136 "none" => WebSearchMode::None,
137 other => {
138 return Err(ConfigOptionError::InvalidValue {
139 id: WEBSEARCH_CONFIG_OPTION_ID.to_string(),
140 value: other.to_string(),
141 reason: "must be one of duckduckgo/searxng/none".to_string(),
142 });
143 }
144 };
145 Ok(ConfigOptionValue::WebSearch(mode))
146}
147
148fn validate_reasoning_effort(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
149 ReasoningEffort::parse(value)
150 .map(ConfigOptionValue::ReasoningEffort)
151 .ok_or_else(|| ConfigOptionError::InvalidValue {
152 id: REASONING_EFFORT_CONFIG_OPTION_ID.to_string(),
153 value: value.to_string(),
154 reason: "must be one of auto/on/none/minimal/low/medium/high/xhigh/max".to_string(),
155 })
156}
157
158fn validate_reasoning_summary(value: &str) -> Result<ConfigOptionValue, ConfigOptionError> {
159 ReasoningSummary::parse(value)
160 .map(ConfigOptionValue::ReasoningSummary)
161 .ok_or_else(|| ConfigOptionError::InvalidValue {
162 id: REASONING_SUMMARY_CONFIG_OPTION_ID.to_string(),
163 value: value.to_string(),
164 reason: "must be one of off/auto".to_string(),
165 })
166}
167
168fn model_config_option(model: &str) -> SessionConfigOption {
169 SessionConfigOption::select(
170 MODEL_CONFIG_OPTION_ID,
171 "Model",
172 model.to_string(),
173 vec![SessionConfigSelectOption::new(model.to_string(), model.to_string())],
174 )
175 .description("Provider model for future prompt turns")
176 .category(SessionConfigOptionCategory::Model)
177}
178
179fn websearch_config_option(websearch: WebSearchMode) -> SessionConfigOption {
180 let current = websearch.label();
181 SessionConfigOption::select(
182 WEBSEARCH_CONFIG_OPTION_ID,
183 "Web Search",
184 current,
185 vec![
186 SessionConfigSelectOption::new("duckduckgo", "DuckDuckGo"),
187 SessionConfigSelectOption::new("searxng", "SearXNG"),
188 SessionConfigSelectOption::new("none", "None"),
189 ],
190 )
191 .description("Web search mode for future prompt turns")
192 .category(SessionConfigOptionCategory::ModelConfig)
193}
194
195fn reasoning_effort_config_option(model: &str, effort: ReasoningEffort) -> SessionConfigOption {
196 let options: Vec<SessionConfigSelectOption> = crate::providers::reasoning_options(model)
197 .into_iter()
198 .map(|option| SessionConfigSelectOption::new(option.label(), option.display_label()))
199 .collect();
200 SessionConfigOption::select(
201 REASONING_EFFORT_CONFIG_OPTION_ID,
202 "Reasoning Effort",
203 effort.label(),
204 options,
205 )
206 .description("Reasoning control supported by the selected model for future prompt turns")
207 .category(SessionConfigOptionCategory::ModelConfig)
208}
209
210fn reasoning_summary_config_option(summary: ReasoningSummary) -> SessionConfigOption {
211 SessionConfigOption::select(
212 REASONING_SUMMARY_CONFIG_OPTION_ID,
213 "Reasoning Summary",
214 summary.label(),
215 vec![
216 SessionConfigSelectOption::new("off", "Off"),
217 SessionConfigSelectOption::new("auto", "Auto"),
218 ],
219 )
220 .description("Whether ChatGPT Codex reasoning summaries are shown for future prompt turns")
221 .category(SessionConfigOptionCategory::ModelConfig)
222}