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