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