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