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}