add_llm_provider_modal.rs

  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                    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}