open_router.rs

   1use anyhow::Result;
   2use collections::HashMap;
   3use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
   4use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
   5use http_client::HttpClient;
   6use language_model::{
   7    ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
   8    LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
   9    LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
  10    LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
  11    LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
  12    StopReason, TokenUsage, env_var,
  13};
  14use open_router::{
  15    Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,
  16};
  17use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore};
  18use std::pin::Pin;
  19use std::sync::{Arc, LazyLock};
  20use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
  21use ui_input::InputField;
  22use util::ResultExt;
  23
  24use crate::provider::util::parse_tool_arguments;
  25
  26const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
  27const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
  28
  29const API_KEY_ENV_VAR_NAME: &str = "OPENROUTER_API_KEY";
  30static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
  31
  32#[derive(Default, Clone, Debug, PartialEq)]
  33pub struct OpenRouterSettings {
  34    pub api_url: String,
  35    pub available_models: Vec<AvailableModel>,
  36}
  37
  38pub struct OpenRouterLanguageModelProvider {
  39    http_client: Arc<dyn HttpClient>,
  40    state: Entity<State>,
  41}
  42
  43pub struct State {
  44    api_key_state: ApiKeyState,
  45    http_client: Arc<dyn HttpClient>,
  46    available_models: Vec<open_router::Model>,
  47    fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
  48}
  49
  50impl State {
  51    fn is_authenticated(&self) -> bool {
  52        self.api_key_state.has_key()
  53    }
  54
  55    fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
  56        let api_url = OpenRouterLanguageModelProvider::api_url(cx);
  57        self.api_key_state
  58            .store(api_url, api_key, |this| &mut this.api_key_state, cx)
  59    }
  60
  61    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
  62        let api_url = OpenRouterLanguageModelProvider::api_url(cx);
  63        let task = self
  64            .api_key_state
  65            .load_if_needed(api_url, |this| &mut this.api_key_state, cx);
  66
  67        cx.spawn(async move |this, cx| {
  68            let result = task.await;
  69            this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
  70                .ok();
  71            result
  72        })
  73    }
  74
  75    fn fetch_models(
  76        &mut self,
  77        cx: &mut Context<Self>,
  78    ) -> Task<Result<(), LanguageModelCompletionError>> {
  79        let http_client = self.http_client.clone();
  80        let api_url = OpenRouterLanguageModelProvider::api_url(cx);
  81        let Some(api_key) = self.api_key_state.key(&api_url) else {
  82            return Task::ready(Err(LanguageModelCompletionError::NoApiKey {
  83                provider: PROVIDER_NAME,
  84            }));
  85        };
  86        cx.spawn(async move |this, cx| {
  87            let models = list_models(http_client.as_ref(), &api_url, &api_key)
  88                .await
  89                .map_err(|e| {
  90                    LanguageModelCompletionError::Other(anyhow::anyhow!(
  91                        "OpenRouter error: {:?}",
  92                        e
  93                    ))
  94                })?;
  95
  96            this.update(cx, |this, cx| {
  97                this.available_models = models;
  98                cx.notify();
  99            })
 100            .map_err(|e| LanguageModelCompletionError::Other(e))?;
 101
 102            Ok(())
 103        })
 104    }
 105
 106    fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
 107        if self.is_authenticated() {
 108            let task = self.fetch_models(cx);
 109            self.fetch_models_task.replace(task);
 110        } else {
 111            self.available_models = Vec::new();
 112        }
 113    }
 114}
 115
 116impl OpenRouterLanguageModelProvider {
 117    pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
 118        let state = cx.new(|cx| {
 119            cx.observe_global::<SettingsStore>({
 120                let mut last_settings = OpenRouterLanguageModelProvider::settings(cx).clone();
 121                move |this: &mut State, cx| {
 122                    let current_settings = OpenRouterLanguageModelProvider::settings(cx);
 123                    let settings_changed = current_settings != &last_settings;
 124                    if settings_changed {
 125                        last_settings = current_settings.clone();
 126                        this.authenticate(cx).detach();
 127                        cx.notify();
 128                    }
 129                }
 130            })
 131            .detach();
 132            State {
 133                api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
 134                http_client: http_client.clone(),
 135                available_models: Vec::new(),
 136                fetch_models_task: None,
 137            }
 138        });
 139
 140        Self { http_client, state }
 141    }
 142
 143    fn settings(cx: &App) -> &OpenRouterSettings {
 144        &crate::AllLanguageModelSettings::get_global(cx).open_router
 145    }
 146
 147    fn api_url(cx: &App) -> SharedString {
 148        let api_url = &Self::settings(cx).api_url;
 149        if api_url.is_empty() {
 150            OPEN_ROUTER_API_URL.into()
 151        } else {
 152            SharedString::new(api_url.as_str())
 153        }
 154    }
 155
 156    fn create_language_model(&self, model: open_router::Model) -> Arc<dyn LanguageModel> {
 157        Arc::new(OpenRouterLanguageModel {
 158            id: LanguageModelId::from(model.id().to_string()),
 159            model,
 160            state: self.state.clone(),
 161            http_client: self.http_client.clone(),
 162            request_limiter: RateLimiter::new(4),
 163        })
 164    }
 165}
 166
 167impl LanguageModelProviderState for OpenRouterLanguageModelProvider {
 168    type ObservableEntity = State;
 169
 170    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
 171        Some(self.state.clone())
 172    }
 173}
 174
 175impl LanguageModelProvider for OpenRouterLanguageModelProvider {
 176    fn id(&self) -> LanguageModelProviderId {
 177        PROVIDER_ID
 178    }
 179
 180    fn name(&self) -> LanguageModelProviderName {
 181        PROVIDER_NAME
 182    }
 183
 184    fn icon(&self) -> IconOrSvg {
 185        IconOrSvg::Icon(IconName::AiOpenRouter)
 186    }
 187
 188    fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
 189        Some(self.create_language_model(open_router::Model::default()))
 190    }
 191
 192    fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
 193        None
 194    }
 195
 196    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
 197        let mut models_from_api = self.state.read(cx).available_models.clone();
 198        let mut settings_models = Vec::new();
 199
 200        for model in &Self::settings(cx).available_models {
 201            settings_models.push(open_router::Model {
 202                name: model.name.clone(),
 203                display_name: model.display_name.clone(),
 204                max_tokens: model.max_tokens,
 205                supports_tools: model.supports_tools,
 206                supports_images: model.supports_images,
 207                mode: model.mode.unwrap_or_default(),
 208                provider: model.provider.clone(),
 209            });
 210        }
 211
 212        for settings_model in &settings_models {
 213            if let Some(pos) = models_from_api
 214                .iter()
 215                .position(|m| m.name == settings_model.name)
 216            {
 217                models_from_api[pos] = settings_model.clone();
 218            } else {
 219                models_from_api.push(settings_model.clone());
 220            }
 221        }
 222
 223        models_from_api
 224            .into_iter()
 225            .map(|model| self.create_language_model(model))
 226            .collect()
 227    }
 228
 229    fn is_authenticated(&self, cx: &App) -> bool {
 230        self.state.read(cx).is_authenticated()
 231    }
 232
 233    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
 234        self.state.update(cx, |state, cx| state.authenticate(cx))
 235    }
 236
 237    fn configuration_view(
 238        &self,
 239        _target_agent: language_model::ConfigurationViewTargetAgent,
 240        window: &mut Window,
 241        cx: &mut App,
 242    ) -> AnyView {
 243        cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
 244            .into()
 245    }
 246
 247    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
 248        self.state
 249            .update(cx, |state, cx| state.set_api_key(None, cx))
 250    }
 251}
 252
 253pub struct OpenRouterLanguageModel {
 254    id: LanguageModelId,
 255    model: open_router::Model,
 256    state: Entity<State>,
 257    http_client: Arc<dyn HttpClient>,
 258    request_limiter: RateLimiter,
 259}
 260
 261impl OpenRouterLanguageModel {
 262    fn stream_completion(
 263        &self,
 264        request: open_router::Request,
 265        cx: &AsyncApp,
 266    ) -> BoxFuture<
 267        'static,
 268        Result<
 269            futures::stream::BoxStream<
 270                'static,
 271                Result<ResponseStreamEvent, open_router::OpenRouterError>,
 272            >,
 273            LanguageModelCompletionError,
 274        >,
 275    > {
 276        let http_client = self.http_client.clone();
 277        let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
 278            let api_url = OpenRouterLanguageModelProvider::api_url(cx);
 279            (state.api_key_state.key(&api_url), api_url)
 280        });
 281
 282        async move {
 283            let Some(api_key) = api_key else {
 284                return Err(LanguageModelCompletionError::NoApiKey {
 285                    provider: PROVIDER_NAME,
 286                });
 287            };
 288            let request =
 289                open_router::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
 290            request.await.map_err(Into::into)
 291        }
 292        .boxed()
 293    }
 294}
 295
 296impl LanguageModel for OpenRouterLanguageModel {
 297    fn id(&self) -> LanguageModelId {
 298        self.id.clone()
 299    }
 300
 301    fn name(&self) -> LanguageModelName {
 302        LanguageModelName::from(self.model.display_name().to_string())
 303    }
 304
 305    fn provider_id(&self) -> LanguageModelProviderId {
 306        PROVIDER_ID
 307    }
 308
 309    fn provider_name(&self) -> LanguageModelProviderName {
 310        PROVIDER_NAME
 311    }
 312
 313    fn supports_tools(&self) -> bool {
 314        self.model.supports_tool_calls()
 315    }
 316
 317    fn supports_streaming_tools(&self) -> bool {
 318        true
 319    }
 320
 321    fn supports_thinking(&self) -> bool {
 322        matches!(self.model.mode, OpenRouterModelMode::Thinking { .. })
 323    }
 324
 325    fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
 326        let model_id = self.model.id().trim().to_lowercase();
 327        if model_id.contains("gemini") || model_id.contains("grok") {
 328            LanguageModelToolSchemaFormat::JsonSchemaSubset
 329        } else {
 330            LanguageModelToolSchemaFormat::JsonSchema
 331        }
 332    }
 333
 334    fn telemetry_id(&self) -> String {
 335        format!("openrouter/{}", self.model.id())
 336    }
 337
 338    fn max_token_count(&self) -> u64 {
 339        self.model.max_token_count()
 340    }
 341
 342    fn max_output_tokens(&self) -> Option<u64> {
 343        self.model.max_output_tokens()
 344    }
 345
 346    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
 347        match choice {
 348            LanguageModelToolChoice::Auto => true,
 349            LanguageModelToolChoice::Any => true,
 350            LanguageModelToolChoice::None => true,
 351        }
 352    }
 353
 354    fn supports_images(&self) -> bool {
 355        self.model.supports_images.unwrap_or(false)
 356    }
 357
 358    fn count_tokens(
 359        &self,
 360        request: LanguageModelRequest,
 361        cx: &App,
 362    ) -> BoxFuture<'static, Result<u64>> {
 363        count_open_router_tokens(request, self.model.clone(), cx)
 364    }
 365
 366    fn stream_completion(
 367        &self,
 368        request: LanguageModelRequest,
 369        cx: &AsyncApp,
 370    ) -> BoxFuture<
 371        'static,
 372        Result<
 373            futures::stream::BoxStream<
 374                'static,
 375                Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
 376            >,
 377            LanguageModelCompletionError,
 378        >,
 379    > {
 380        let openrouter_request = into_open_router(request, &self.model, self.max_output_tokens());
 381        let request = self.stream_completion(openrouter_request, cx);
 382        let future = self.request_limiter.stream(async move {
 383            let response = request.await?;
 384            Ok(OpenRouterEventMapper::new().map_stream(response))
 385        });
 386        async move { Ok(future.await?.boxed()) }.boxed()
 387    }
 388}
 389
 390pub fn into_open_router(
 391    request: LanguageModelRequest,
 392    model: &Model,
 393    max_output_tokens: Option<u64>,
 394) -> open_router::Request {
 395    // Anthropic models via OpenRouter don't accept reasoning_details being echoed back
 396    // in requests - it's an output-only field for them. However, Gemini models require
 397    // the thought signatures to be echoed back for proper reasoning chain continuity.
 398    // Note: OpenRouter's model API provides an `architecture.tokenizer` field (e.g. "Claude",
 399    // "Gemini") which could replace this ID prefix check, but since this is the only place
 400    // we need this distinction, we're just using this less invasive check instead.
 401    // If we ever have a more formal distionction between the models in the future,
 402    // we should revise this to use that instead.
 403    let is_anthropic_model = model.id().starts_with("anthropic/");
 404
 405    let mut messages = Vec::new();
 406    for message in request.messages {
 407        let reasoning_details_for_message = if is_anthropic_model {
 408            None
 409        } else {
 410            message.reasoning_details.clone()
 411        };
 412
 413        for content in message.content {
 414            match content {
 415                MessageContent::Text(text) => add_message_content_part(
 416                    open_router::MessagePart::Text { text },
 417                    message.role,
 418                    &mut messages,
 419                    reasoning_details_for_message.clone(),
 420                ),
 421                MessageContent::Thinking { .. } => {}
 422                MessageContent::RedactedThinking(_) => {}
 423                MessageContent::Image(image) => {
 424                    add_message_content_part(
 425                        open_router::MessagePart::Image {
 426                            image_url: image.to_base64_url(),
 427                        },
 428                        message.role,
 429                        &mut messages,
 430                        reasoning_details_for_message.clone(),
 431                    );
 432                }
 433                MessageContent::ToolUse(tool_use) => {
 434                    let tool_call = open_router::ToolCall {
 435                        id: tool_use.id.to_string(),
 436                        content: open_router::ToolCallContent::Function {
 437                            function: open_router::FunctionContent {
 438                                name: tool_use.name.to_string(),
 439                                arguments: serde_json::to_string(&tool_use.input)
 440                                    .unwrap_or_default(),
 441                                thought_signature: tool_use.thought_signature.clone(),
 442                            },
 443                        },
 444                    };
 445
 446                    if let Some(open_router::RequestMessage::Assistant { tool_calls, .. }) =
 447                        messages.last_mut()
 448                    {
 449                        tool_calls.push(tool_call);
 450                    } else {
 451                        messages.push(open_router::RequestMessage::Assistant {
 452                            content: None,
 453                            tool_calls: vec![tool_call],
 454                            reasoning_details: reasoning_details_for_message.clone(),
 455                        });
 456                    }
 457                }
 458                MessageContent::ToolResult(tool_result) => {
 459                    let content = match &tool_result.content {
 460                        LanguageModelToolResultContent::Text(text) => {
 461                            vec![open_router::MessagePart::Text {
 462                                text: text.to_string(),
 463                            }]
 464                        }
 465                        LanguageModelToolResultContent::Image(image) => {
 466                            vec![open_router::MessagePart::Image {
 467                                image_url: image.to_base64_url(),
 468                            }]
 469                        }
 470                    };
 471
 472                    messages.push(open_router::RequestMessage::Tool {
 473                        content: content.into(),
 474                        tool_call_id: tool_result.tool_use_id.to_string(),
 475                    });
 476                }
 477            }
 478        }
 479    }
 480
 481    open_router::Request {
 482        model: model.id().into(),
 483        messages,
 484        stream: true,
 485        stop: request.stop,
 486        temperature: request.temperature.unwrap_or(0.4),
 487        max_tokens: max_output_tokens,
 488        parallel_tool_calls: if model.supports_parallel_tool_calls() && !request.tools.is_empty() {
 489            Some(false)
 490        } else {
 491            None
 492        },
 493        usage: open_router::RequestUsage { include: true },
 494        reasoning: if request.thinking_allowed
 495            && let OpenRouterModelMode::Thinking { budget_tokens } = model.mode
 496        {
 497            Some(open_router::Reasoning {
 498                effort: None,
 499                max_tokens: budget_tokens,
 500                exclude: Some(false),
 501                enabled: Some(true),
 502            })
 503        } else {
 504            None
 505        },
 506        tools: request
 507            .tools
 508            .into_iter()
 509            .map(|tool| open_router::ToolDefinition::Function {
 510                function: open_router::FunctionDefinition {
 511                    name: tool.name,
 512                    description: Some(tool.description),
 513                    parameters: Some(tool.input_schema),
 514                },
 515            })
 516            .collect(),
 517        tool_choice: request.tool_choice.map(|choice| match choice {
 518            LanguageModelToolChoice::Auto => open_router::ToolChoice::Auto,
 519            LanguageModelToolChoice::Any => open_router::ToolChoice::Required,
 520            LanguageModelToolChoice::None => open_router::ToolChoice::None,
 521        }),
 522        provider: model.provider.clone(),
 523    }
 524}
 525
 526fn add_message_content_part(
 527    new_part: open_router::MessagePart,
 528    role: Role,
 529    messages: &mut Vec<open_router::RequestMessage>,
 530    reasoning_details: Option<serde_json::Value>,
 531) {
 532    match (role, messages.last_mut()) {
 533        (Role::User, Some(open_router::RequestMessage::User { content }))
 534        | (Role::System, Some(open_router::RequestMessage::System { content })) => {
 535            content.push_part(new_part);
 536        }
 537        (
 538            Role::Assistant,
 539            Some(open_router::RequestMessage::Assistant {
 540                content: Some(content),
 541                ..
 542            }),
 543        ) => {
 544            content.push_part(new_part);
 545        }
 546        _ => {
 547            messages.push(match role {
 548                Role::User => open_router::RequestMessage::User {
 549                    content: open_router::MessageContent::from(vec![new_part]),
 550                },
 551                Role::Assistant => open_router::RequestMessage::Assistant {
 552                    content: Some(open_router::MessageContent::from(vec![new_part])),
 553                    tool_calls: Vec::new(),
 554                    reasoning_details,
 555                },
 556                Role::System => open_router::RequestMessage::System {
 557                    content: open_router::MessageContent::from(vec![new_part]),
 558                },
 559            });
 560        }
 561    }
 562}
 563
 564pub struct OpenRouterEventMapper {
 565    tool_calls_by_index: HashMap<usize, RawToolCall>,
 566    reasoning_details: Option<serde_json::Value>,
 567}
 568
 569impl OpenRouterEventMapper {
 570    pub fn new() -> Self {
 571        Self {
 572            tool_calls_by_index: HashMap::default(),
 573            reasoning_details: None,
 574        }
 575    }
 576
 577    pub fn map_stream(
 578        mut self,
 579        events: Pin<
 580            Box<
 581                dyn Send + Stream<Item = Result<ResponseStreamEvent, open_router::OpenRouterError>>,
 582            >,
 583        >,
 584    ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
 585    {
 586        events.flat_map(move |event| {
 587            futures::stream::iter(match event {
 588                Ok(event) => self.map_event(event),
 589                Err(error) => vec![Err(error.into())],
 590            })
 591        })
 592    }
 593
 594    pub fn map_event(
 595        &mut self,
 596        event: ResponseStreamEvent,
 597    ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 598        let mut events = Vec::new();
 599
 600        if let Some(usage) = event.usage {
 601            events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 602                input_tokens: usage.prompt_tokens,
 603                output_tokens: usage.completion_tokens,
 604                cache_creation_input_tokens: 0,
 605                cache_read_input_tokens: 0,
 606            })));
 607        }
 608
 609        let Some(choice) = event.choices.first() else {
 610            return events;
 611        };
 612
 613        if let Some(details) = choice.delta.reasoning_details.clone() {
 614            // Emit reasoning_details immediately
 615            events.push(Ok(LanguageModelCompletionEvent::ReasoningDetails(
 616                details.clone(),
 617            )));
 618            self.reasoning_details = Some(details);
 619        }
 620
 621        if let Some(reasoning) = choice.delta.reasoning.clone() {
 622            events.push(Ok(LanguageModelCompletionEvent::Thinking {
 623                text: reasoning,
 624                signature: None,
 625            }));
 626        }
 627
 628        if let Some(content) = choice.delta.content.clone() {
 629            // OpenRouter send empty content string with the reasoning content
 630            // This is a workaround for the OpenRouter API bug
 631            if !content.is_empty() {
 632                events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 633            }
 634        }
 635
 636        if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
 637            for tool_call in tool_calls {
 638                let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
 639
 640                if let Some(tool_id) = tool_call.id.clone() {
 641                    entry.id = tool_id;
 642                }
 643
 644                if let Some(function) = tool_call.function.as_ref() {
 645                    if let Some(name) = function.name.clone() {
 646                        entry.name = name;
 647                    }
 648
 649                    if let Some(arguments) = function.arguments.clone() {
 650                        entry.arguments.push_str(&arguments);
 651                    }
 652
 653                    if let Some(signature) = function.thought_signature.clone() {
 654                        entry.thought_signature = Some(signature);
 655                    }
 656                }
 657
 658                if !entry.id.is_empty() && !entry.name.is_empty() {
 659                    if let Ok(input) = serde_json::from_str::<serde_json::Value>(
 660                        &partial_json_fixer::fix_json(&entry.arguments),
 661                    ) {
 662                        events.push(Ok(LanguageModelCompletionEvent::ToolUse(
 663                            LanguageModelToolUse {
 664                                id: entry.id.clone().into(),
 665                                name: entry.name.as_str().into(),
 666                                is_input_complete: false,
 667                                input,
 668                                raw_input: entry.arguments.clone(),
 669                                thought_signature: entry.thought_signature.clone(),
 670                            },
 671                        )));
 672                    }
 673                }
 674            }
 675        }
 676
 677        match choice.finish_reason.as_deref() {
 678            Some("stop") => {
 679                // Don't emit reasoning_details here - already emitted immediately when captured
 680                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 681            }
 682            Some("tool_calls") => {
 683                events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
 684                    match parse_tool_arguments(&tool_call.arguments) {
 685                        Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
 686                            LanguageModelToolUse {
 687                                id: tool_call.id.clone().into(),
 688                                name: tool_call.name.as_str().into(),
 689                                is_input_complete: true,
 690                                input,
 691                                raw_input: tool_call.arguments.clone(),
 692                                thought_signature: tool_call.thought_signature.clone(),
 693                            },
 694                        )),
 695                        Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
 696                            id: tool_call.id.clone().into(),
 697                            tool_name: tool_call.name.as_str().into(),
 698                            raw_input: tool_call.arguments.clone().into(),
 699                            json_parse_error: error.to_string(),
 700                        }),
 701                    }
 702                }));
 703
 704                // Don't emit reasoning_details here - already emitted immediately when captured
 705                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
 706            }
 707            Some(stop_reason) => {
 708                log::error!("Unexpected OpenRouter stop_reason: {stop_reason:?}",);
 709                // Don't emit reasoning_details here - already emitted immediately when captured
 710                events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 711            }
 712            None => {}
 713        }
 714
 715        events
 716    }
 717}
 718
 719#[derive(Default)]
 720struct RawToolCall {
 721    id: String,
 722    name: String,
 723    arguments: String,
 724    thought_signature: Option<String>,
 725}
 726
 727pub fn count_open_router_tokens(
 728    request: LanguageModelRequest,
 729    _model: open_router::Model,
 730    cx: &App,
 731) -> BoxFuture<'static, Result<u64>> {
 732    cx.background_spawn(async move {
 733        let messages = request
 734            .messages
 735            .into_iter()
 736            .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
 737                role: match message.role {
 738                    Role::User => "user".into(),
 739                    Role::Assistant => "assistant".into(),
 740                    Role::System => "system".into(),
 741                },
 742                content: Some(message.string_contents()),
 743                name: None,
 744                function_call: None,
 745            })
 746            .collect::<Vec<_>>();
 747
 748        tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages).map(|tokens| tokens as u64)
 749    })
 750    .boxed()
 751}
 752
 753struct ConfigurationView {
 754    api_key_editor: Entity<InputField>,
 755    state: Entity<State>,
 756    load_credentials_task: Option<Task<()>>,
 757}
 758
 759impl ConfigurationView {
 760    fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 761        let api_key_editor = cx.new(|cx| {
 762            InputField::new(
 763                window,
 764                cx,
 765                "sk_or_000000000000000000000000000000000000000000000000",
 766            )
 767        });
 768
 769        cx.observe(&state, |_, _, cx| {
 770            cx.notify();
 771        })
 772        .detach();
 773
 774        let load_credentials_task = Some(cx.spawn_in(window, {
 775            let state = state.clone();
 776            async move |this, cx| {
 777                if let Some(task) = Some(state.update(cx, |state, cx| state.authenticate(cx))) {
 778                    let _ = task.await;
 779                }
 780
 781                this.update(cx, |this, cx| {
 782                    this.load_credentials_task = None;
 783                    cx.notify();
 784                })
 785                .log_err();
 786            }
 787        }));
 788
 789        Self {
 790            api_key_editor,
 791            state,
 792            load_credentials_task,
 793        }
 794    }
 795
 796    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 797        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
 798        if api_key.is_empty() {
 799            return;
 800        }
 801
 802        // url changes can cause the editor to be displayed again
 803        self.api_key_editor
 804            .update(cx, |editor, cx| editor.set_text("", window, cx));
 805
 806        let state = self.state.clone();
 807        cx.spawn_in(window, async move |_, cx| {
 808            state
 809                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))
 810                .await
 811        })
 812        .detach_and_log_err(cx);
 813    }
 814
 815    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 816        self.api_key_editor
 817            .update(cx, |editor, cx| editor.set_text("", window, cx));
 818
 819        let state = self.state.clone();
 820        cx.spawn_in(window, async move |_, cx| {
 821            state
 822                .update(cx, |state, cx| state.set_api_key(None, cx))
 823                .await
 824        })
 825        .detach_and_log_err(cx);
 826    }
 827
 828    fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
 829        !self.state.read(cx).is_authenticated()
 830    }
 831}
 832
 833impl Render for ConfigurationView {
 834    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 835        let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
 836        let configured_card_label = if env_var_set {
 837            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
 838        } else {
 839            let api_url = OpenRouterLanguageModelProvider::api_url(cx);
 840            if api_url == OPEN_ROUTER_API_URL {
 841                "API key configured".to_string()
 842            } else {
 843                format!("API key configured for {}", api_url)
 844            }
 845        };
 846
 847        if self.load_credentials_task.is_some() {
 848            div()
 849                .child(Label::new("Loading credentials..."))
 850                .into_any_element()
 851        } else if self.should_render_editor(cx) {
 852            v_flex()
 853                .size_full()
 854                .on_action(cx.listener(Self::save_api_key))
 855                .child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:"))
 856                .child(
 857                    List::new()
 858                        .child(
 859                            ListBulletItem::new("")
 860                                .child(Label::new("Create an API key by visiting"))
 861                                .child(ButtonLink::new("OpenRouter's console", "https://openrouter.ai/keys"))
 862                        )
 863                        .child(ListBulletItem::new("Ensure your OpenRouter account has credits")
 864                        )
 865                        .child(ListBulletItem::new("Paste your API key below and hit enter to start using the assistant")
 866                        ),
 867                )
 868                .child(self.api_key_editor.clone())
 869                .child(
 870                    Label::new(
 871                        format!("You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
 872                    )
 873                    .size(LabelSize::Small).color(Color::Muted),
 874                )
 875                .into_any_element()
 876        } else {
 877            ConfiguredApiCard::new(configured_card_label)
 878                .disabled(env_var_set)
 879                .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
 880                .when(env_var_set, |this| {
 881                    this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
 882                })
 883                .into_any_element()
 884        }
 885    }
 886}
 887
 888#[cfg(test)]
 889mod tests {
 890    use super::*;
 891
 892    use open_router::{ChoiceDelta, FunctionChunk, ResponseMessageDelta, ToolCallChunk};
 893
 894    #[gpui::test]
 895    async fn test_reasoning_details_preservation_with_tool_calls() {
 896        // This test verifies that reasoning_details are properly captured and preserved
 897        // when a model uses tool calling with reasoning/thinking tokens.
 898        //
 899        // The key regression this prevents:
 900        // - OpenRouter sends multiple reasoning_details updates during streaming
 901        // - First with actual content (encrypted reasoning data)
 902        // - Then with empty array on completion
 903        // - We must NOT overwrite the real data with the empty array
 904
 905        let mut mapper = OpenRouterEventMapper::new();
 906
 907        // Simulate the streaming events as they come from OpenRouter/Gemini
 908        let events = vec![
 909            // Event 1: Initial reasoning details with text
 910            ResponseStreamEvent {
 911                id: Some("response_123".into()),
 912                created: 1234567890,
 913                model: "google/gemini-3.1-pro-preview".into(),
 914                choices: vec![ChoiceDelta {
 915                    index: 0,
 916                    delta: ResponseMessageDelta {
 917                        role: None,
 918                        content: None,
 919                        reasoning: None,
 920                        tool_calls: None,
 921                        reasoning_details: Some(serde_json::json!([
 922                            {
 923                                "type": "reasoning.text",
 924                                "text": "Let me analyze this request...",
 925                                "format": "google-gemini-v1",
 926                                "index": 0
 927                            }
 928                        ])),
 929                    },
 930                    finish_reason: None,
 931                }],
 932                usage: None,
 933            },
 934            // Event 2: More reasoning details
 935            ResponseStreamEvent {
 936                id: Some("response_123".into()),
 937                created: 1234567890,
 938                model: "google/gemini-3.1-pro-preview".into(),
 939                choices: vec![ChoiceDelta {
 940                    index: 0,
 941                    delta: ResponseMessageDelta {
 942                        role: None,
 943                        content: None,
 944                        reasoning: None,
 945                        tool_calls: None,
 946                        reasoning_details: Some(serde_json::json!([
 947                            {
 948                                "type": "reasoning.encrypted",
 949                                "data": "EtgDCtUDAdHtim9OF5jm4aeZSBAtl/randomized123",
 950                                "format": "google-gemini-v1",
 951                                "index": 0,
 952                                "id": "tool_call_abc123"
 953                            }
 954                        ])),
 955                    },
 956                    finish_reason: None,
 957                }],
 958                usage: None,
 959            },
 960            // Event 3: Tool call starts
 961            ResponseStreamEvent {
 962                id: Some("response_123".into()),
 963                created: 1234567890,
 964                model: "google/gemini-3.1-pro-preview".into(),
 965                choices: vec![ChoiceDelta {
 966                    index: 0,
 967                    delta: ResponseMessageDelta {
 968                        role: None,
 969                        content: None,
 970                        reasoning: None,
 971                        tool_calls: Some(vec![ToolCallChunk {
 972                            index: 0,
 973                            id: Some("tool_call_abc123".into()),
 974                            function: Some(FunctionChunk {
 975                                name: Some("list_directory".into()),
 976                                arguments: Some("{\"path\":\"test\"}".into()),
 977                                thought_signature: Some("sha256:test_signature_xyz789".into()),
 978                            }),
 979                        }]),
 980                        reasoning_details: None,
 981                    },
 982                    finish_reason: None,
 983                }],
 984                usage: None,
 985            },
 986            // Event 4: Empty reasoning_details on tool_calls finish
 987            // This is the critical event - we must not overwrite with this empty array!
 988            ResponseStreamEvent {
 989                id: Some("response_123".into()),
 990                created: 1234567890,
 991                model: "google/gemini-3.1-pro-preview".into(),
 992                choices: vec![ChoiceDelta {
 993                    index: 0,
 994                    delta: ResponseMessageDelta {
 995                        role: None,
 996                        content: None,
 997                        reasoning: None,
 998                        tool_calls: None,
 999                        reasoning_details: Some(serde_json::json!([])),
1000                    },
1001                    finish_reason: Some("tool_calls".into()),
1002                }],
1003                usage: None,
1004            },
1005        ];
1006
1007        // Process all events
1008        let mut collected_events = Vec::new();
1009        for event in events {
1010            let mapped = mapper.map_event(event);
1011            collected_events.extend(mapped);
1012        }
1013
1014        // Verify we got the expected events
1015        let mut has_tool_use = false;
1016        let mut reasoning_details_events = Vec::new();
1017        let mut thought_signature_value = None;
1018
1019        for event_result in collected_events {
1020            match event_result {
1021                Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
1022                    has_tool_use = true;
1023                    assert_eq!(tool_use.id.to_string(), "tool_call_abc123");
1024                    assert_eq!(tool_use.name.as_ref(), "list_directory");
1025                    thought_signature_value = tool_use.thought_signature.clone();
1026                }
1027                Ok(LanguageModelCompletionEvent::ReasoningDetails(details)) => {
1028                    reasoning_details_events.push(details);
1029                }
1030                _ => {}
1031            }
1032        }
1033
1034        // Assertions
1035        assert!(has_tool_use, "Should have emitted ToolUse event");
1036        assert!(
1037            !reasoning_details_events.is_empty(),
1038            "Should have emitted ReasoningDetails events"
1039        );
1040
1041        // We should have received multiple reasoning_details events (text, encrypted, empty)
1042        // The agent layer is responsible for keeping only the first non-empty one
1043        assert!(
1044            reasoning_details_events.len() >= 2,
1045            "Should have multiple reasoning_details events from streaming"
1046        );
1047
1048        // Verify at least one contains the encrypted data
1049        let has_encrypted = reasoning_details_events.iter().any(|details| {
1050            if let serde_json::Value::Array(arr) = details {
1051                arr.iter().any(|item| {
1052                    item["type"] == "reasoning.encrypted"
1053                        && item["data"]
1054                            .as_str()
1055                            .map_or(false, |s| s.contains("EtgDCtUDAdHtim9OF5jm4aeZSBAtl"))
1056                })
1057            } else {
1058                false
1059            }
1060        });
1061        assert!(
1062            has_encrypted,
1063            "Should have at least one reasoning_details with encrypted data"
1064        );
1065
1066        // Verify thought_signature was captured
1067        assert!(
1068            thought_signature_value.is_some(),
1069            "Tool use should have thought_signature"
1070        );
1071        assert_eq!(
1072            thought_signature_value.unwrap(),
1073            "sha256:test_signature_xyz789"
1074        );
1075    }
1076
1077    #[gpui::test]
1078    async fn test_usage_only_chunk_with_empty_choices_does_not_error() {
1079        let mut mapper = OpenRouterEventMapper::new();
1080
1081        let events = mapper.map_event(ResponseStreamEvent {
1082            id: Some("response_123".into()),
1083            created: 1234567890,
1084            model: "google/gemini-3-flash-preview".into(),
1085            choices: Vec::new(),
1086            usage: Some(open_router::Usage {
1087                prompt_tokens: 12,
1088                completion_tokens: 7,
1089                total_tokens: 19,
1090            }),
1091        });
1092
1093        assert_eq!(events.len(), 1);
1094        match events.into_iter().next().unwrap() {
1095            Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => {
1096                assert_eq!(usage.input_tokens, 12);
1097                assert_eq!(usage.output_tokens, 7);
1098            }
1099            other => panic!("Expected usage update event, got: {other:?}"),
1100        }
1101    }
1102
1103    #[gpui::test]
1104    async fn test_agent_prevents_empty_reasoning_details_overwrite() {
1105        // This test verifies that the agent layer prevents empty reasoning_details
1106        // from overwriting non-empty ones, even though the mapper emits all events.
1107
1108        // Simulate what the agent does when it receives multiple ReasoningDetails events
1109        let mut agent_reasoning_details: Option<serde_json::Value> = None;
1110
1111        let events = vec![
1112            // First event: non-empty reasoning_details
1113            serde_json::json!([
1114                {
1115                    "type": "reasoning.encrypted",
1116                    "data": "real_data_here",
1117                    "format": "google-gemini-v1"
1118                }
1119            ]),
1120            // Second event: empty array (should not overwrite)
1121            serde_json::json!([]),
1122        ];
1123
1124        for details in events {
1125            // This mimics the agent's logic: only store if we don't already have it
1126            if agent_reasoning_details.is_none() {
1127                agent_reasoning_details = Some(details);
1128            }
1129        }
1130
1131        // Verify the agent kept the first non-empty reasoning_details
1132        assert!(agent_reasoning_details.is_some());
1133        let final_details = agent_reasoning_details.unwrap();
1134        if let serde_json::Value::Array(arr) = &final_details {
1135            assert!(
1136                !arr.is_empty(),
1137                "Agent should have kept the non-empty reasoning_details"
1138            );
1139            assert_eq!(arr[0]["data"], "real_data_here");
1140        } else {
1141            panic!("Expected array");
1142        }
1143    }
1144}