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::SingleLineInput;
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<SingleLineInput>,
37 api_url: Entity<SingleLineInput>,
38 api_key: Entity<SingleLineInput>,
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<SingleLineInput>,
80 max_completion_tokens: Entity<SingleLineInput>,
81 max_output_tokens: Entity<SingleLineInput>,
82 max_tokens: Entity<SingleLineInput>,
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<SingleLineInput> {
175 cx.new(|cx| {
176 let input = SingleLineInput::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 window,
488 cx,
489 )
490 .map(|kb| kb.size(rems_from_px(12.))),
491 )
492 .on_click(cx.listener(|this, _event, window, cx| {
493 this.cancel(&menu::Cancel, window, cx)
494 })),
495 )
496 .child(
497 Button::new("save-server", "Save Provider")
498 .key_binding(
499 KeyBinding::for_action_in(
500 &menu::Confirm,
501 &focus_handle,
502 window,
503 cx,
504 )
505 .map(|kb| kb.size(rems_from_px(12.))),
506 )
507 .on_click(cx.listener(|this, _event, window, cx| {
508 this.confirm(&menu::Confirm, window, cx)
509 })),
510 ),
511 ),
512 ),
513 )
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use editor::EditorSettings;
521 use fs::FakeFs;
522 use gpui::{TestAppContext, VisualTestContext};
523 use language::language_settings;
524 use language_model::{
525 LanguageModelProviderId, LanguageModelProviderName,
526 fake_provider::FakeLanguageModelProvider,
527 };
528 use project::Project;
529 use settings::{Settings as _, SettingsStore};
530 use util::path;
531
532 #[gpui::test]
533 async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
534 let cx = setup_test(cx).await;
535
536 assert_eq!(
537 save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await,
538 Some("Provider Name cannot be empty".into())
539 );
540
541 assert_eq!(
542 save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await,
543 Some("API URL cannot be empty".into())
544 );
545
546 assert_eq!(
547 save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await,
548 Some("API Key cannot be empty".into())
549 );
550
551 assert_eq!(
552 save_provider_validation_errors(
553 "someprovider",
554 "someurl",
555 "somekey",
556 vec![("", "200000", "200000", "32000")],
557 cx,
558 )
559 .await,
560 Some("Model Name cannot be empty".into())
561 );
562
563 assert_eq!(
564 save_provider_validation_errors(
565 "someprovider",
566 "someurl",
567 "somekey",
568 vec![("somemodel", "abc", "200000", "32000")],
569 cx,
570 )
571 .await,
572 Some("Max Tokens must be a number".into())
573 );
574
575 assert_eq!(
576 save_provider_validation_errors(
577 "someprovider",
578 "someurl",
579 "somekey",
580 vec![("somemodel", "200000", "abc", "32000")],
581 cx,
582 )
583 .await,
584 Some("Max Completion Tokens must be a number".into())
585 );
586
587 assert_eq!(
588 save_provider_validation_errors(
589 "someprovider",
590 "someurl",
591 "somekey",
592 vec![("somemodel", "200000", "200000", "abc")],
593 cx,
594 )
595 .await,
596 Some("Max Output Tokens must be a number".into())
597 );
598
599 assert_eq!(
600 save_provider_validation_errors(
601 "someprovider",
602 "someurl",
603 "somekey",
604 vec![
605 ("somemodel", "200000", "200000", "32000"),
606 ("somemodel", "200000", "200000", "32000"),
607 ],
608 cx,
609 )
610 .await,
611 Some("Model Names must be unique".into())
612 );
613 }
614
615 #[gpui::test]
616 async fn test_save_provider_name_conflict(cx: &mut TestAppContext) {
617 let cx = setup_test(cx).await;
618
619 cx.update(|_window, cx| {
620 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
621 registry.register_provider(
622 Arc::new(FakeLanguageModelProvider::new(
623 LanguageModelProviderId::new("someprovider"),
624 LanguageModelProviderName::new("Some Provider"),
625 )),
626 cx,
627 );
628 });
629 });
630
631 assert_eq!(
632 save_provider_validation_errors(
633 "someprovider",
634 "someurl",
635 "someapikey",
636 vec![("somemodel", "200000", "200000", "32000")],
637 cx,
638 )
639 .await,
640 Some("Provider Name is already taken by another provider".into())
641 );
642 }
643
644 #[gpui::test]
645 async fn test_model_input_default_capabilities(cx: &mut TestAppContext) {
646 let cx = setup_test(cx).await;
647
648 cx.update(|window, cx| {
649 let model_input = ModelInput::new(window, cx);
650 model_input.name.update(cx, |input, cx| {
651 input.editor().update(cx, |editor, cx| {
652 editor.set_text("somemodel", window, cx);
653 });
654 });
655 assert_eq!(
656 model_input.capabilities.supports_tools,
657 ToggleState::Selected
658 );
659 assert_eq!(
660 model_input.capabilities.supports_images,
661 ToggleState::Unselected
662 );
663 assert_eq!(
664 model_input.capabilities.supports_parallel_tool_calls,
665 ToggleState::Unselected
666 );
667 assert_eq!(
668 model_input.capabilities.supports_prompt_cache_key,
669 ToggleState::Unselected
670 );
671
672 let parsed_model = model_input.parse(cx).unwrap();
673 assert!(parsed_model.capabilities.tools);
674 assert!(!parsed_model.capabilities.images);
675 assert!(!parsed_model.capabilities.parallel_tool_calls);
676 assert!(!parsed_model.capabilities.prompt_cache_key);
677 });
678 }
679
680 #[gpui::test]
681 async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) {
682 let cx = setup_test(cx).await;
683
684 cx.update(|window, cx| {
685 let mut model_input = ModelInput::new(window, cx);
686 model_input.name.update(cx, |input, cx| {
687 input.editor().update(cx, |editor, cx| {
688 editor.set_text("somemodel", window, cx);
689 });
690 });
691
692 model_input.capabilities.supports_tools = ToggleState::Unselected;
693 model_input.capabilities.supports_images = ToggleState::Unselected;
694 model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected;
695 model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
696
697 let parsed_model = model_input.parse(cx).unwrap();
698 assert!(!parsed_model.capabilities.tools);
699 assert!(!parsed_model.capabilities.images);
700 assert!(!parsed_model.capabilities.parallel_tool_calls);
701 assert!(!parsed_model.capabilities.prompt_cache_key);
702 });
703 }
704
705 #[gpui::test]
706 async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) {
707 let cx = setup_test(cx).await;
708
709 cx.update(|window, cx| {
710 let mut model_input = ModelInput::new(window, cx);
711 model_input.name.update(cx, |input, cx| {
712 input.editor().update(cx, |editor, cx| {
713 editor.set_text("somemodel", window, cx);
714 });
715 });
716
717 model_input.capabilities.supports_tools = ToggleState::Selected;
718 model_input.capabilities.supports_images = ToggleState::Unselected;
719 model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected;
720 model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
721
722 let parsed_model = model_input.parse(cx).unwrap();
723 assert_eq!(parsed_model.name, "somemodel");
724 assert!(parsed_model.capabilities.tools);
725 assert!(!parsed_model.capabilities.images);
726 assert!(parsed_model.capabilities.parallel_tool_calls);
727 assert!(!parsed_model.capabilities.prompt_cache_key);
728 });
729 }
730
731 async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
732 cx.update(|cx| {
733 let store = SettingsStore::test(cx);
734 cx.set_global(store);
735 workspace::init_settings(cx);
736 Project::init_settings(cx);
737 theme::init(theme::LoadThemes::JustBase, cx);
738 language_settings::init(cx);
739 EditorSettings::register(cx);
740 language_model::init_settings(cx);
741 language_models::init_settings(cx);
742 });
743
744 let fs = FakeFs::new(cx.executor());
745 cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
746 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
747 let (_, cx) =
748 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
749
750 cx
751 }
752
753 async fn save_provider_validation_errors(
754 provider_name: &str,
755 api_url: &str,
756 api_key: &str,
757 models: Vec<(&str, &str, &str, &str)>,
758 cx: &mut VisualTestContext,
759 ) -> Option<SharedString> {
760 fn set_text(
761 input: &Entity<SingleLineInput>,
762 text: &str,
763 window: &mut Window,
764 cx: &mut App,
765 ) {
766 input.update(cx, |input, cx| {
767 input.editor().update(cx, |editor, cx| {
768 editor.set_text(text, window, cx);
769 });
770 });
771 }
772
773 let task = cx.update(|window, cx| {
774 let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx);
775 set_text(&input.provider_name, provider_name, window, cx);
776 set_text(&input.api_url, api_url, window, cx);
777 set_text(&input.api_key, api_key, window, cx);
778
779 for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in
780 models.iter().enumerate()
781 {
782 if i >= input.models.len() {
783 input.models.push(ModelInput::new(window, cx));
784 }
785 let model = &mut input.models[i];
786 set_text(&model.name, name, window, cx);
787 set_text(&model.max_tokens, max_tokens, window, cx);
788 set_text(
789 &model.max_completion_tokens,
790 max_completion_tokens,
791 window,
792 cx,
793 );
794 set_text(&model.max_output_tokens, max_output_tokens, window, cx);
795 }
796 save_provider_to_settings(&input, cx)
797 });
798
799 task.await.err()
800 }
801}