open_router.rs

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