open_router.rs

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