Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:43:07.264Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

api_compatible.rs

299 lines · 11.0 KB · rust
1use std::sync::Arc;
2
3use ::util::ResultExt;
4use anyhow::Result;
5use convert_case::{Case, Casing};
6use credentials_provider::CredentialsProvider;
7use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, TaskExt, Window};
8use language_model::{ApiKeyState, AuthenticateError, EnvVar};
9use settings::SettingsStore;
10use ui::{ElevationIndex, Tooltip, prelude::*};
11use ui_input::InputField;
12
13pub trait ApiCompatibleProviderSettings: Clone + Default + PartialEq + 'static {
14    fn api_url(&self) -> &str;
15}
16
17pub struct ApiCompatibleProviderState<S: ApiCompatibleProviderSettings> {
18    id: Arc<str>,
19    pub api_key_state: ApiKeyState,
20    pub settings: S,
21    credentials_provider: Arc<dyn CredentialsProvider>,
22}
23
24impl<S: ApiCompatibleProviderSettings> ApiCompatibleProviderState<S> {
25    pub fn new(
26        id: Arc<str>,
27        credentials_provider: Arc<dyn CredentialsProvider>,
28        resolve_settings: for<'a> fn(&'a str, &'a App) -> Option<&'a S>,
29        cx: &mut App,
30    ) -> Entity<Self> {
31        let api_key_env_var_name: SharedString =
32            format!("{}_API_KEY", id).to_case(Case::UpperSnake).into();
33        cx.new(|cx| {
34            cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
35                let Some(settings) = resolve_settings(&this.id, cx).cloned() else {
36                    return;
37                };
38                this.update_settings(settings, cx);
39            })
40            .detach();
41
42            let settings = resolve_settings(&id, cx).cloned().unwrap_or_default();
43            Self {
44                id,
45                api_key_state: ApiKeyState::new(
46                    SharedString::new(settings.api_url()),
47                    EnvVar::new(api_key_env_var_name),
48                ),
49                settings,
50                credentials_provider,
51            }
52        })
53    }
54
55    pub fn is_authenticated(&self) -> bool {
56        self.api_key_state.has_key()
57    }
58
59    pub fn set_api_key(
60        &mut self,
61        api_key: Option<String>,
62        cx: &mut Context<Self>,
63    ) -> Task<Result<()>> {
64        let api_url = SharedString::new(self.settings.api_url());
65        self.api_key_state.store(
66            api_url,
67            api_key,
68            |this| &mut this.api_key_state,
69            self.credentials_provider.clone(),
70            cx,
71        )
72    }
73
74    pub fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
75        let api_url = SharedString::new(self.settings.api_url());
76        self.api_key_state.load_if_needed(
77            api_url,
78            |this| &mut this.api_key_state,
79            self.credentials_provider.clone(),
80            cx,
81        )
82    }
83
84    pub fn update_settings(&mut self, settings: S, cx: &mut Context<Self>) {
85        if self.settings != settings {
86            let api_url = SharedString::new(settings.api_url());
87            self.api_key_state.handle_url_change(
88                api_url,
89                |this| &mut this.api_key_state,
90                self.credentials_provider.clone(),
91                cx,
92            );
93            self.settings = settings;
94            cx.notify();
95        }
96    }
97}
98
99pub struct ApiCompatibleProviderConfigurationView<S: ApiCompatibleProviderSettings> {
100    api_key_editor: Entity<InputField>,
101    state: Entity<ApiCompatibleProviderState<S>>,
102    provider_name: &'static str,
103    load_credentials_task: Option<Task<()>>,
104}
105
106impl<S: ApiCompatibleProviderSettings> ApiCompatibleProviderConfigurationView<S> {
107    pub fn new(
108        state: Entity<ApiCompatibleProviderState<S>>,
109        provider_name: &'static str,
110        placeholder_text: &'static str,
111        window: &mut Window,
112        cx: &mut Context<Self>,
113    ) -> Self {
114        let api_key_editor = cx.new(|cx| InputField::new(window, cx, placeholder_text));
115
116        cx.observe(&state, |_, _, cx| {
117            cx.notify();
118        })
119        .detach();
120
121        let load_credentials_task = Some(cx.spawn_in(window, {
122            let state = state.clone();
123            async move |this, cx| {
124                let task = state.update(cx, |state, cx| state.authenticate(cx));
125                match task.await {
126                    Ok(()) | Err(AuthenticateError::CredentialsNotFound) => {}
127                    Err(error) => {
128                        log::error!(
129                            "Failed to load {provider_name}-compatible provider API credentials: {error}"
130                        );
131                    }
132                }
133                this.update(cx, |this, cx| {
134                    this.load_credentials_task = None;
135                    cx.notify();
136                })
137                .log_err();
138            }
139        }));
140
141        Self {
142            api_key_editor,
143            state,
144            provider_name,
145            load_credentials_task,
146        }
147    }
148
149    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
150        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
151        if api_key.is_empty() {
152            return;
153        }
154
155        // url changes can cause the editor to be displayed again
156        self.api_key_editor
157            .update(cx, |input, cx| input.set_text("", window, cx));
158
159        let state = self.state.clone();
160        cx.spawn_in(window, async move |_, cx| {
161            state
162                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
163                .await
164        })
165        .detach_and_log_err(cx);
166    }
167
168    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
169        self.api_key_editor
170            .update(cx, |input, cx| input.set_text("", window, cx));
171
172        let state = self.state.clone();
173        cx.spawn_in(window, async move |_, cx| {
174            state
175                .update(cx, |state, cx| state.set_api_key(None, cx))
176                .await
177        })
178        .detach_and_log_err(cx);
179    }
180
181    fn remove_provider(&mut self, _: &mut Window, cx: &mut Context<Self>) {
182        let id = self.state.read(cx).id.clone();
183        let fs = <dyn fs::Fs>::global(cx);
184        settings::update_settings_file(fs, cx, move |settings, _| {
185            let Some(language_models) = settings.language_models.as_mut() else {
186                return;
187            };
188
189            if let Some(providers) = language_models.openai_compatible.as_mut() {
190                providers.remove(id.as_ref());
191            }
192            if let Some(providers) = language_models.anthropic_compatible.as_mut() {
193                providers.remove(id.as_ref());
194            }
195        });
196    }
197
198    fn should_render_editor(&self, cx: &Context<Self>) -> bool {
199        !self.state.read(cx).is_authenticated()
200    }
201}
202
203impl<S: ApiCompatibleProviderSettings> Render for ApiCompatibleProviderConfigurationView<S> {
204    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
205        let state = self.state.read(cx);
206        let env_var_set = state.api_key_state.is_from_env_var();
207        let env_var_name = state.api_key_state.env_var_name();
208        let provider_name = self.provider_name;
209
210        let api_key_section = if self.should_render_editor(cx) {
211            v_flex()
212                .on_action(cx.listener(Self::save_api_key))
213                .child(Label::new(format!(
214                    "To use Omega Agent with an {provider_name}-compatible provider, you need to add an API key."
215                )))
216                .child(
217                    div()
218                        .pt(DynamicSpacing::Base04.rems(cx))
219                        .child(self.api_key_editor.clone()),
220                )
221                .child(
222                    Label::new(format!(
223                        "You can also set the {env_var_name} environment variable and restart Omega.",
224                    ))
225                    .size(LabelSize::Small)
226                    .color(Color::Muted),
227                )
228                .into_any()
229        } else {
230            h_flex()
231                .mt_1()
232                .p_1()
233                .justify_between()
234                .rounded_md()
235                .border_1()
236                .border_color(cx.theme().colors().border)
237                .bg(cx.theme().colors().background)
238                .child(
239                    h_flex()
240                        .flex_1()
241                        .min_w_0()
242                        .gap_1()
243                        .child(Icon::new(IconName::Check).color(Color::Success))
244                        .child(
245                            div().w_full().overflow_x_hidden().text_ellipsis().child(Label::new(
246                                if env_var_set {
247                                    format!("API key set in {env_var_name} environment variable")
248                                } else {
249                                    format!("API key configured for {}", state.settings.api_url())
250                                },
251                            )),
252                        ),
253                )
254                .child(
255                    h_flex().flex_shrink_0().child(
256                        Button::new("reset-api-key", "Reset API Key")
257                            .label_size(LabelSize::Small)
258                            .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
259                            .layer(ElevationIndex::ModalSurface)
260                            .when(env_var_set, |this| {
261                                this.tooltip(Tooltip::text(format!(
262                                    "To reset your API key, unset the {env_var_name} environment variable.",
263                                )))
264                            })
265                            .on_click(cx.listener(|this, _, window, cx| {
266                                this.reset_api_key(window, cx)
267                            })),
268                    ),
269                )
270                .into_any()
271        };
272
273        if self.load_credentials_task.is_some() {
274            div().child(Label::new("Loading credentials…")).into_any()
275        } else {
276            v_flex()
277                .size_full()
278                .gap_4()
279                .child(api_key_section)
280                .child(
281                    h_flex().w_full().justify_end().child(
282                        Button::new("remove-compatible-provider", "Remove Provider")
283                            .style(ButtonStyle::OutlinedGhost)
284                            .label_size(LabelSize::Small)
285                            .start_icon(
286                                Icon::new(IconName::Trash)
287                                    .size(IconSize::Small)
288                                    .color(Color::Muted),
289                            )
290                            .on_click(cx.listener(|this, _event, window, cx| {
291                                this.remove_provider(window, cx)
292                            })),
293                    ),
294                )
295                .into_any()
296        }
297    }
298}
299
Served at tenant.openagents/omega Member data and write actions are omitted.