open_router.rs

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