Skip to repository content299 lines · 11.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:34:14.954Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
api_compatible.rs
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