open_router.rs

  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}