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