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