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, 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}