1use std::sync::Arc;
2
3use anyhow::Result;
4use collections::HashSet;
5use fs::Fs;
6use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
7use language_model::LanguageModelRegistry;
8use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
9use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
10use ui::{
11 Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
12};
13use ui_input::InputField;
14use workspace::{ModalView, Workspace};
15
16#[derive(Clone, Copy)]
17pub enum LlmCompatibleProvider {
18 OpenAi,
19}
20
21impl LlmCompatibleProvider {
22 fn name(&self) -> &'static str {
23 match self {
24 LlmCompatibleProvider::OpenAi => "OpenAI",
25 }
26 }
27
28 fn api_url(&self) -> &'static str {
29 match self {
30 LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1",
31 }
32 }
33}
34
35struct AddLlmProviderInput {
36 provider_name: Entity<InputField>,
37 api_url: Entity<InputField>,
38 api_key: Entity<InputField>,
39 models: Vec<ModelInput>,
40}
41
42impl AddLlmProviderInput {
43 fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
44 let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
45 let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
46 let api_key = single_line_input(
47 "API Key",
48 "000000000000000000000000000000000000000000000000",
49 None,
50 window,
51 cx,
52 );
53
54 Self {
55 provider_name,
56 api_url,
57 api_key,
58 models: vec![ModelInput::new(window, cx)],
59 }
60 }
61
62 fn add_model(&mut self, window: &mut Window, cx: &mut App) {
63 self.models.push(ModelInput::new(window, cx));
64 }
65
66 fn remove_model(&mut self, index: usize) {
67 self.models.remove(index);
68 }
69}
70
71struct ModelCapabilityToggles {
72 pub supports_tools: ToggleState,
73 pub supports_images: ToggleState,
74 pub supports_parallel_tool_calls: ToggleState,
75 pub supports_prompt_cache_key: ToggleState,
76}
77
78struct ModelInput {
79 name: Entity<InputField>,
80 max_completion_tokens: Entity<InputField>,
81 max_output_tokens: Entity<InputField>,
82 max_tokens: Entity<InputField>,
83 capabilities: ModelCapabilityToggles,
84}
85
86impl ModelInput {
87 fn new(window: &mut Window, cx: &mut App) -> Self {
88 let model_name = single_line_input(
89 "Model Name",
90 "e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
91 None,
92 window,
93 cx,
94 );
95 let max_completion_tokens = single_line_input(
96 "Max Completion Tokens",
97 "200000",
98 Some("200000"),
99 window,
100 cx,
101 );
102 let max_output_tokens = single_line_input(
103 "Max Output Tokens",
104 "Max Output Tokens",
105 Some("32000"),
106 window,
107 cx,
108 );
109 let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
110 let ModelCapabilities {
111 tools,
112 images,
113 parallel_tool_calls,
114 prompt_cache_key,
115 } = ModelCapabilities::default();
116 Self {
117 name: model_name,
118 max_completion_tokens,
119 max_output_tokens,
120 max_tokens,
121 capabilities: ModelCapabilityToggles {
122 supports_tools: tools.into(),
123 supports_images: images.into(),
124 supports_parallel_tool_calls: parallel_tool_calls.into(),
125 supports_prompt_cache_key: prompt_cache_key.into(),
126 },
127 }
128 }
129
130 fn parse(&self, cx: &App) -> Result<AvailableModel, SharedString> {
131 let name = self.name.read(cx).text(cx);
132 if name.is_empty() {
133 return Err(SharedString::from("Model Name cannot be empty"));
134 }
135 Ok(AvailableModel {
136 name,
137 display_name: None,
138 max_completion_tokens: Some(
139 self.max_completion_tokens
140 .read(cx)
141 .text(cx)
142 .parse::<u64>()
143 .map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?,
144 ),
145 max_output_tokens: Some(
146 self.max_output_tokens
147 .read(cx)
148 .text(cx)
149 .parse::<u64>()
150 .map_err(|_| SharedString::from("Max Output Tokens must be a number"))?,
151 ),
152 max_tokens: self
153 .max_tokens
154 .read(cx)
155 .text(cx)
156 .parse::<u64>()
157 .map_err(|_| SharedString::from("Max Tokens must be a number"))?,
158 capabilities: ModelCapabilities {
159 tools: self.capabilities.supports_tools.selected(),
160 images: self.capabilities.supports_images.selected(),
161 parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(),
162 prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(),
163 },
164 })
165 }
166}
167
168fn single_line_input(
169 label: impl Into<SharedString>,
170 placeholder: impl Into<SharedString>,
171 text: Option<&str>,
172 window: &mut Window,
173 cx: &mut App,
174) -> Entity<InputField> {
175 cx.new(|cx| {
176 let input = InputField::new(window, cx, placeholder).label(label);
177 if let Some(text) = text {
178 input
179 .editor()
180 .update(cx, |editor, cx| editor.set_text(text, window, cx));
181 }
182 input
183 })
184}
185
186fn save_provider_to_settings(
187 input: &AddLlmProviderInput,
188 cx: &mut App,
189) -> Task<Result<(), SharedString>> {
190 let provider_name: Arc<str> = input.provider_name.read(cx).text(cx).into();
191 if provider_name.is_empty() {
192 return Task::ready(Err("Provider Name cannot be empty".into()));
193 }
194
195 if LanguageModelRegistry::read_global(cx)
196 .providers()
197 .iter()
198 .any(|provider| {
199 provider.id().0.as_ref() == provider_name.as_ref()
200 || provider.name().0.as_ref() == provider_name.as_ref()
201 })
202 {
203 return Task::ready(Err(
204 "Provider Name is already taken by another provider".into()
205 ));
206 }
207
208 let api_url = input.api_url.read(cx).text(cx);
209 if api_url.is_empty() {
210 return Task::ready(Err("API URL cannot be empty".into()));
211 }
212
213 let api_key = input.api_key.read(cx).text(cx);
214 if api_key.is_empty() {
215 return Task::ready(Err("API Key cannot be empty".into()));
216 }
217
218 let mut models = Vec::new();
219 let mut model_names: HashSet<String> = HashSet::default();
220 for model in &input.models {
221 match model.parse(cx) {
222 Ok(model) => {
223 if !model_names.insert(model.name.clone()) {
224 return Task::ready(Err("Model Names must be unique".into()));
225 }
226 models.push(model)
227 }
228 Err(err) => return Task::ready(Err(err)),
229 }
230 }
231
232 let fs = <dyn Fs>::global(cx);
233 let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes());
234 cx.spawn(async move |cx| {
235 task.await
236 .map_err(|_| "Failed to write API key to keychain")?;
237 cx.update(|cx| {
238 update_settings_file(fs, cx, |settings, _cx| {
239 settings
240 .language_models
241 .get_or_insert_default()
242 .openai_compatible
243 .get_or_insert_default()
244 .insert(
245 provider_name,
246 OpenAiCompatibleSettingsContent {
247 api_url,
248 available_models: models,
249 },
250 );
251 });
252 })
253 .ok();
254 Ok(())
255 })
256}
257
258pub struct AddLlmProviderModal {
259 provider: LlmCompatibleProvider,
260 input: AddLlmProviderInput,
261 focus_handle: FocusHandle,
262 last_error: Option<SharedString>,
263}
264
265impl AddLlmProviderModal {
266 pub fn toggle(
267 provider: LlmCompatibleProvider,
268 workspace: &mut Workspace,
269 window: &mut Window,
270 cx: &mut Context<Workspace>,
271 ) {
272 workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx));
273 }
274
275 fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context<Self>) -> Self {
276 Self {
277 input: AddLlmProviderInput::new(provider, window, cx),
278 provider,
279 last_error: None,
280 focus_handle: cx.focus_handle(),
281 }
282 }
283
284 fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context<Self>) {
285 let task = save_provider_to_settings(&self.input, cx);
286 cx.spawn(async move |this, cx| {
287 let result = task.await;
288 this.update(cx, |this, cx| match result {
289 Ok(_) => {
290 cx.emit(DismissEvent);
291 }
292 Err(error) => {
293 this.last_error = Some(error);
294 cx.notify();
295 }
296 })
297 })
298 .detach_and_log_err(cx);
299 }
300
301 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
302 cx.emit(DismissEvent);
303 }
304
305 fn render_model_section(&self, cx: &mut Context<Self>) -> impl IntoElement {
306 v_flex()
307 .mt_1()
308 .gap_2()
309 .child(
310 h_flex()
311 .justify_between()
312 .child(Label::new("Models").size(LabelSize::Small))
313 .child(
314 Button::new("add-model", "Add Model")
315 .icon(IconName::Plus)
316 .icon_position(IconPosition::Start)
317 .icon_size(IconSize::XSmall)
318 .icon_color(Color::Muted)
319 .label_size(LabelSize::Small)
320 .on_click(cx.listener(|this, _, window, cx| {
321 this.input.add_model(window, cx);
322 cx.notify();
323 })),
324 ),
325 )
326 .children(
327 self.input
328 .models
329 .iter()
330 .enumerate()
331 .map(|(ix, _)| self.render_model(ix, cx)),
332 )
333 }
334
335 fn render_model(&self, ix: usize, cx: &mut Context<Self>) -> impl IntoElement + use<> {
336 let has_more_than_one_model = self.input.models.len() > 1;
337 let model = &self.input.models[ix];
338
339 v_flex()
340 .p_2()
341 .gap_2()
342 .rounded_sm()
343 .border_1()
344 .border_dashed()
345 .border_color(cx.theme().colors().border.opacity(0.6))
346 .bg(cx.theme().colors().element_active.opacity(0.15))
347 .child(model.name.clone())
348 .child(
349 h_flex()
350 .gap_2()
351 .child(model.max_completion_tokens.clone())
352 .child(model.max_output_tokens.clone()),
353 )
354 .child(model.max_tokens.clone())
355 .child(
356 v_flex()
357 .gap_1()
358 .child(
359 Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools)
360 .label("Supports tools")
361 .on_click(cx.listener(move |this, checked, _window, cx| {
362 this.input.models[ix].capabilities.supports_tools = *checked;
363 cx.notify();
364 })),
365 )
366 .child(
367 Checkbox::new(("supports-images", ix), model.capabilities.supports_images)
368 .label("Supports images")
369 .on_click(cx.listener(move |this, checked, _window, cx| {
370 this.input.models[ix].capabilities.supports_images = *checked;
371 cx.notify();
372 })),
373 )
374 .child(
375 Checkbox::new(
376 ("supports-parallel-tool-calls", ix),
377 model.capabilities.supports_parallel_tool_calls,
378 )
379 .label("Supports parallel_tool_calls")
380 .on_click(cx.listener(
381 move |this, checked, _window, cx| {
382 this.input.models[ix]
383 .capabilities
384 .supports_parallel_tool_calls = *checked;
385 cx.notify();
386 },
387 )),
388 )
389 .child(
390 Checkbox::new(
391 ("supports-prompt-cache-key", ix),
392 model.capabilities.supports_prompt_cache_key,
393 )
394 .label("Supports prompt_cache_key")
395 .on_click(cx.listener(
396 move |this, checked, _window, cx| {
397 this.input.models[ix].capabilities.supports_prompt_cache_key =
398 *checked;
399 cx.notify();
400 },
401 )),
402 ),
403 )
404 .when(has_more_than_one_model, |this| {
405 this.child(
406 Button::new(("remove-model", ix), "Remove Model")
407 .icon(IconName::Trash)
408 .icon_position(IconPosition::Start)
409 .icon_size(IconSize::XSmall)
410 .icon_color(Color::Muted)
411 .label_size(LabelSize::Small)
412 .style(ButtonStyle::Outlined)
413 .full_width()
414 .on_click(cx.listener(move |this, _, _window, cx| {
415 this.input.remove_model(ix);
416 cx.notify();
417 })),
418 )
419 })
420 }
421}
422
423impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
424
425impl Focusable for AddLlmProviderModal {
426 fn focus_handle(&self, _cx: &App) -> FocusHandle {
427 self.focus_handle.clone()
428 }
429}
430
431impl ModalView for AddLlmProviderModal {}
432
433impl Render for AddLlmProviderModal {
434 fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
435 let focus_handle = self.focus_handle(cx);
436
437 div()
438 .id("add-llm-provider-modal")
439 .key_context("AddLlmProviderModal")
440 .w(rems(34.))
441 .elevation_3(cx)
442 .on_action(cx.listener(Self::cancel))
443 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
444 this.focus_handle(cx).focus(window);
445 }))
446 .child(
447 Modal::new("configure-context-server", None)
448 .header(ModalHeader::new().headline("Add LLM Provider").description(
449 match self.provider {
450 LlmCompatibleProvider::OpenAi => {
451 "This provider will use an OpenAI compatible API."
452 }
453 },
454 ))
455 .when_some(self.last_error.clone(), |this, error| {
456 this.section(
457 Section::new().child(
458 Banner::new()
459 .severity(Severity::Warning)
460 .child(div().text_xs().child(error)),
461 ),
462 )
463 })
464 .child(
465 v_flex()
466 .id("modal_content")
467 .size_full()
468 .max_h_128()
469 .overflow_y_scroll()
470 .px(DynamicSpacing::Base12.rems(cx))
471 .gap(DynamicSpacing::Base04.rems(cx))
472 .child(self.input.provider_name.clone())
473 .child(self.input.api_url.clone())
474 .child(self.input.api_key.clone())
475 .child(self.render_model_section(cx)),
476 )
477 .footer(
478 ModalFooter::new().end_slot(
479 h_flex()
480 .gap_1()
481 .child(
482 Button::new("cancel", "Cancel")
483 .key_binding(
484 KeyBinding::for_action_in(
485 &menu::Cancel,
486 &focus_handle,
487 cx,
488 )
489 .map(|kb| kb.size(rems_from_px(12.))),
490 )
491 .on_click(cx.listener(|this, _event, window, cx| {
492 this.cancel(&menu::Cancel, window, cx)
493 })),
494 )
495 .child(
496 Button::new("save-server", "Save Provider")
497 .key_binding(
498 KeyBinding::for_action_in(
499 &menu::Confirm,
500 &focus_handle,
501 cx,
502 )
503 .map(|kb| kb.size(rems_from_px(12.))),
504 )
505 .on_click(cx.listener(|this, _event, window, cx| {
506 this.confirm(&menu::Confirm, window, cx)
507 })),
508 ),
509 ),
510 ),
511 )
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use editor::EditorSettings;
519 use fs::FakeFs;
520 use gpui::{TestAppContext, VisualTestContext};
521 use language::language_settings;
522 use language_model::{
523 LanguageModelProviderId, LanguageModelProviderName,
524 fake_provider::FakeLanguageModelProvider,
525 };
526 use project::Project;
527 use settings::{Settings as _, SettingsStore};
528 use util::path;
529
530 #[gpui::test]
531 async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
532 let cx = setup_test(cx).await;
533
534 assert_eq!(
535 save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await,
536 Some("Provider Name cannot be empty".into())
537 );
538
539 assert_eq!(
540 save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await,
541 Some("API URL cannot be empty".into())
542 );
543
544 assert_eq!(
545 save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await,
546 Some("API Key cannot be empty".into())
547 );
548
549 assert_eq!(
550 save_provider_validation_errors(
551 "someprovider",
552 "someurl",
553 "somekey",
554 vec![("", "200000", "200000", "32000")],
555 cx,
556 )
557 .await,
558 Some("Model Name cannot be empty".into())
559 );
560
561 assert_eq!(
562 save_provider_validation_errors(
563 "someprovider",
564 "someurl",
565 "somekey",
566 vec![("somemodel", "abc", "200000", "32000")],
567 cx,
568 )
569 .await,
570 Some("Max Tokens must be a number".into())
571 );
572
573 assert_eq!(
574 save_provider_validation_errors(
575 "someprovider",
576 "someurl",
577 "somekey",
578 vec![("somemodel", "200000", "abc", "32000")],
579 cx,
580 )
581 .await,
582 Some("Max Completion Tokens must be a number".into())
583 );
584
585 assert_eq!(
586 save_provider_validation_errors(
587 "someprovider",
588 "someurl",
589 "somekey",
590 vec![("somemodel", "200000", "200000", "abc")],
591 cx,
592 )
593 .await,
594 Some("Max Output Tokens must be a number".into())
595 );
596
597 assert_eq!(
598 save_provider_validation_errors(
599 "someprovider",
600 "someurl",
601 "somekey",
602 vec![
603 ("somemodel", "200000", "200000", "32000"),
604 ("somemodel", "200000", "200000", "32000"),
605 ],
606 cx,
607 )
608 .await,
609 Some("Model Names must be unique".into())
610 );
611 }
612
613 #[gpui::test]
614 async fn test_save_provider_name_conflict(cx: &mut TestAppContext) {
615 let cx = setup_test(cx).await;
616
617 cx.update(|_window, cx| {
618 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
619 registry.register_provider(
620 Arc::new(FakeLanguageModelProvider::new(
621 LanguageModelProviderId::new("someprovider"),
622 LanguageModelProviderName::new("Some Provider"),
623 )),
624 cx,
625 );
626 });
627 });
628
629 assert_eq!(
630 save_provider_validation_errors(
631 "someprovider",
632 "someurl",
633 "someapikey",
634 vec![("somemodel", "200000", "200000", "32000")],
635 cx,
636 )
637 .await,
638 Some("Provider Name is already taken by another provider".into())
639 );
640 }
641
642 #[gpui::test]
643 async fn test_model_input_default_capabilities(cx: &mut TestAppContext) {
644 let cx = setup_test(cx).await;
645
646 cx.update(|window, cx| {
647 let model_input = ModelInput::new(window, cx);
648 model_input.name.update(cx, |input, cx| {
649 input.editor().update(cx, |editor, cx| {
650 editor.set_text("somemodel", window, cx);
651 });
652 });
653 assert_eq!(
654 model_input.capabilities.supports_tools,
655 ToggleState::Selected
656 );
657 assert_eq!(
658 model_input.capabilities.supports_images,
659 ToggleState::Unselected
660 );
661 assert_eq!(
662 model_input.capabilities.supports_parallel_tool_calls,
663 ToggleState::Unselected
664 );
665 assert_eq!(
666 model_input.capabilities.supports_prompt_cache_key,
667 ToggleState::Unselected
668 );
669
670 let parsed_model = model_input.parse(cx).unwrap();
671 assert!(parsed_model.capabilities.tools);
672 assert!(!parsed_model.capabilities.images);
673 assert!(!parsed_model.capabilities.parallel_tool_calls);
674 assert!(!parsed_model.capabilities.prompt_cache_key);
675 });
676 }
677
678 #[gpui::test]
679 async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) {
680 let cx = setup_test(cx).await;
681
682 cx.update(|window, cx| {
683 let mut model_input = ModelInput::new(window, cx);
684 model_input.name.update(cx, |input, cx| {
685 input.editor().update(cx, |editor, cx| {
686 editor.set_text("somemodel", window, cx);
687 });
688 });
689
690 model_input.capabilities.supports_tools = ToggleState::Unselected;
691 model_input.capabilities.supports_images = ToggleState::Unselected;
692 model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected;
693 model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
694
695 let parsed_model = model_input.parse(cx).unwrap();
696 assert!(!parsed_model.capabilities.tools);
697 assert!(!parsed_model.capabilities.images);
698 assert!(!parsed_model.capabilities.parallel_tool_calls);
699 assert!(!parsed_model.capabilities.prompt_cache_key);
700 });
701 }
702
703 #[gpui::test]
704 async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) {
705 let cx = setup_test(cx).await;
706
707 cx.update(|window, cx| {
708 let mut model_input = ModelInput::new(window, cx);
709 model_input.name.update(cx, |input, cx| {
710 input.editor().update(cx, |editor, cx| {
711 editor.set_text("somemodel", window, cx);
712 });
713 });
714
715 model_input.capabilities.supports_tools = ToggleState::Selected;
716 model_input.capabilities.supports_images = ToggleState::Unselected;
717 model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected;
718 model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
719
720 let parsed_model = model_input.parse(cx).unwrap();
721 assert_eq!(parsed_model.name, "somemodel");
722 assert!(parsed_model.capabilities.tools);
723 assert!(!parsed_model.capabilities.images);
724 assert!(parsed_model.capabilities.parallel_tool_calls);
725 assert!(!parsed_model.capabilities.prompt_cache_key);
726 });
727 }
728
729 async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
730 cx.update(|cx| {
731 let store = SettingsStore::test(cx);
732 cx.set_global(store);
733 workspace::init_settings(cx);
734 Project::init_settings(cx);
735 theme::init(theme::LoadThemes::JustBase, cx);
736 language_settings::init(cx);
737 EditorSettings::register(cx);
738 language_model::init_settings(cx);
739 language_models::init_settings(cx);
740 });
741
742 let fs = FakeFs::new(cx.executor());
743 cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
744 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
745 let (_, cx) =
746 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
747
748 cx
749 }
750
751 async fn save_provider_validation_errors(
752 provider_name: &str,
753 api_url: &str,
754 api_key: &str,
755 models: Vec<(&str, &str, &str, &str)>,
756 cx: &mut VisualTestContext,
757 ) -> Option<SharedString> {
758 fn set_text(input: &Entity<InputField>, text: &str, window: &mut Window, cx: &mut App) {
759 input.update(cx, |input, cx| {
760 input.editor().update(cx, |editor, cx| {
761 editor.set_text(text, window, cx);
762 });
763 });
764 }
765
766 let task = cx.update(|window, cx| {
767 let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx);
768 set_text(&input.provider_name, provider_name, window, cx);
769 set_text(&input.api_url, api_url, window, cx);
770 set_text(&input.api_key, api_key, window, cx);
771
772 for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in
773 models.iter().enumerate()
774 {
775 if i >= input.models.len() {
776 input.models.push(ModelInput::new(window, cx));
777 }
778 let model = &mut input.models[i];
779 set_text(&model.name, name, window, cx);
780 set_text(&model.max_tokens, max_tokens, window, cx);
781 set_text(
782 &model.max_completion_tokens,
783 max_completion_tokens,
784 window,
785 cx,
786 );
787 set_text(&model.max_output_tokens, max_output_tokens, window, cx);
788 }
789 save_provider_to_settings(&input, cx)
790 });
791
792 task.await.err()
793 }
794}