open_router.rs

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