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_dock(&mut self, dock: AssistantDockPosition) {
161 match self {
162 AssistantSettingsContent::Versioned(settings) => match settings {
163 VersionedAssistantSettingsContent::V1(settings) => {
164 settings.dock = Some(dock);
165 }
166 VersionedAssistantSettingsContent::V2(settings) => {
167 settings.dock = Some(dock);
168 }
169 },
170 AssistantSettingsContent::Legacy(settings) => {
171 settings.dock = Some(dock);
172 }
173 }
174 }
175
176 pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
177 let model = language_model.id().0.to_string();
178 let provider = language_model.provider_id().0.to_string();
179
180 match self {
181 AssistantSettingsContent::Versioned(settings) => match settings {
182 VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
183 "zed.dev" => {
184 log::warn!("attempted to set zed.dev model on outdated settings");
185 }
186 "anthropic" => {
187 let api_url = match &settings.provider {
188 Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
189 api_url.clone()
190 }
191 _ => None,
192 };
193 settings.provider = Some(AssistantProviderContentV1::Anthropic {
194 default_model: AnthropicModel::from_id(&model).ok(),
195 api_url,
196 });
197 }
198 "ollama" => {
199 let api_url = match &settings.provider {
200 Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
201 api_url.clone()
202 }
203 _ => None,
204 };
205 settings.provider = Some(AssistantProviderContentV1::Ollama {
206 default_model: Some(ollama::Model::new(&model, None, None)),
207 api_url,
208 });
209 }
210 "openai" => {
211 let (api_url, available_models) = match &settings.provider {
212 Some(AssistantProviderContentV1::OpenAi {
213 api_url,
214 available_models,
215 ..
216 }) => (api_url.clone(), available_models.clone()),
217 _ => (None, None),
218 };
219 settings.provider = Some(AssistantProviderContentV1::OpenAi {
220 default_model: OpenAiModel::from_id(&model).ok(),
221 api_url,
222 available_models,
223 });
224 }
225 _ => {}
226 },
227 VersionedAssistantSettingsContent::V2(settings) => {
228 settings.default_model = Some(LanguageModelSelection { provider, model });
229 }
230 },
231 AssistantSettingsContent::Legacy(settings) => {
232 if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
233 settings.default_open_ai_model = Some(model);
234 }
235 }
236 }
237 }
238}
239
240#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
241#[serde(tag = "version")]
242pub enum VersionedAssistantSettingsContent {
243 #[serde(rename = "1")]
244 V1(AssistantSettingsContentV1),
245 #[serde(rename = "2")]
246 V2(AssistantSettingsContentV2),
247}
248
249impl Default for VersionedAssistantSettingsContent {
250 fn default() -> Self {
251 Self::V2(AssistantSettingsContentV2 {
252 enabled: None,
253 button: None,
254 dock: None,
255 default_width: None,
256 default_height: None,
257 default_model: None,
258 inline_alternatives: None,
259 enable_experimental_live_diffs: None,
260 })
261 }
262}
263
264#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
265pub struct AssistantSettingsContentV2 {
266 /// Whether the Assistant is enabled.
267 ///
268 /// Default: true
269 enabled: Option<bool>,
270 /// Whether to show the assistant panel button in the status bar.
271 ///
272 /// Default: true
273 button: Option<bool>,
274 /// Where to dock the assistant.
275 ///
276 /// Default: right
277 dock: Option<AssistantDockPosition>,
278 /// Default width in pixels when the assistant is docked to the left or right.
279 ///
280 /// Default: 640
281 default_width: Option<f32>,
282 /// Default height in pixels when the assistant is docked to the bottom.
283 ///
284 /// Default: 320
285 default_height: Option<f32>,
286 /// The default model to use when creating new chats.
287 default_model: Option<LanguageModelSelection>,
288 /// Additional models with which to generate alternatives when performing inline assists.
289 inline_alternatives: Option<Vec<LanguageModelSelection>>,
290 /// Enable experimental live diffs in the assistant panel.
291 ///
292 /// Default: false
293 enable_experimental_live_diffs: Option<bool>,
294}
295
296#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
297pub struct LanguageModelSelection {
298 #[schemars(schema_with = "providers_schema")]
299 pub provider: String,
300 pub model: String,
301}
302
303fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
304 schemars::schema::SchemaObject {
305 enum_values: Some(vec![
306 "anthropic".into(),
307 "google".into(),
308 "ollama".into(),
309 "openai".into(),
310 "zed.dev".into(),
311 "copilot_chat".into(),
312 ]),
313 ..Default::default()
314 }
315 .into()
316}
317
318impl Default for LanguageModelSelection {
319 fn default() -> Self {
320 Self {
321 provider: "openai".to_string(),
322 model: "gpt-4".to_string(),
323 }
324 }
325}
326
327#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
328pub struct AssistantSettingsContentV1 {
329 /// Whether the Assistant is enabled.
330 ///
331 /// Default: true
332 enabled: Option<bool>,
333 /// Whether to show the assistant panel button in the status bar.
334 ///
335 /// Default: true
336 button: Option<bool>,
337 /// Where to dock the assistant.
338 ///
339 /// Default: right
340 dock: Option<AssistantDockPosition>,
341 /// Default width in pixels when the assistant is docked to the left or right.
342 ///
343 /// Default: 640
344 default_width: Option<f32>,
345 /// Default height in pixels when the assistant is docked to the bottom.
346 ///
347 /// Default: 320
348 default_height: Option<f32>,
349 /// The provider of the assistant service.
350 ///
351 /// This can be "openai", "anthropic", "ollama", "zed.dev"
352 /// each with their respective default models and configurations.
353 provider: Option<AssistantProviderContentV1>,
354}
355
356#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
357pub struct LegacyAssistantSettingsContent {
358 /// Whether to show the assistant panel button in the status bar.
359 ///
360 /// Default: true
361 pub button: Option<bool>,
362 /// Where to dock the assistant.
363 ///
364 /// Default: right
365 pub dock: Option<AssistantDockPosition>,
366 /// Default width in pixels when the assistant is docked to the left or right.
367 ///
368 /// Default: 640
369 pub default_width: Option<f32>,
370 /// Default height in pixels when the assistant is docked to the bottom.
371 ///
372 /// Default: 320
373 pub default_height: Option<f32>,
374 /// The default OpenAI model to use when creating new chats.
375 ///
376 /// Default: gpt-4-1106-preview
377 pub default_open_ai_model: Option<OpenAiModel>,
378 /// OpenAI API base URL to use when creating new chats.
379 ///
380 /// Default: https://api.openai.com/v1
381 pub openai_api_url: Option<String>,
382}
383
384impl Settings for AssistantSettings {
385 const KEY: Option<&'static str> = Some("assistant");
386
387 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
388
389 type FileContent = AssistantSettingsContent;
390
391 fn load(
392 sources: SettingsSources<Self::FileContent>,
393 _: &mut gpui::AppContext,
394 ) -> anyhow::Result<Self> {
395 let mut settings = AssistantSettings::default();
396
397 for value in sources.defaults_and_customizations() {
398 if value.is_version_outdated() {
399 settings.using_outdated_settings_version = true;
400 }
401
402 let value = value.upgrade();
403 merge(&mut settings.enabled, value.enabled);
404 merge(&mut settings.button, value.button);
405 merge(&mut settings.dock, value.dock);
406 merge(
407 &mut settings.default_width,
408 value.default_width.map(Into::into),
409 );
410 merge(
411 &mut settings.default_height,
412 value.default_height.map(Into::into),
413 );
414 merge(&mut settings.default_model, value.default_model);
415 merge(&mut settings.inline_alternatives, value.inline_alternatives);
416 merge(
417 &mut settings.enable_experimental_live_diffs,
418 value.enable_experimental_live_diffs,
419 );
420 }
421
422 Ok(settings)
423 }
424}
425
426fn merge<T>(target: &mut T, value: Option<T>) {
427 if let Some(value) = value {
428 *target = value;
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use fs::Fs;
435 use gpui::{ReadGlobal, TestAppContext};
436
437 use super::*;
438
439 #[gpui::test]
440 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
441 let fs = fs::FakeFs::new(cx.executor().clone());
442 fs.create_dir(paths::settings_file().parent().unwrap())
443 .await
444 .unwrap();
445
446 cx.update(|cx| {
447 let test_settings = settings::SettingsStore::test(cx);
448 cx.set_global(test_settings);
449 AssistantSettings::register(cx);
450 });
451
452 cx.update(|cx| {
453 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
454 assert_eq!(
455 AssistantSettings::get_global(cx).default_model,
456 LanguageModelSelection {
457 provider: "zed.dev".into(),
458 model: "claude-3-5-sonnet".into(),
459 }
460 );
461 });
462
463 cx.update(|cx| {
464 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
465 fs.clone(),
466 |settings, _| {
467 *settings = AssistantSettingsContent::Versioned(
468 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
469 default_model: Some(LanguageModelSelection {
470 provider: "test-provider".into(),
471 model: "gpt-99".into(),
472 }),
473 inline_alternatives: None,
474 enabled: None,
475 button: None,
476 dock: None,
477 default_width: None,
478 default_height: None,
479 enable_experimental_live_diffs: None,
480 }),
481 )
482 },
483 );
484 });
485
486 cx.run_until_parked();
487
488 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
489 assert!(raw_settings_value.contains(r#""version": "2""#));
490
491 #[derive(Debug, Deserialize)]
492 struct AssistantSettingsTest {
493 assistant: AssistantSettingsContent,
494 }
495
496 let assistant_settings: AssistantSettingsTest =
497 serde_json_lenient::from_str(&raw_settings_value).unwrap();
498
499 assert!(!assistant_settings.assistant.is_version_outdated());
500 }
501}