1use anyhow::{Context as _, Result, anyhow};
2use collections::HashMap;
3use credentials_provider::CredentialsProvider;
4use editor::{Editor, EditorElement, EditorStyle};
5use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
6use gpui::{
7 AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
8};
9use http_client::HttpClient;
10use language_model::{
11 AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
12 LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
13 LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
14 LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
15 LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
16};
17use open_router::{
18 Model, ModelMode as OpenRouterModelMode, ResponseStreamEvent, list_models, stream_completion,
19};
20use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore};
21use std::pin::Pin;
22use std::str::FromStr as _;
23use std::sync::Arc;
24use theme::ThemeSettings;
25use ui::{Icon, IconName, List, Tooltip, prelude::*};
26use util::ResultExt;
27
28use crate::{AllLanguageModelSettings, ui::InstructionListItem};
29
30const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
31const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
32
33#[derive(Default, Clone, Debug, PartialEq)]
34pub struct OpenRouterSettings {
35 pub api_url: String,
36 pub available_models: Vec<AvailableModel>,
37}
38
39pub struct OpenRouterLanguageModelProvider {
40 http_client: Arc<dyn HttpClient>,
41 state: gpui::Entity<State>,
42}
43
44pub struct State {
45 api_key: Option<String>,
46 api_key_from_env: bool,
47 http_client: Arc<dyn HttpClient>,
48 available_models: Vec<open_router::Model>,
49 fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
50 settings: OpenRouterSettings,
51 _subscription: Subscription,
52}
53
54const OPENROUTER_API_KEY_VAR: &str = "OPENROUTER_API_KEY";
55
56impl State {
57 fn is_authenticated(&self) -> bool {
58 self.api_key.is_some()
59 }
60
61 fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
62 let credentials_provider = <dyn CredentialsProvider>::global(cx);
63 let api_url = AllLanguageModelSettings::get_global(cx)
64 .open_router
65 .api_url
66 .clone();
67 cx.spawn(async move |this, cx| {
68 credentials_provider
69 .delete_credentials(&api_url, cx)
70 .await
71 .log_err();
72 this.update(cx, |this, cx| {
73 this.api_key = None;
74 this.api_key_from_env = false;
75 cx.notify();
76 })
77 })
78 }
79
80 fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
81 let credentials_provider = <dyn CredentialsProvider>::global(cx);
82 let api_url = AllLanguageModelSettings::get_global(cx)
83 .open_router
84 .api_url
85 .clone();
86 cx.spawn(async move |this, cx| {
87 credentials_provider
88 .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
89 .await
90 .log_err();
91 this.update(cx, |this, cx| {
92 this.api_key = Some(api_key);
93 this.restart_fetch_models_task(cx);
94 cx.notify();
95 })
96 })
97 }
98
99 fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
100 if self.is_authenticated() {
101 return Task::ready(Ok(()));
102 }
103
104 let credentials_provider = <dyn CredentialsProvider>::global(cx);
105 let api_url = AllLanguageModelSettings::get_global(cx)
106 .open_router
107 .api_url
108 .clone();
109
110 cx.spawn(async move |this, cx| {
111 let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENROUTER_API_KEY_VAR) {
112 (api_key, true)
113 } else {
114 let (_, api_key) = credentials_provider
115 .read_credentials(&api_url, cx)
116 .await?
117 .ok_or(AuthenticateError::CredentialsNotFound)?;
118 (
119 String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
120 false,
121 )
122 };
123
124 this.update(cx, |this, cx| {
125 this.api_key = Some(api_key);
126 this.api_key_from_env = from_env;
127 this.restart_fetch_models_task(cx);
128 cx.notify();
129 })?;
130
131 Ok(())
132 })
133 }
134
135 fn fetch_models(
136 &mut self,
137 cx: &mut Context<Self>,
138 ) -> Task<Result<(), LanguageModelCompletionError>> {
139 let settings = &AllLanguageModelSettings::get_global(cx).open_router;
140 let http_client = self.http_client.clone();
141 let api_url = settings.api_url.clone();
142 let Some(api_key) = self.api_key.clone() else {
143 return Task::ready(Err(LanguageModelCompletionError::NoApiKey {
144 provider: PROVIDER_NAME,
145 }));
146 };
147 cx.spawn(async move |this, cx| {
148 let models = list_models(http_client.as_ref(), &api_url, &api_key)
149 .await
150 .map_err(|e| {
151 LanguageModelCompletionError::Other(anyhow::anyhow!(
152 "OpenRouter error: {:?}",
153 e
154 ))
155 })?;
156
157 this.update(cx, |this, cx| {
158 this.available_models = models;
159 cx.notify();
160 })
161 .map_err(|e| LanguageModelCompletionError::Other(e))?;
162
163 Ok(())
164 })
165 }
166
167 fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
168 if self.is_authenticated() {
169 let task = self.fetch_models(cx);
170 self.fetch_models_task.replace(task);
171 }
172 }
173}
174
175impl OpenRouterLanguageModelProvider {
176 pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
177 let state = cx.new(|cx| State {
178 api_key: None,
179 api_key_from_env: false,
180 http_client: http_client.clone(),
181 available_models: Vec::new(),
182 fetch_models_task: None,
183 settings: OpenRouterSettings::default(),
184 _subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
185 let current_settings = &AllLanguageModelSettings::get_global(cx).open_router;
186 let settings_changed = current_settings != &this.settings;
187 if settings_changed {
188 this.settings = current_settings.clone();
189 this.restart_fetch_models_task(cx);
190 }
191 cx.notify();
192 }),
193 });
194
195 Self { http_client, state }
196 }
197
198 fn create_language_model(&self, model: open_router::Model) -> Arc<dyn LanguageModel> {
199 Arc::new(OpenRouterLanguageModel {
200 id: LanguageModelId::from(model.id().to_string()),
201 model,
202 state: self.state.clone(),
203 http_client: self.http_client.clone(),
204 request_limiter: RateLimiter::new(4),
205 })
206 }
207}
208
209impl LanguageModelProviderState for OpenRouterLanguageModelProvider {
210 type ObservableEntity = State;
211
212 fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> {
213 Some(self.state.clone())
214 }
215}
216
217impl LanguageModelProvider for OpenRouterLanguageModelProvider {
218 fn id(&self) -> LanguageModelProviderId {
219 PROVIDER_ID
220 }
221
222 fn name(&self) -> LanguageModelProviderName {
223 PROVIDER_NAME
224 }
225
226 fn icon(&self) -> IconName {
227 IconName::AiOpenRouter
228 }
229
230 fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
231 Some(self.create_language_model(open_router::Model::default()))
232 }
233
234 fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
235 Some(self.create_language_model(open_router::Model::default_fast()))
236 }
237
238 fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
239 let mut models_from_api = self.state.read(cx).available_models.clone();
240 let mut settings_models = Vec::new();
241
242 for model in &AllLanguageModelSettings::get_global(cx)
243 .open_router
244 .available_models
245 {
246 settings_models.push(open_router::Model {
247 name: model.name.clone(),
248 display_name: model.display_name.clone(),
249 max_tokens: model.max_tokens,
250 supports_tools: model.supports_tools,
251 supports_images: model.supports_images,
252 mode: model.mode.clone().unwrap_or_default(),
253 provider: model.provider.clone(),
254 });
255 }
256
257 for settings_model in &settings_models {
258 if let Some(pos) = models_from_api
259 .iter()
260 .position(|m| m.name == settings_model.name)
261 {
262 models_from_api[pos] = settings_model.clone();
263 } else {
264 models_from_api.push(settings_model.clone());
265 }
266 }
267
268 models_from_api
269 .into_iter()
270 .map(|model| self.create_language_model(model))
271 .collect()
272 }
273
274 fn is_authenticated(&self, cx: &App) -> bool {
275 self.state.read(cx).is_authenticated()
276 }
277
278 fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
279 self.state.update(cx, |state, cx| state.authenticate(cx))
280 }
281
282 fn configuration_view(
283 &self,
284 _target_agent: language_model::ConfigurationViewTargetAgent,
285 window: &mut Window,
286 cx: &mut App,
287 ) -> AnyView {
288 cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
289 .into()
290 }
291
292 fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
293 self.state.update(cx, |state, cx| state.reset_api_key(cx))
294 }
295}
296
297pub struct OpenRouterLanguageModel {
298 id: LanguageModelId,
299 model: open_router::Model,
300 state: gpui::Entity<State>,
301 http_client: Arc<dyn HttpClient>,
302 request_limiter: RateLimiter,
303}
304
305impl OpenRouterLanguageModel {
306 fn stream_completion(
307 &self,
308 request: open_router::Request,
309 cx: &AsyncApp,
310 ) -> BoxFuture<
311 'static,
312 Result<
313 futures::stream::BoxStream<
314 'static,
315 Result<ResponseStreamEvent, open_router::OpenRouterError>,
316 >,
317 LanguageModelCompletionError,
318 >,
319 > {
320 let http_client = self.http_client.clone();
321 let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
322 let settings = &AllLanguageModelSettings::get_global(cx).open_router;
323 (state.api_key.clone(), settings.api_url.clone())
324 }) else {
325 return futures::future::ready(Err(LanguageModelCompletionError::Other(anyhow!(
326 "App state dropped"
327 ))))
328 .boxed();
329 };
330
331 async move {
332 let Some(api_key) = api_key else {
333 return Err(LanguageModelCompletionError::NoApiKey {
334 provider: PROVIDER_NAME,
335 });
336 };
337 let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
338 request.await.map_err(Into::into)
339 }
340 .boxed()
341 }
342}
343
344impl LanguageModel for OpenRouterLanguageModel {
345 fn id(&self) -> LanguageModelId {
346 self.id.clone()
347 }
348
349 fn name(&self) -> LanguageModelName {
350 LanguageModelName::from(self.model.display_name().to_string())
351 }
352
353 fn provider_id(&self) -> LanguageModelProviderId {
354 PROVIDER_ID
355 }
356
357 fn provider_name(&self) -> LanguageModelProviderName {
358 PROVIDER_NAME
359 }
360
361 fn supports_tools(&self) -> bool {
362 self.model.supports_tool_calls()
363 }
364
365 fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
366 let model_id = self.model.id().trim().to_lowercase();
367 if model_id.contains("gemini") || model_id.contains("grok") {
368 LanguageModelToolSchemaFormat::JsonSchemaSubset
369 } else {
370 LanguageModelToolSchemaFormat::JsonSchema
371 }
372 }
373
374 fn telemetry_id(&self) -> String {
375 format!("openrouter/{}", self.model.id())
376 }
377
378 fn max_token_count(&self) -> u64 {
379 self.model.max_token_count()
380 }
381
382 fn max_output_tokens(&self) -> Option<u64> {
383 self.model.max_output_tokens()
384 }
385
386 fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
387 match choice {
388 LanguageModelToolChoice::Auto => true,
389 LanguageModelToolChoice::Any => true,
390 LanguageModelToolChoice::None => true,
391 }
392 }
393
394 fn supports_images(&self) -> bool {
395 self.model.supports_images.unwrap_or(false)
396 }
397
398 fn count_tokens(
399 &self,
400 request: LanguageModelRequest,
401 cx: &App,
402 ) -> BoxFuture<'static, Result<u64>> {
403 count_open_router_tokens(request, self.model.clone(), cx)
404 }
405
406 fn stream_completion(
407 &self,
408 request: LanguageModelRequest,
409 cx: &AsyncApp,
410 ) -> BoxFuture<
411 'static,
412 Result<
413 futures::stream::BoxStream<
414 'static,
415 Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
416 >,
417 LanguageModelCompletionError,
418 >,
419 > {
420 let request = into_open_router(request, &self.model, self.max_output_tokens());
421 let request = self.stream_completion(request, cx);
422 let future = self.request_limiter.stream(async move {
423 let response = request.await?;
424 Ok(OpenRouterEventMapper::new().map_stream(response))
425 });
426 async move { Ok(future.await?.boxed()) }.boxed()
427 }
428}
429
430pub fn into_open_router(
431 request: LanguageModelRequest,
432 model: &Model,
433 max_output_tokens: Option<u64>,
434) -> open_router::Request {
435 let mut messages = Vec::new();
436 for message in request.messages {
437 for content in message.content {
438 match content {
439 MessageContent::Text(text) => add_message_content_part(
440 open_router::MessagePart::Text { text },
441 message.role,
442 &mut messages,
443 ),
444 MessageContent::Thinking { .. } => {}
445 MessageContent::RedactedThinking(_) => {}
446 MessageContent::Image(image) => {
447 add_message_content_part(
448 open_router::MessagePart::Image {
449 image_url: image.to_base64_url(),
450 },
451 message.role,
452 &mut messages,
453 );
454 }
455 MessageContent::ToolUse(tool_use) => {
456 let tool_call = open_router::ToolCall {
457 id: tool_use.id.to_string(),
458 content: open_router::ToolCallContent::Function {
459 function: open_router::FunctionContent {
460 name: tool_use.name.to_string(),
461 arguments: serde_json::to_string(&tool_use.input)
462 .unwrap_or_default(),
463 },
464 },
465 };
466
467 if let Some(open_router::RequestMessage::Assistant { tool_calls, .. }) =
468 messages.last_mut()
469 {
470 tool_calls.push(tool_call);
471 } else {
472 messages.push(open_router::RequestMessage::Assistant {
473 content: None,
474 tool_calls: vec![tool_call],
475 });
476 }
477 }
478 MessageContent::ToolResult(tool_result) => {
479 let content = match &tool_result.content {
480 LanguageModelToolResultContent::Text(text) => {
481 vec![open_router::MessagePart::Text {
482 text: text.to_string(),
483 }]
484 }
485 LanguageModelToolResultContent::Image(image) => {
486 vec![open_router::MessagePart::Image {
487 image_url: image.to_base64_url(),
488 }]
489 }
490 };
491
492 messages.push(open_router::RequestMessage::Tool {
493 content: content.into(),
494 tool_call_id: tool_result.tool_use_id.to_string(),
495 });
496 }
497 }
498 }
499 }
500
501 open_router::Request {
502 model: model.id().into(),
503 messages,
504 stream: true,
505 stop: request.stop,
506 temperature: request.temperature.unwrap_or(0.4),
507 max_tokens: max_output_tokens,
508 parallel_tool_calls: if model.supports_parallel_tool_calls() && !request.tools.is_empty() {
509 Some(false)
510 } else {
511 None
512 },
513 usage: open_router::RequestUsage { include: true },
514 reasoning: if request.thinking_allowed
515 && let OpenRouterModelMode::Thinking { budget_tokens } = model.mode
516 {
517 Some(open_router::Reasoning {
518 effort: None,
519 max_tokens: budget_tokens,
520 exclude: Some(false),
521 enabled: Some(true),
522 })
523 } else {
524 None
525 },
526 tools: request
527 .tools
528 .into_iter()
529 .map(|tool| open_router::ToolDefinition::Function {
530 function: open_router::FunctionDefinition {
531 name: tool.name,
532 description: Some(tool.description),
533 parameters: Some(tool.input_schema),
534 },
535 })
536 .collect(),
537 tool_choice: request.tool_choice.map(|choice| match choice {
538 LanguageModelToolChoice::Auto => open_router::ToolChoice::Auto,
539 LanguageModelToolChoice::Any => open_router::ToolChoice::Required,
540 LanguageModelToolChoice::None => open_router::ToolChoice::None,
541 }),
542 provider: model.provider.clone(),
543 }
544}
545
546fn add_message_content_part(
547 new_part: open_router::MessagePart,
548 role: Role,
549 messages: &mut Vec<open_router::RequestMessage>,
550) {
551 match (role, messages.last_mut()) {
552 (Role::User, Some(open_router::RequestMessage::User { content }))
553 | (Role::System, Some(open_router::RequestMessage::System { content })) => {
554 content.push_part(new_part);
555 }
556 (
557 Role::Assistant,
558 Some(open_router::RequestMessage::Assistant {
559 content: Some(content),
560 ..
561 }),
562 ) => {
563 content.push_part(new_part);
564 }
565 _ => {
566 messages.push(match role {
567 Role::User => open_router::RequestMessage::User {
568 content: open_router::MessageContent::from(vec![new_part]),
569 },
570 Role::Assistant => open_router::RequestMessage::Assistant {
571 content: Some(open_router::MessageContent::from(vec![new_part])),
572 tool_calls: Vec::new(),
573 },
574 Role::System => open_router::RequestMessage::System {
575 content: open_router::MessageContent::from(vec![new_part]),
576 },
577 });
578 }
579 }
580}
581
582pub struct OpenRouterEventMapper {
583 tool_calls_by_index: HashMap<usize, RawToolCall>,
584}
585
586impl OpenRouterEventMapper {
587 pub fn new() -> Self {
588 Self {
589 tool_calls_by_index: HashMap::default(),
590 }
591 }
592
593 pub fn map_stream(
594 mut self,
595 events: Pin<
596 Box<
597 dyn Send + Stream<Item = Result<ResponseStreamEvent, open_router::OpenRouterError>>,
598 >,
599 >,
600 ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
601 {
602 events.flat_map(move |event| {
603 futures::stream::iter(match event {
604 Ok(event) => self.map_event(event),
605 Err(error) => vec![Err(error.into())],
606 })
607 })
608 }
609
610 pub fn map_event(
611 &mut self,
612 event: ResponseStreamEvent,
613 ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
614 let Some(choice) = event.choices.first() else {
615 return vec![Err(LanguageModelCompletionError::from(anyhow!(
616 "Response contained no choices"
617 )))];
618 };
619
620 let mut events = Vec::new();
621 if let Some(reasoning) = choice.delta.reasoning.clone() {
622 events.push(Ok(LanguageModelCompletionEvent::Thinking {
623 text: reasoning,
624 signature: None,
625 }));
626 }
627
628 if let Some(content) = choice.delta.content.clone() {
629 // OpenRouter send empty content string with the reasoning content
630 // This is a workaround for the OpenRouter API bug
631 if !content.is_empty() {
632 events.push(Ok(LanguageModelCompletionEvent::Text(content)));
633 }
634 }
635
636 if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
637 for tool_call in tool_calls {
638 let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
639
640 if let Some(tool_id) = tool_call.id.clone() {
641 entry.id = tool_id;
642 }
643
644 if let Some(function) = tool_call.function.as_ref() {
645 if let Some(name) = function.name.clone() {
646 entry.name = name;
647 }
648
649 if let Some(arguments) = function.arguments.clone() {
650 entry.arguments.push_str(&arguments);
651 }
652 }
653 }
654 }
655
656 if let Some(usage) = event.usage {
657 events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
658 input_tokens: usage.prompt_tokens,
659 output_tokens: usage.completion_tokens,
660 cache_creation_input_tokens: 0,
661 cache_read_input_tokens: 0,
662 })));
663 }
664
665 match choice.finish_reason.as_deref() {
666 Some("stop") => {
667 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
668 }
669 Some("tool_calls") => {
670 events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
671 match serde_json::Value::from_str(&tool_call.arguments) {
672 Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
673 LanguageModelToolUse {
674 id: tool_call.id.clone().into(),
675 name: tool_call.name.as_str().into(),
676 is_input_complete: true,
677 input,
678 raw_input: tool_call.arguments.clone(),
679 },
680 )),
681 Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
682 id: tool_call.id.clone().into(),
683 tool_name: tool_call.name.as_str().into(),
684 raw_input: tool_call.arguments.clone().into(),
685 json_parse_error: error.to_string(),
686 }),
687 }
688 }));
689
690 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
691 }
692 Some(stop_reason) => {
693 log::error!("Unexpected OpenRouter stop_reason: {stop_reason:?}",);
694 events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
695 }
696 None => {}
697 }
698
699 events
700 }
701}
702
703#[derive(Default)]
704struct RawToolCall {
705 id: String,
706 name: String,
707 arguments: String,
708}
709
710pub fn count_open_router_tokens(
711 request: LanguageModelRequest,
712 _model: open_router::Model,
713 cx: &App,
714) -> BoxFuture<'static, Result<u64>> {
715 cx.background_spawn(async move {
716 let messages = request
717 .messages
718 .into_iter()
719 .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
720 role: match message.role {
721 Role::User => "user".into(),
722 Role::Assistant => "assistant".into(),
723 Role::System => "system".into(),
724 },
725 content: Some(message.string_contents()),
726 name: None,
727 function_call: None,
728 })
729 .collect::<Vec<_>>();
730
731 tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages).map(|tokens| tokens as u64)
732 })
733 .boxed()
734}
735
736struct ConfigurationView {
737 api_key_editor: Entity<Editor>,
738 state: gpui::Entity<State>,
739 load_credentials_task: Option<Task<()>>,
740}
741
742impl ConfigurationView {
743 fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
744 let api_key_editor = cx.new(|cx| {
745 let mut editor = Editor::single_line(window, cx);
746 editor.set_placeholder_text(
747 "sk_or_000000000000000000000000000000000000000000000000",
748 window,
749 cx,
750 );
751 editor
752 });
753
754 cx.observe(&state, |_, _, cx| {
755 cx.notify();
756 })
757 .detach();
758
759 let load_credentials_task = Some(cx.spawn_in(window, {
760 let state = state.clone();
761 async move |this, cx| {
762 if let Some(task) = state
763 .update(cx, |state, cx| state.authenticate(cx))
764 .log_err()
765 {
766 let _ = task.await;
767 }
768
769 this.update(cx, |this, cx| {
770 this.load_credentials_task = None;
771 cx.notify();
772 })
773 .log_err();
774 }
775 }));
776
777 Self {
778 api_key_editor,
779 state,
780 load_credentials_task,
781 }
782 }
783
784 fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
785 let api_key = self.api_key_editor.read(cx).text(cx);
786 if api_key.is_empty() {
787 return;
788 }
789
790 let state = self.state.clone();
791 cx.spawn_in(window, async move |_, cx| {
792 state
793 .update(cx, |state, cx| state.set_api_key(api_key, cx))?
794 .await
795 })
796 .detach_and_log_err(cx);
797
798 cx.notify();
799 }
800
801 fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
802 self.api_key_editor
803 .update(cx, |editor, cx| editor.set_text("", window, cx));
804
805 let state = self.state.clone();
806 cx.spawn_in(window, async move |_, cx| {
807 state.update(cx, |state, cx| state.reset_api_key(cx))?.await
808 })
809 .detach_and_log_err(cx);
810
811 cx.notify();
812 }
813
814 fn render_api_key_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
815 let settings = ThemeSettings::get_global(cx);
816 let text_style = TextStyle {
817 color: cx.theme().colors().text,
818 font_family: settings.ui_font.family.clone(),
819 font_features: settings.ui_font.features.clone(),
820 font_fallbacks: settings.ui_font.fallbacks.clone(),
821 font_size: rems(0.875).into(),
822 font_weight: settings.ui_font.weight,
823 font_style: FontStyle::Normal,
824 line_height: relative(1.3),
825 white_space: WhiteSpace::Normal,
826 ..Default::default()
827 };
828 EditorElement::new(
829 &self.api_key_editor,
830 EditorStyle {
831 background: cx.theme().colors().editor_background,
832 local_player: cx.theme().players().local(),
833 text: text_style,
834 ..Default::default()
835 },
836 )
837 }
838
839 fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
840 !self.state.read(cx).is_authenticated()
841 }
842}
843
844impl Render for ConfigurationView {
845 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
846 let env_var_set = self.state.read(cx).api_key_from_env;
847
848 if self.load_credentials_task.is_some() {
849 div().child(Label::new("Loading credentials...")).into_any()
850 } else if self.should_render_editor(cx) {
851 v_flex()
852 .size_full()
853 .on_action(cx.listener(Self::save_api_key))
854 .child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:"))
855 .child(
856 List::new()
857 .child(InstructionListItem::new(
858 "Create an API key by visiting",
859 Some("OpenRouter's console"),
860 Some("https://openrouter.ai/keys"),
861 ))
862 .child(InstructionListItem::text_only(
863 "Ensure your OpenRouter account has credits",
864 ))
865 .child(InstructionListItem::text_only(
866 "Paste your API key below and hit enter to start using the assistant",
867 )),
868 )
869 .child(
870 h_flex()
871 .w_full()
872 .my_2()
873 .px_2()
874 .py_1()
875 .bg(cx.theme().colors().editor_background)
876 .border_1()
877 .border_color(cx.theme().colors().border)
878 .rounded_sm()
879 .child(self.render_api_key_editor(cx)),
880 )
881 .child(
882 Label::new(
883 format!("You can also assign the {OPENROUTER_API_KEY_VAR} environment variable and restart Zed."),
884 )
885 .size(LabelSize::Small).color(Color::Muted),
886 )
887 .into_any()
888 } else {
889 h_flex()
890 .mt_1()
891 .p_1()
892 .justify_between()
893 .rounded_md()
894 .border_1()
895 .border_color(cx.theme().colors().border)
896 .bg(cx.theme().colors().background)
897 .child(
898 h_flex()
899 .gap_1()
900 .child(Icon::new(IconName::Check).color(Color::Success))
901 .child(Label::new(if env_var_set {
902 format!("API key set in {OPENROUTER_API_KEY_VAR} environment variable.")
903 } else {
904 "API key configured.".to_string()
905 })),
906 )
907 .child(
908 Button::new("reset-key", "Reset Key")
909 .label_size(LabelSize::Small)
910 .icon(Some(IconName::Trash))
911 .icon_size(IconSize::Small)
912 .icon_position(IconPosition::Start)
913 .disabled(env_var_set)
914 .when(env_var_set, |this| {
915 this.tooltip(Tooltip::text(format!("To reset your API key, unset the {OPENROUTER_API_KEY_VAR} environment variable.")))
916 })
917 .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
918 )
919 .into_any()
920 }
921 }
922}