1use std::fmt;
2
3pub use anthropic::Model as AnthropicModel;
4use gpui::Pixels;
5pub use open_ai::Model as OpenAiModel;
6use schemars::{
7 schema::{InstanceType, Metadata, Schema, SchemaObject},
8 JsonSchema,
9};
10use serde::{
11 de::{self, Visitor},
12 Deserialize, Deserializer, Serialize, Serializer,
13};
14use settings::{Settings, SettingsSources};
15
16#[derive(Clone, Debug, Default, PartialEq)]
17pub enum ZedDotDevModel {
18 Gpt3Point5Turbo,
19 Gpt4,
20 Gpt4Turbo,
21 #[default]
22 Gpt4Omni,
23 Claude3Opus,
24 Claude3Sonnet,
25 Claude3Haiku,
26 Custom(String),
27}
28
29impl Serialize for ZedDotDevModel {
30 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
31 where
32 S: Serializer,
33 {
34 serializer.serialize_str(self.id())
35 }
36}
37
38impl<'de> Deserialize<'de> for ZedDotDevModel {
39 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40 where
41 D: Deserializer<'de>,
42 {
43 struct ZedDotDevModelVisitor;
44
45 impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
46 type Value = ZedDotDevModel;
47
48 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
49 formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
50 }
51
52 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
53 where
54 E: de::Error,
55 {
56 match value {
57 "gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
58 "gpt-4" => Ok(ZedDotDevModel::Gpt4),
59 "gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
60 "gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
61 _ => Ok(ZedDotDevModel::Custom(value.to_owned())),
62 }
63 }
64 }
65
66 deserializer.deserialize_str(ZedDotDevModelVisitor)
67 }
68}
69
70impl JsonSchema for ZedDotDevModel {
71 fn schema_name() -> String {
72 "ZedDotDevModel".to_owned()
73 }
74
75 fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
76 let variants = vec![
77 "gpt-3.5-turbo".to_owned(),
78 "gpt-4".to_owned(),
79 "gpt-4-turbo-preview".to_owned(),
80 "gpt-4o".to_owned(),
81 ];
82 Schema::Object(SchemaObject {
83 instance_type: Some(InstanceType::String.into()),
84 enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
85 metadata: Some(Box::new(Metadata {
86 title: Some("ZedDotDevModel".to_owned()),
87 default: Some(serde_json::json!("gpt-4-turbo-preview")),
88 examples: vec![
89 serde_json::json!("gpt-3.5-turbo"),
90 serde_json::json!("gpt-4"),
91 serde_json::json!("gpt-4-turbo-preview"),
92 serde_json::json!("custom-model-name"),
93 ],
94 ..Default::default()
95 })),
96 ..Default::default()
97 })
98 }
99}
100
101impl ZedDotDevModel {
102 pub fn id(&self) -> &str {
103 match self {
104 Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
105 Self::Gpt4 => "gpt-4",
106 Self::Gpt4Turbo => "gpt-4-turbo-preview",
107 Self::Gpt4Omni => "gpt-4o",
108 Self::Claude3Opus => "claude-3-opus",
109 Self::Claude3Sonnet => "claude-3-sonnet",
110 Self::Claude3Haiku => "claude-3-haiku",
111 Self::Custom(id) => id,
112 }
113 }
114
115 pub fn display_name(&self) -> &str {
116 match self {
117 Self::Gpt3Point5Turbo => "GPT 3.5 Turbo",
118 Self::Gpt4 => "GPT 4",
119 Self::Gpt4Turbo => "GPT 4 Turbo",
120 Self::Gpt4Omni => "GPT 4 Omni",
121 Self::Claude3Opus => "Claude 3 Opus",
122 Self::Claude3Sonnet => "Claude 3 Sonnet",
123 Self::Claude3Haiku => "Claude 3 Haiku",
124 Self::Custom(id) => id.as_str(),
125 }
126 }
127
128 pub fn max_token_count(&self) -> usize {
129 match self {
130 Self::Gpt3Point5Turbo => 2048,
131 Self::Gpt4 => 4096,
132 Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
133 Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
134 Self::Custom(_) => 4096, // TODO: Make this configurable
135 }
136 }
137}
138
139#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
140#[serde(rename_all = "snake_case")]
141pub enum AssistantDockPosition {
142 Left,
143 #[default]
144 Right,
145 Bottom,
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
149#[serde(tag = "name", rename_all = "snake_case")]
150pub enum AssistantProvider {
151 #[serde(rename = "zed.dev")]
152 ZedDotDev {
153 #[serde(default)]
154 default_model: ZedDotDevModel,
155 },
156 #[serde(rename = "openai")]
157 OpenAi {
158 #[serde(default)]
159 default_model: OpenAiModel,
160 #[serde(default = "open_ai_url")]
161 api_url: String,
162 #[serde(default)]
163 low_speed_timeout_in_seconds: Option<u64>,
164 },
165 #[serde(rename = "anthropic")]
166 Anthropic {
167 #[serde(default)]
168 default_model: AnthropicModel,
169 #[serde(default = "anthropic_api_url")]
170 api_url: String,
171 #[serde(default)]
172 low_speed_timeout_in_seconds: Option<u64>,
173 },
174}
175
176impl Default for AssistantProvider {
177 fn default() -> Self {
178 Self::ZedDotDev {
179 default_model: ZedDotDevModel::default(),
180 }
181 }
182}
183
184fn open_ai_url() -> String {
185 open_ai::OPEN_AI_API_URL.to_string()
186}
187
188fn anthropic_api_url() -> String {
189 anthropic::ANTHROPIC_API_URL.to_string()
190}
191
192#[derive(Default, Debug, Deserialize, Serialize)]
193pub struct AssistantSettings {
194 pub enabled: bool,
195 pub button: bool,
196 pub dock: AssistantDockPosition,
197 pub default_width: Pixels,
198 pub default_height: Pixels,
199 pub provider: AssistantProvider,
200}
201
202/// Assistant panel settings
203#[derive(Clone, Serialize, Deserialize, Debug)]
204#[serde(untagged)]
205pub enum AssistantSettingsContent {
206 Versioned(VersionedAssistantSettingsContent),
207 Legacy(LegacyAssistantSettingsContent),
208}
209
210impl JsonSchema for AssistantSettingsContent {
211 fn schema_name() -> String {
212 VersionedAssistantSettingsContent::schema_name()
213 }
214
215 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
216 VersionedAssistantSettingsContent::json_schema(gen)
217 }
218
219 fn is_referenceable() -> bool {
220 VersionedAssistantSettingsContent::is_referenceable()
221 }
222}
223
224impl Default for AssistantSettingsContent {
225 fn default() -> Self {
226 Self::Versioned(VersionedAssistantSettingsContent::default())
227 }
228}
229
230impl AssistantSettingsContent {
231 fn upgrade(&self) -> AssistantSettingsContentV1 {
232 match self {
233 AssistantSettingsContent::Versioned(settings) => match settings {
234 VersionedAssistantSettingsContent::V1(settings) => settings.clone(),
235 },
236 AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV1 {
237 enabled: None,
238 button: settings.button,
239 dock: settings.dock,
240 default_width: settings.default_width,
241 default_height: settings.default_height,
242 provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
243 Some(AssistantProvider::OpenAi {
244 default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
245 api_url: open_ai_api_url.clone(),
246 low_speed_timeout_in_seconds: None,
247 })
248 } else {
249 settings.default_open_ai_model.clone().map(|open_ai_model| {
250 AssistantProvider::OpenAi {
251 default_model: open_ai_model,
252 api_url: open_ai_url(),
253 low_speed_timeout_in_seconds: None,
254 }
255 })
256 },
257 },
258 }
259 }
260
261 pub fn set_dock(&mut self, dock: AssistantDockPosition) {
262 match self {
263 AssistantSettingsContent::Versioned(settings) => match settings {
264 VersionedAssistantSettingsContent::V1(settings) => {
265 settings.dock = Some(dock);
266 }
267 },
268 AssistantSettingsContent::Legacy(settings) => {
269 settings.dock = Some(dock);
270 }
271 }
272 }
273}
274
275#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
276#[serde(tag = "version")]
277pub enum VersionedAssistantSettingsContent {
278 #[serde(rename = "1")]
279 V1(AssistantSettingsContentV1),
280}
281
282impl Default for VersionedAssistantSettingsContent {
283 fn default() -> Self {
284 Self::V1(AssistantSettingsContentV1 {
285 enabled: None,
286 button: None,
287 dock: None,
288 default_width: None,
289 default_height: None,
290 provider: None,
291 })
292 }
293}
294
295#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
296pub struct AssistantSettingsContentV1 {
297 /// Whether the Assistant is enabled.
298 ///
299 /// Default: true
300 enabled: Option<bool>,
301 /// Whether to show the assistant panel button in the status bar.
302 ///
303 /// Default: true
304 button: Option<bool>,
305 /// Where to dock the assistant.
306 ///
307 /// Default: right
308 dock: Option<AssistantDockPosition>,
309 /// Default width in pixels when the assistant is docked to the left or right.
310 ///
311 /// Default: 640
312 default_width: Option<f32>,
313 /// Default height in pixels when the assistant is docked to the bottom.
314 ///
315 /// Default: 320
316 default_height: Option<f32>,
317 /// The provider of the assistant service.
318 ///
319 /// This can either be the internal `zed.dev` service or an external `openai` service,
320 /// each with their respective default models and configurations.
321 provider: Option<AssistantProvider>,
322}
323
324#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
325pub struct LegacyAssistantSettingsContent {
326 /// Whether to show the assistant panel button in the status bar.
327 ///
328 /// Default: true
329 pub button: Option<bool>,
330 /// Where to dock the assistant.
331 ///
332 /// Default: right
333 pub dock: Option<AssistantDockPosition>,
334 /// Default width in pixels when the assistant is docked to the left or right.
335 ///
336 /// Default: 640
337 pub default_width: Option<f32>,
338 /// Default height in pixels when the assistant is docked to the bottom.
339 ///
340 /// Default: 320
341 pub default_height: Option<f32>,
342 /// The default OpenAI model to use when creating new contexts.
343 ///
344 /// Default: gpt-4-1106-preview
345 pub default_open_ai_model: Option<OpenAiModel>,
346 /// OpenAI API base URL to use when creating new contexts.
347 ///
348 /// Default: https://api.openai.com/v1
349 pub openai_api_url: Option<String>,
350}
351
352impl Settings for AssistantSettings {
353 const KEY: Option<&'static str> = Some("assistant");
354
355 type FileContent = AssistantSettingsContent;
356
357 fn load(
358 sources: SettingsSources<Self::FileContent>,
359 _: &mut gpui::AppContext,
360 ) -> anyhow::Result<Self> {
361 let mut settings = AssistantSettings::default();
362
363 for value in sources.defaults_and_customizations() {
364 let value = value.upgrade();
365 merge(&mut settings.enabled, value.enabled);
366 merge(&mut settings.button, value.button);
367 merge(&mut settings.dock, value.dock);
368 merge(
369 &mut settings.default_width,
370 value.default_width.map(Into::into),
371 );
372 merge(
373 &mut settings.default_height,
374 value.default_height.map(Into::into),
375 );
376 if let Some(provider) = value.provider.clone() {
377 match (&mut settings.provider, provider) {
378 (
379 AssistantProvider::ZedDotDev { default_model },
380 AssistantProvider::ZedDotDev {
381 default_model: default_model_override,
382 },
383 ) => {
384 *default_model = default_model_override;
385 }
386 (
387 AssistantProvider::OpenAi {
388 default_model,
389 api_url,
390 low_speed_timeout_in_seconds,
391 },
392 AssistantProvider::OpenAi {
393 default_model: default_model_override,
394 api_url: api_url_override,
395 low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
396 },
397 ) => {
398 *default_model = default_model_override;
399 *api_url = api_url_override;
400 *low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_override;
401 }
402 (merged, provider_override) => {
403 *merged = provider_override;
404 }
405 }
406 }
407 }
408
409 Ok(settings)
410 }
411}
412
413fn merge<T: Copy>(target: &mut T, value: Option<T>) {
414 if let Some(value) = value {
415 *target = value;
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use gpui::{AppContext, UpdateGlobal};
422 use settings::SettingsStore;
423
424 use super::*;
425
426 #[gpui::test]
427 fn test_deserialize_assistant_settings(cx: &mut AppContext) {
428 let store = settings::SettingsStore::test(cx);
429 cx.set_global(store);
430
431 // Settings default to gpt-4-turbo.
432 AssistantSettings::register(cx);
433 assert_eq!(
434 AssistantSettings::get_global(cx).provider,
435 AssistantProvider::OpenAi {
436 default_model: OpenAiModel::FourOmni,
437 api_url: open_ai_url(),
438 low_speed_timeout_in_seconds: None,
439 }
440 );
441
442 // Ensure backward-compatibility.
443 SettingsStore::update_global(cx, |store, cx| {
444 store
445 .set_user_settings(
446 r#"{
447 "assistant": {
448 "openai_api_url": "test-url",
449 }
450 }"#,
451 cx,
452 )
453 .unwrap();
454 });
455 assert_eq!(
456 AssistantSettings::get_global(cx).provider,
457 AssistantProvider::OpenAi {
458 default_model: OpenAiModel::FourOmni,
459 api_url: "test-url".into(),
460 low_speed_timeout_in_seconds: None,
461 }
462 );
463 SettingsStore::update_global(cx, |store, cx| {
464 store
465 .set_user_settings(
466 r#"{
467 "assistant": {
468 "default_open_ai_model": "gpt-4-0613"
469 }
470 }"#,
471 cx,
472 )
473 .unwrap();
474 });
475 assert_eq!(
476 AssistantSettings::get_global(cx).provider,
477 AssistantProvider::OpenAi {
478 default_model: OpenAiModel::Four,
479 api_url: open_ai_url(),
480 low_speed_timeout_in_seconds: None,
481 }
482 );
483
484 // The new version supports setting a custom model when using zed.dev.
485 SettingsStore::update_global(cx, |store, cx| {
486 store
487 .set_user_settings(
488 r#"{
489 "assistant": {
490 "version": "1",
491 "provider": {
492 "name": "zed.dev",
493 "default_model": "custom"
494 }
495 }
496 }"#,
497 cx,
498 )
499 .unwrap();
500 });
501 assert_eq!(
502 AssistantSettings::get_global(cx).provider,
503 AssistantProvider::ZedDotDev {
504 default_model: ZedDotDevModel::Custom("custom".into())
505 }
506 );
507 }
508}