open_router.rs

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