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