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}