use anyhow::Result;use collections::HashMap;use credentials_provider::CredentialsProvider;use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task};use http_client::{CustomHeaders, HttpClient};use language_model::{ ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, ProviderSettingsView, RateLimiter, Role, StopReason, TokenUsage, env_var,};use open_router::{ Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,};use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore};use std::pin::Pin;use std::sync::{Arc, LazyLock};use ui::IconName; use language_model::util::{fix_streamed_json, parse_tool_arguments}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter"); const API_KEY_ENV_VAR_NAME: &str = "OPENROUTER_API_KEY";static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);pub(crate) const RESERVED_HEADER_NAMES: &[&str] = &["HTTP-Referer", "X-Title"];const MAX_OPEN_ROUTER_SESSION_ID_LENGTH: usize = 256; #[derive(Default, Clone, Debug, PartialEq)]pub struct OpenRouterSettings { pub api_url: String, pub available_models: Vec<AvailableModel>, pub custom_headers: CustomHeaders,} pub struct OpenRouterLanguageModelProvider { http_client: Arc<dyn HttpClient>, state: Entity<State>,} pub struct State { api_key_state: ApiKeyState, credentials_provider: Arc<dyn CredentialsProvider>, http_client: Arc<dyn HttpClient>, available_models: Vec<open_router::Model>, fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,} impl State { fn is_authenticated(&self) -> bool { self.api_key_state.has_key() } fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> { let credentials_provider = self.credentials_provider.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let task = self.api_key_state.store( api_url, api_key, |this| &mut this.api_key_state, credentials_provider, cx, ); cx.spawn(async move |this, cx| { let result = task.await?; this.update(cx, |this, cx| this.restart_fetch_models_task(cx)) .ok(); Ok(result) }) } fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> { let credentials_provider = self.credentials_provider.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let task = self.api_key_state.load_if_needed( api_url, |this| &mut this.api_key_state, credentials_provider, cx, ); cx.spawn(async move |this, cx| { let result = task.await; this.update(cx, |this, cx| this.restart_fetch_models_task(cx)) .ok(); result }) } fn fetch_models( &mut self, cx: &mut Context<Self>, ) -> Task<Result<(), LanguageModelCompletionError>> { let http_client = self.http_client.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let extra_headers = OpenRouterLanguageModelProvider::settings(cx) .custom_headers .clone(); let Some(api_key) = self.api_key_state.key(&api_url) else { return Task::ready(Err(LanguageModelCompletionError::NoApiKey { provider: PROVIDER_NAME, })); }; cx.spawn(async move |this, cx| { let models = list_models(http_client.as_ref(), &api_url, &api_key, &extra_headers) .await .map_err(LanguageModelCompletionError::from)?; this.update(cx, |this, cx| { this.available_models = models; cx.notify(); }) .map_err(|e| LanguageModelCompletionError::Other(e))?; Ok(()) }) } fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) { if self.is_authenticated() { let task = self.fetch_models(cx); self.fetch_models_task.replace(task); } else { self.available_models.clear(); } }} impl OpenRouterLanguageModelProvider { pub fn new( http_client: Arc<dyn HttpClient>, credentials_provider: Arc<dyn CredentialsProvider>, cx: &mut App, ) -> Self { let state = cx.new(|cx| { cx.observe_global::<SettingsStore>({ let mut last_settings = OpenRouterLanguageModelProvider::settings(cx).clone(); move |this: &mut State, cx| { let current_settings = OpenRouterLanguageModelProvider::settings(cx); let settings_changed = current_settings != &last_settings; if settings_changed { last_settings = current_settings.clone(); this.authenticate(cx).detach(); cx.notify(); } } }) .detach(); State { api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), credentials_provider, http_client: http_client.clone(), available_models: Vec::new(), fetch_models_task: None, } }); Self { http_client, state } } fn settings(cx: &App) -> &OpenRouterSettings { &crate::AllLanguageModelSettings::get_global(cx).open_router } fn api_url(cx: &App) -> SharedString { let api_url = &Self::settings(cx).api_url; if api_url.is_empty() { OPEN_ROUTER_API_URL.into() } else { SharedString::new(api_url.as_str()) } } fn create_language_model(&self, model: open_router::Model) -> Arc<dyn LanguageModel> { Arc::new(OpenRouterLanguageModel { id: LanguageModelId::from(model.id().to_string()), model, state: self.state.clone(), http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) }} impl LanguageModelProviderState for OpenRouterLanguageModelProvider { type ObservableEntity = State; fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> { Some(self.state.clone()) }} impl LanguageModelProvider for OpenRouterLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn icon(&self) -> IconOrSvg { IconOrSvg::Icon(IconName::AiOpenRouter) } fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { Some(self.create_language_model(open_router::Model::default())) } fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { None } fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let mut models_from_api = self.state.read(cx).available_models.clone(); let mut settings_models = Vec::new(); for model in &Self::settings(cx).available_models { settings_models.push(open_router::Model { name: model.name.clone(), display_name: model.display_name.clone(), max_tokens: model.max_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, mode: model.mode.unwrap_or_default(), provider: model.provider.clone(), }); } for settings_model in &settings_models { if let Some(pos) = models_from_api .iter() .position(|m| m.name == settings_model.name) { models_from_api[pos] = settings_model.clone(); } else { models_from_api.push(settings_model.clone()); } } models_from_api .into_iter() .map(|model| self.create_language_model(model)) .collect() } fn is_authenticated(&self, cx: &App) -> bool { self.state.read(cx).is_authenticated() } fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> { self.state.update(cx, |state, cx| state.authenticate(cx)) } fn settings_view(&self, cx: &mut App) -> Option<ProviderSettingsView> { let state = self.state.read(cx); Some(ProviderSettingsView::ApiKey(ApiKeyConfiguration::new( state.api_key_state.has_key(), state.api_key_state.is_from_env_var(), state.api_key_state.env_var_name().clone(), "https://openrouter.ai/keys".into(), ))) } fn set_api_key(&self, api_key: Option<String>, cx: &mut App) -> Task<Result<()>> { self.state .update(cx, |state, cx| state.set_api_key(api_key, cx)) }} pub struct OpenRouterLanguageModel { id: LanguageModelId, model: open_router::Model, state: Entity<State>, http_client: Arc<dyn HttpClient>, request_limiter: RateLimiter,} impl OpenRouterLanguageModel { fn stream_completion( &self, request: open_router::Request, cx: &AsyncApp, ) -> BoxFuture< 'static, Result< futures::stream::BoxStream< 'static, Result<ResponseStreamEvent, open_router::OpenRouterError>, >, LanguageModelCompletionError, >, > { let http_client = self.http_client.clone(); let (api_key, api_url, extra_headers) = self.state.read_with(cx, |state, cx| { let api_url = OpenRouterLanguageModelProvider::api_url(cx); let extra_headers = OpenRouterLanguageModelProvider::settings(cx) .custom_headers .clone(); (state.api_key_state.key(&api_url), api_url, extra_headers) }); async move { let Some(api_key) = api_key else { return Err(LanguageModelCompletionError::NoApiKey { provider: PROVIDER_NAME, }); }; let request = open_router::stream_completion( http_client.as_ref(), &api_url, &api_key, request, &extra_headers, ); request.await.map_err(Into::into) } .boxed() }} impl LanguageModel for OpenRouterLanguageModel { fn id(&self) -> LanguageModelId { self.id.clone() } fn name(&self) -> LanguageModelName { LanguageModelName::from(self.model.display_name().to_string()) } fn provider_id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn provider_name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn supports_tools(&self) -> bool { self.model.supports_tool_calls() } fn supports_streaming_tools(&self) -> bool { true } fn supports_thinking(&self) -> bool { matches!(self.model.mode, OpenRouterModelMode::Thinking { .. }) } fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); if model_id.contains("gemini") || model_id.contains("grok") { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema } } fn telemetry_id(&self) -> String { format!("openrouter/{}", self.model.id()) } fn max_token_count(&self) -> u64 { self.model.max_token_count() } fn max_output_tokens(&self) -> Option<u64> { self.model.max_output_tokens() } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { LanguageModelToolChoice::Auto => true, LanguageModelToolChoice::Any => true, LanguageModelToolChoice::None => true, } } fn supports_images(&self) -> bool { self.model.supports_images.unwrap_or(false) } fn stream_completion( &self, request: LanguageModelRequest, cx: &AsyncApp, ) -> BoxFuture< 'static, Result< futures::stream::BoxStream< 'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, >, LanguageModelCompletionError, >, > { let openrouter_request = match into_open_router(request, &self.model, self.max_output_tokens()) { Ok(request) => request, Err(error) => return async move { Err(error.into()) }.boxed(), }; let request = self.stream_completion(openrouter_request, cx); let future = self.request_limiter.stream(async move { let response = request.await?; Ok(OpenRouterEventMapper::new().map_stream(response)) }); async move { Ok(future.await?.boxed()) }.boxed() }} pub fn into_open_router( request: LanguageModelRequest, model: &Model, max_output_tokens: Option<u64>,) -> Result<open_router::Request> { if request.contains_custom_tool_input() { anyhow::bail!("OpenRouter does not support custom tools"); } // Anthropic models via OpenRouter don't accept reasoning_details being echoed back // in requests - it's an output-only field for them. However, Gemini models require // the thought signatures to be echoed back for proper reasoning chain continuity. // Note: OpenRouter's model API provides an `architecture.tokenizer` field (e.g. "Claude", // "Gemini") which could replace this ID prefix check, but since this is the only place // we need this distinction, we're just using this less invasive check instead. // If we ever have a more formal distionction between the models in the future, // we should revise this to use that instead. let is_anthropic_model = model.id().starts_with("anthropic/"); let session_id = open_router_session_id(request.thread_id); let mut messages = Vec::new(); let mut any_message_wants_cache = false; let mut last_cache_message_index: Option<usize> = None; for message in request.messages { let mut message_added_content = false; let reasoning_details_for_message = if is_anthropic_model { None } else { message.reasoning_details.clone() }; let message_wants_cache = message.cache; if message_wants_cache { any_message_wants_cache = true; } for content in message.content { match content { MessageContent::Text(text) => { add_message_content_part( open_router::MessagePart::Text { text, cache_control: None, }, message.role, &mut messages, reasoning_details_for_message.clone(), ); message_added_content = true; } MessageContent::Thinking { .. } => {} MessageContent::RedactedThinking(_) => {} MessageContent::Compaction(_) => {} MessageContent::Image(image) => { add_message_content_part( open_router::MessagePart::Image { image_url: image.to_base64_url(), }, message.role, &mut messages, reasoning_details_for_message.clone(), ); message_added_content = true; } MessageContent::ToolUse(tool_use) => { let input = tool_use.input.as_json().ok_or_else(|| { anyhow::anyhow!("OpenRouter does not support custom tool calls") })?; let tool_call = open_router::ToolCall { id: tool_use.id.to_string(), content: open_router::ToolCallContent::Function { function: open_router::FunctionContent { name: tool_use.name.to_string(), arguments: serde_json::to_string(input).unwrap_or_default(), thought_signature: tool_use.thought_signature.clone(), }, }, }; if let Some(open_router::RequestMessage::Assistant { tool_calls, .. }) = messages.last_mut() { tool_calls.push(tool_call);Lines 1–500 of 1464. Load more
use anyhow::Result; use collections::HashMap; use credentials_provider::CredentialsProvider; use futures::{FutureExt, Stream, StreamExt, future::BoxFuture}; use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task}; use http_client::{CustomHeaders, HttpClient}; use language_model::{ ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, ProviderSettingsView, RateLimiter, Role, StopReason, TokenUsage, env_var, }; use open_router::{ Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models, }; use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore}; use std::pin::Pin; use std::sync::{Arc, LazyLock}; use ui::IconName; use language_model::util::{fix_streamed_json, parse_tool_arguments}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter"); const API_KEY_ENV_VAR_NAME: &str = "OPENROUTER_API_KEY"; static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME); pub(crate) const RESERVED_HEADER_NAMES: &[&str] = &["HTTP-Referer", "X-Title"]; const MAX_OPEN_ROUTER_SESSION_ID_LENGTH: usize = 256; #[derive(Default, Clone, Debug, PartialEq)] pub struct OpenRouterSettings { pub api_url: String, pub available_models: Vec<AvailableModel>, pub custom_headers: CustomHeaders, } pub struct OpenRouterLanguageModelProvider { http_client: Arc<dyn HttpClient>, state: Entity<State>, } pub struct State { api_key_state: ApiKeyState, credentials_provider: Arc<dyn CredentialsProvider>, http_client: Arc<dyn HttpClient>, available_models: Vec<open_router::Model>, fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>, } impl State { fn is_authenticated(&self) -> bool { self.api_key_state.has_key() } fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> { let credentials_provider = self.credentials_provider.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let task = self.api_key_state.store( api_url, api_key, |this| &mut this.api_key_state, credentials_provider, cx, ); cx.spawn(async move |this, cx| { let result = task.await?; this.update(cx, |this, cx| this.restart_fetch_models_task(cx)) .ok(); Ok(result) }) } fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> { let credentials_provider = self.credentials_provider.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let task = self.api_key_state.load_if_needed( api_url, |this| &mut this.api_key_state, credentials_provider, cx, ); cx.spawn(async move |this, cx| { let result = task.await; this.update(cx, |this, cx| this.restart_fetch_models_task(cx)) .ok(); result }) } fn fetch_models( &mut self, cx: &mut Context<Self>, ) -> Task<Result<(), LanguageModelCompletionError>> { let http_client = self.http_client.clone(); let api_url = OpenRouterLanguageModelProvider::api_url(cx); let extra_headers = OpenRouterLanguageModelProvider::settings(cx) .custom_headers .clone(); let Some(api_key) = self.api_key_state.key(&api_url) else { return Task::ready(Err(LanguageModelCompletionError::NoApiKey { provider: PROVIDER_NAME, })); }; cx.spawn(async move |this, cx| { let models = list_models(http_client.as_ref(), &api_url, &api_key, &extra_headers) .await .map_err(LanguageModelCompletionError::from)?; this.update(cx, |this, cx| { this.available_models = models; cx.notify(); }) .map_err(|e| LanguageModelCompletionError::Other(e))?; Ok(()) }) } fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) { if self.is_authenticated() { let task = self.fetch_models(cx); self.fetch_models_task.replace(task); } else { self.available_models.clear(); } } } impl OpenRouterLanguageModelProvider { pub fn new( http_client: Arc<dyn HttpClient>, credentials_provider: Arc<dyn CredentialsProvider>, cx: &mut App, ) -> Self { let state = cx.new(|cx| { cx.observe_global::<SettingsStore>({ let mut last_settings = OpenRouterLanguageModelProvider::settings(cx).clone(); move |this: &mut State, cx| { let current_settings = OpenRouterLanguageModelProvider::settings(cx); let settings_changed = current_settings != &last_settings; if settings_changed { last_settings = current_settings.clone(); this.authenticate(cx).detach(); cx.notify(); } } }) .detach(); State { api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()), credentials_provider, http_client: http_client.clone(), available_models: Vec::new(), fetch_models_task: None, } }); Self { http_client, state } } fn settings(cx: &App) -> &OpenRouterSettings { &crate::AllLanguageModelSettings::get_global(cx).open_router } fn api_url(cx: &App) -> SharedString { let api_url = &Self::settings(cx).api_url; if api_url.is_empty() { OPEN_ROUTER_API_URL.into() } else { SharedString::new(api_url.as_str()) } } fn create_language_model(&self, model: open_router::Model) -> Arc<dyn LanguageModel> { Arc::new(OpenRouterLanguageModel { id: LanguageModelId::from(model.id().to_string()), model, state: self.state.clone(), http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) } } impl LanguageModelProviderState for OpenRouterLanguageModelProvider { type ObservableEntity = State; fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> { Some(self.state.clone()) } } impl LanguageModelProvider for OpenRouterLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn icon(&self) -> IconOrSvg { IconOrSvg::Icon(IconName::AiOpenRouter) } fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { Some(self.create_language_model(open_router::Model::default())) } fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { None } fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { let mut models_from_api = self.state.read(cx).available_models.clone(); let mut settings_models = Vec::new(); for model in &Self::settings(cx).available_models { settings_models.push(open_router::Model { name: model.name.clone(), display_name: model.display_name.clone(), max_tokens: model.max_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, mode: model.mode.unwrap_or_default(), provider: model.provider.clone(), }); } for settings_model in &settings_models { if let Some(pos) = models_from_api .iter() .position(|m| m.name == settings_model.name) { models_from_api[pos] = settings_model.clone(); } else { models_from_api.push(settings_model.clone()); } } models_from_api .into_iter() .map(|model| self.create_language_model(model)) .collect() } fn is_authenticated(&self, cx: &App) -> bool { self.state.read(cx).is_authenticated() } fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> { self.state.update(cx, |state, cx| state.authenticate(cx)) } fn settings_view(&self, cx: &mut App) -> Option<ProviderSettingsView> { let state = self.state.read(cx); Some(ProviderSettingsView::ApiKey(ApiKeyConfiguration::new( state.api_key_state.has_key(), state.api_key_state.is_from_env_var(), state.api_key_state.env_var_name().clone(), "https://openrouter.ai/keys".into(), ))) } fn set_api_key(&self, api_key: Option<String>, cx: &mut App) -> Task<Result<()>> { self.state .update(cx, |state, cx| state.set_api_key(api_key, cx)) } } pub struct OpenRouterLanguageModel { id: LanguageModelId, model: open_router::Model, state: Entity<State>, http_client: Arc<dyn HttpClient>, request_limiter: RateLimiter, } impl OpenRouterLanguageModel { fn stream_completion( &self, request: open_router::Request, cx: &AsyncApp, ) -> BoxFuture< 'static, Result< futures::stream::BoxStream< 'static, Result<ResponseStreamEvent, open_router::OpenRouterError>, >, LanguageModelCompletionError, >, > { let http_client = self.http_client.clone(); let (api_key, api_url, extra_headers) = self.state.read_with(cx, |state, cx| { let api_url = OpenRouterLanguageModelProvider::api_url(cx); let extra_headers = OpenRouterLanguageModelProvider::settings(cx) .custom_headers .clone(); (state.api_key_state.key(&api_url), api_url, extra_headers) }); async move { let Some(api_key) = api_key else { return Err(LanguageModelCompletionError::NoApiKey { provider: PROVIDER_NAME, }); }; let request = open_router::stream_completion( http_client.as_ref(), &api_url, &api_key, request, &extra_headers, ); request.await.map_err(Into::into) } .boxed() } } impl LanguageModel for OpenRouterLanguageModel { fn id(&self) -> LanguageModelId { self.id.clone() } fn name(&self) -> LanguageModelName { LanguageModelName::from(self.model.display_name().to_string()) } fn provider_id(&self) -> LanguageModelProviderId { PROVIDER_ID } fn provider_name(&self) -> LanguageModelProviderName { PROVIDER_NAME } fn supports_tools(&self) -> bool { self.model.supports_tool_calls() } fn supports_streaming_tools(&self) -> bool { true } fn supports_thinking(&self) -> bool { matches!(self.model.mode, OpenRouterModelMode::Thinking { .. }) } fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); if model_id.contains("gemini") || model_id.contains("grok") { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema } } fn telemetry_id(&self) -> String { format!("openrouter/{}", self.model.id()) } fn max_token_count(&self) -> u64 { self.model.max_token_count() } fn max_output_tokens(&self) -> Option<u64> { self.model.max_output_tokens() } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { LanguageModelToolChoice::Auto => true, LanguageModelToolChoice::Any => true, LanguageModelToolChoice::None => true, } } fn supports_images(&self) -> bool { self.model.supports_images.unwrap_or(false) } fn stream_completion( &self, request: LanguageModelRequest, cx: &AsyncApp, ) -> BoxFuture< 'static, Result< futures::stream::BoxStream< 'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, >, LanguageModelCompletionError, >, > { let openrouter_request = match into_open_router(request, &self.model, self.max_output_tokens()) { Ok(request) => request, Err(error) => return async move { Err(error.into()) }.boxed(), }; let request = self.stream_completion(openrouter_request, cx); let future = self.request_limiter.stream(async move { let response = request.await?; Ok(OpenRouterEventMapper::new().map_stream(response)) }); async move { Ok(future.await?.boxed()) }.boxed() } } pub fn into_open_router( request: LanguageModelRequest, model: &Model, max_output_tokens: Option<u64>, ) -> Result<open_router::Request> { if request.contains_custom_tool_input() { anyhow::bail!("OpenRouter does not support custom tools"); } // Anthropic models via OpenRouter don't accept reasoning_details being echoed back // in requests - it's an output-only field for them. However, Gemini models require // the thought signatures to be echoed back for proper reasoning chain continuity. // Note: OpenRouter's model API provides an `architecture.tokenizer` field (e.g. "Claude", // "Gemini") which could replace this ID prefix check, but since this is the only place // we need this distinction, we're just using this less invasive check instead. // If we ever have a more formal distionction between the models in the future, // we should revise this to use that instead. let is_anthropic_model = model.id().starts_with("anthropic/"); let session_id = open_router_session_id(request.thread_id); let mut messages = Vec::new(); let mut any_message_wants_cache = false; let mut last_cache_message_index: Option<usize> = None; for message in request.messages { let mut message_added_content = false; let reasoning_details_for_message = if is_anthropic_model { None } else { message.reasoning_details.clone() }; let message_wants_cache = message.cache; if message_wants_cache { any_message_wants_cache = true; } for content in message.content { match content { MessageContent::Text(text) => { add_message_content_part( open_router::MessagePart::Text { text, cache_control: None, }, message.role, &mut messages, reasoning_details_for_message.clone(), ); message_added_content = true; } MessageContent::Thinking { .. } => {} MessageContent::RedactedThinking(_) => {} MessageContent::Compaction(_) => {} MessageContent::Image(image) => { add_message_content_part( open_router::MessagePart::Image { image_url: image.to_base64_url(), }, message.role, &mut messages, reasoning_details_for_message.clone(), ); message_added_content = true; } MessageContent::ToolUse(tool_use) => { let input = tool_use.input.as_json().ok_or_else(|| { anyhow::anyhow!("OpenRouter does not support custom tool calls") })?; let tool_call = open_router::ToolCall { id: tool_use.id.to_string(), content: open_router::ToolCallContent::Function { function: open_router::FunctionContent { name: tool_use.name.to_string(), arguments: serde_json::to_string(input).unwrap_or_default(), thought_signature: tool_use.thought_signature.clone(), }, }, }; if let Some(open_router::RequestMessage::Assistant { tool_calls, .. }) = messages.last_mut() { tool_calls.push(tool_call);