1use std::sync::Arc;
2
3use ::open_ai::Model as OpenAiModel;
4use anthropic::Model as AnthropicModel;
5use gpui::Pixels;
6use language_model::{CloudModel, LanguageModel};
7use ollama::Model as OllamaModel;
8use schemars::{schema::Schema, JsonSchema};
9use serde::{Deserialize, Serialize};
10use settings::{Settings, SettingsSources};
11
12#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum AssistantDockPosition {
15 Left,
16 #[default]
17 Right,
18 Bottom,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
22#[serde(tag = "name", rename_all = "snake_case")]
23pub enum AssistantProviderContentV1 {
24 #[serde(rename = "zed.dev")]
25 ZedDotDev { default_model: Option<CloudModel> },
26 #[serde(rename = "openai")]
27 OpenAi {
28 default_model: Option<OpenAiModel>,
29 api_url: Option<String>,
30 available_models: Option<Vec<OpenAiModel>>,
31 },
32 #[serde(rename = "anthropic")]
33 Anthropic {
34 default_model: Option<AnthropicModel>,
35 api_url: Option<String>,
36 },
37 #[serde(rename = "ollama")]
38 Ollama {
39 default_model: Option<OllamaModel>,
40 api_url: Option<String>,
41 },
42}
43
44#[derive(Debug, Default)]
45pub struct AssistantSettings {
46 pub enabled: bool,
47 pub button: bool,
48 pub dock: AssistantDockPosition,
49 pub default_width: Pixels,
50 pub default_height: Pixels,
51 pub default_model: LanguageModelSelection,
52 pub inline_alternatives: Vec<LanguageModelSelection>,
53 pub using_outdated_settings_version: bool,
54 pub enable_experimental_live_diffs: bool,
55}
56
57/// Assistant panel settings
58#[derive(Clone, Serialize, Deserialize, Debug)]
59#[serde(untagged)]
60pub enum AssistantSettingsContent {
61 Versioned(VersionedAssistantSettingsContent),
62 Legacy(LegacyAssistantSettingsContent),
63}
64
65impl JsonSchema for AssistantSettingsContent {
66 fn schema_name() -> String {
67 VersionedAssistantSettingsContent::schema_name()
68 }
69
70 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
71 VersionedAssistantSettingsContent::json_schema(gen)
72 }
73
74 fn is_referenceable() -> bool {
75 VersionedAssistantSettingsContent::is_referenceable()
76 }
77}
78
79impl Default for AssistantSettingsContent {
80 fn default() -> Self {
81 Self::Versioned(VersionedAssistantSettingsContent::default())
82 }
83}
84
85impl AssistantSettingsContent {
86 pub fn is_version_outdated(&self) -> bool {
87 match self {
88 AssistantSettingsContent::Versioned(settings) => match settings {
89 VersionedAssistantSettingsContent::V1(_) => true,
90 VersionedAssistantSettingsContent::V2(_) => false,
91 },
92 AssistantSettingsContent::Legacy(_) => true,
93 }
94 }
95
96 fn upgrade(&self) -> AssistantSettingsContentV2 {
97 match self {
98 AssistantSettingsContent::Versioned(settings) => match settings {
99 VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
100 enabled: settings.enabled,
101 button: settings.button,
102 dock: settings.dock,
103 default_width: settings.default_width,
104 default_height: settings.default_width,
105 default_model: settings
106 .provider
107 .clone()
108 .and_then(|provider| match provider {
109 AssistantProviderContentV1::ZedDotDev { default_model } => {
110 default_model.map(|model| LanguageModelSelection {
111 provider: "zed.dev".to_string(),
112 model: model.id().to_string(),
113 })
114 }
115 AssistantProviderContentV1::OpenAi { default_model, .. } => {
116 default_model.map(|model| LanguageModelSelection {
117 provider: "openai".to_string(),
118 model: model.id().to_string(),
119 })
120 }
121 AssistantProviderContentV1::Anthropic { default_model, .. } => {
122 default_model.map(|model| LanguageModelSelection {
123 provider: "anthropic".to_string(),
124 model: model.id().to_string(),
125 })
126 }
127 AssistantProviderContentV1::Ollama { default_model, .. } => {
128 default_model.map(|model| LanguageModelSelection {
129 provider: "ollama".to_string(),
130 model: model.id().to_string(),
131 })
132 }
133 }),
134 inline_alternatives: None,
135 enable_experimental_live_diffs: None,
136 },
137 VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
138 },
139 AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
140 enabled: None,
141 button: settings.button,
142 dock: settings.dock,
143 default_width: settings.default_width,
144 default_height: settings.default_height,
145 default_model: Some(LanguageModelSelection {
146 provider: "openai".to_string(),
147 model: settings
148 .default_open_ai_model
149 .clone()
150 .unwrap_or_default()
151 .id()
152 .to_string(),
153 }),
154 inline_alternatives: None,
155 enable_experimental_live_diffs: None,
156 },
157 }
158 }
159
160 pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
161 let model = language_model.id().0.to_string();
162 let provider = language_model.provider_id().0.to_string();
163
164 match self {
165 AssistantSettingsContent::Versioned(settings) => match settings {
166 VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
167 "zed.dev" => {
168 log::warn!("attempted to set zed.dev model on outdated settings");
169 }
170 "anthropic" => {
171 let api_url = match &settings.provider {
172 Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
173 api_url.clone()
174 }
175 _ => None,
176 };
177 settings.provider = Some(AssistantProviderContentV1::Anthropic {
178 default_model: AnthropicModel::from_id(&model).ok(),
179 api_url,
180 });
181 }
182 "ollama" => {
183 let api_url = match &settings.provider {
184 Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
185 api_url.clone()
186 }
187 _ => None,
188 };
189 settings.provider = Some(AssistantProviderContentV1::Ollama {
190 default_model: Some(ollama::Model::new(&model, None, None)),
191 api_url,
192 });
193 }
194 "openai" => {
195 let (api_url, available_models) = match &settings.provider {
196 Some(AssistantProviderContentV1::OpenAi {
197 api_url,
198 available_models,
199 ..
200 }) => (api_url.clone(), available_models.clone()),
201 _ => (None, None),
202 };
203 settings.provider = Some(AssistantProviderContentV1::OpenAi {
204 default_model: OpenAiModel::from_id(&model).ok(),
205 api_url,
206 available_models,
207 });
208 }
209 _ => {}
210 },
211 VersionedAssistantSettingsContent::V2(settings) => {
212 settings.default_model = Some(LanguageModelSelection { provider, model });
213 }
214 },
215 AssistantSettingsContent::Legacy(settings) => {
216 if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
217 settings.default_open_ai_model = Some(model);
218 }
219 }
220 }
221 }
222}
223
224#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
225#[serde(tag = "version")]
226pub enum VersionedAssistantSettingsContent {
227 #[serde(rename = "1")]
228 V1(AssistantSettingsContentV1),
229 #[serde(rename = "2")]
230 V2(AssistantSettingsContentV2),
231}
232
233impl Default for VersionedAssistantSettingsContent {
234 fn default() -> Self {
235 Self::V2(AssistantSettingsContentV2 {
236 enabled: None,
237 button: None,
238 dock: None,
239 default_width: None,
240 default_height: None,
241 default_model: None,
242 inline_alternatives: None,
243 enable_experimental_live_diffs: None,
244 })
245 }
246}
247
248#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
249pub struct AssistantSettingsContentV2 {
250 /// Whether the Assistant is enabled.
251 ///
252 /// Default: true
253 enabled: Option<bool>,
254 /// Whether to show the assistant panel button in the status bar.
255 ///
256 /// Default: true
257 button: Option<bool>,
258 /// Where to dock the assistant.
259 ///
260 /// Default: right
261 dock: Option<AssistantDockPosition>,
262 /// Default width in pixels when the assistant is docked to the left or right.
263 ///
264 /// Default: 640
265 default_width: Option<f32>,
266 /// Default height in pixels when the assistant is docked to the bottom.
267 ///
268 /// Default: 320
269 default_height: Option<f32>,
270 /// The default model to use when creating new chats.
271 default_model: Option<LanguageModelSelection>,
272 /// Additional models with which to generate alternatives when performing inline assists.
273 inline_alternatives: Option<Vec<LanguageModelSelection>>,
274 /// Enable experimental live diffs in the assistant panel.
275 ///
276 /// Default: false
277 enable_experimental_live_diffs: Option<bool>,
278}
279
280#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
281pub struct LanguageModelSelection {
282 #[schemars(schema_with = "providers_schema")]
283 pub provider: String,
284 pub model: String,
285}
286
287fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
288 schemars::schema::SchemaObject {
289 enum_values: Some(vec![
290 "anthropic".into(),
291 "google".into(),
292 "ollama".into(),
293 "openai".into(),
294 "zed.dev".into(),
295 "copilot_chat".into(),
296 ]),
297 ..Default::default()
298 }
299 .into()
300}
301
302impl Default for LanguageModelSelection {
303 fn default() -> Self {
304 Self {
305 provider: "openai".to_string(),
306 model: "gpt-4".to_string(),
307 }
308 }
309}
310
311#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
312pub struct AssistantSettingsContentV1 {
313 /// Whether the Assistant is enabled.
314 ///
315 /// Default: true
316 enabled: Option<bool>,
317 /// Whether to show the assistant panel button in the status bar.
318 ///
319 /// Default: true
320 button: Option<bool>,
321 /// Where to dock the assistant.
322 ///
323 /// Default: right
324 dock: Option<AssistantDockPosition>,
325 /// Default width in pixels when the assistant is docked to the left or right.
326 ///
327 /// Default: 640
328 default_width: Option<f32>,
329 /// Default height in pixels when the assistant is docked to the bottom.
330 ///
331 /// Default: 320
332 default_height: Option<f32>,
333 /// The provider of the assistant service.
334 ///
335 /// This can be "openai", "anthropic", "ollama", "zed.dev"
336 /// each with their respective default models and configurations.
337 provider: Option<AssistantProviderContentV1>,
338}
339
340#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
341pub struct LegacyAssistantSettingsContent {
342 /// Whether to show the assistant panel button in the status bar.
343 ///
344 /// Default: true
345 pub button: Option<bool>,
346 /// Where to dock the assistant.
347 ///
348 /// Default: right
349 pub dock: Option<AssistantDockPosition>,
350 /// Default width in pixels when the assistant is docked to the left or right.
351 ///
352 /// Default: 640
353 pub default_width: Option<f32>,
354 /// Default height in pixels when the assistant is docked to the bottom.
355 ///
356 /// Default: 320
357 pub default_height: Option<f32>,
358 /// The default OpenAI model to use when creating new chats.
359 ///
360 /// Default: gpt-4-1106-preview
361 pub default_open_ai_model: Option<OpenAiModel>,
362 /// OpenAI API base URL to use when creating new chats.
363 ///
364 /// Default: https://api.openai.com/v1
365 pub openai_api_url: Option<String>,
366}
367
368impl Settings for AssistantSettings {
369 const KEY: Option<&'static str> = Some("assistant");
370
371 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
372
373 type FileContent = AssistantSettingsContent;
374
375 fn load(
376 sources: SettingsSources<Self::FileContent>,
377 _: &mut gpui::AppContext,
378 ) -> anyhow::Result<Self> {
379 let mut settings = AssistantSettings::default();
380
381 for value in sources.defaults_and_customizations() {
382 if value.is_version_outdated() {
383 settings.using_outdated_settings_version = true;
384 }
385
386 let value = value.upgrade();
387 merge(&mut settings.enabled, value.enabled);
388 merge(&mut settings.button, value.button);
389 merge(&mut settings.dock, value.dock);
390 merge(
391 &mut settings.default_width,
392 value.default_width.map(Into::into),
393 );
394 merge(
395 &mut settings.default_height,
396 value.default_height.map(Into::into),
397 );
398 merge(&mut settings.default_model, value.default_model);
399 merge(&mut settings.inline_alternatives, value.inline_alternatives);
400 merge(
401 &mut settings.enable_experimental_live_diffs,
402 value.enable_experimental_live_diffs,
403 );
404 }
405
406 Ok(settings)
407 }
408}
409
410fn merge<T>(target: &mut T, value: Option<T>) {
411 if let Some(value) = value {
412 *target = value;
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use fs::Fs;
419 use gpui::{ReadGlobal, TestAppContext};
420
421 use super::*;
422
423 #[gpui::test]
424 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
425 let fs = fs::FakeFs::new(cx.executor().clone());
426 fs.create_dir(paths::settings_file().parent().unwrap())
427 .await
428 .unwrap();
429
430 cx.update(|cx| {
431 let test_settings = settings::SettingsStore::test(cx);
432 cx.set_global(test_settings);
433 AssistantSettings::register(cx);
434 });
435
436 cx.update(|cx| {
437 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
438 assert_eq!(
439 AssistantSettings::get_global(cx).default_model,
440 LanguageModelSelection {
441 provider: "zed.dev".into(),
442 model: "claude-3-5-sonnet".into(),
443 }
444 );
445 });
446
447 cx.update(|cx| {
448 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
449 fs.clone(),
450 |settings, _| {
451 *settings = AssistantSettingsContent::Versioned(
452 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
453 default_model: Some(LanguageModelSelection {
454 provider: "test-provider".into(),
455 model: "gpt-99".into(),
456 }),
457 inline_alternatives: None,
458 enabled: None,
459 button: None,
460 dock: None,
461 default_width: None,
462 default_height: None,
463 enable_experimental_live_diffs: None,
464 }),
465 )
466 },
467 );
468 });
469
470 cx.run_until_parked();
471
472 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
473 assert!(raw_settings_value.contains(r#""version": "2""#));
474
475 #[derive(Debug, Deserialize)]
476 struct AssistantSettingsTest {
477 assistant: AssistantSettingsContent,
478 }
479
480 let assistant_settings: AssistantSettingsTest =
481 serde_json_lenient::from_str(&raw_settings_value).unwrap();
482
483 assert!(!assistant_settings.assistant.is_version_outdated());
484 }
485}