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