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