From 526196917b40287462013c70eb1b61c1332effa8 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:04:26 +0530 Subject: [PATCH] language_models: Add support for API key to Ollama provider (#34110) Closes https://github.com/zed-industries/zed/issues/19491 Release Notes: - Ollama: Added configuration of URL and API key for remote Ollama provider. --------- Signed-off-by: Umesh Yadav Co-authored-by: Peter Tripp Co-authored-by: Oliver Azevedo Barnes Co-authored-by: Michael Sloan --- Cargo.lock | 1 + crates/language_models/Cargo.toml | 1 + crates/language_models/src/provider/ollama.rs | 516 +++++++++++++----- crates/ollama/src/ollama.rs | 36 +- docs/src/ai/llm-providers.md | 14 + 5 files changed, 414 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22589ee11a4ffb657238091ab85a3e76d9b6bf32..14ec0e21db3a1893002c6277c43b9d1b996ab086 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9179,6 +9179,7 @@ dependencies = [ "credentials_provider", "deepseek", "editor", + "fs", "futures 0.3.31", "google_ai", "gpui", diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 7dc0988d23579c4d1ab1ac2dde1f1413c5e751b8..8a2a681c26ede21ce948b6667b4aaea589724dcf 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -29,6 +29,7 @@ copilot.workspace = true credentials_provider.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true +fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index a80cacfc4a02521af74b32c34cc3360e9665a7d9..4932d9507027296003dd8ae095318718e9019334 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -1,7 +1,8 @@ use anyhow::{Result, anyhow}; +use fs::Fs; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream}; use futures::{Stream, TryFutureExt, stream}; -use gpui::{AnyView, App, AsyncApp, Context, Subscription, Task}; +use gpui::{AnyView, App, AsyncApp, Context, Task}; use http_client::HttpClient; use language_model::{ AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, @@ -10,20 +11,25 @@ use language_model::{ LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; +use menu; use ollama::{ - ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall, - OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion, + ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OLLAMA_API_URL, + OllamaFunctionCall, OllamaFunctionTool, OllamaToolCall, get_models, show_model, + stream_chat_completion, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, update_settings_file}; use std::pin::Pin; +use std::sync::LazyLock; use std::sync::atomic::{AtomicU64, Ordering}; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonLike, Indicator, List, prelude::*}; -use util::ResultExt; +use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*}; +use ui_input::SingleLineInput; +use zed_env_vars::{EnvVar, env_var}; use crate::AllLanguageModelSettings; +use crate::api_key::ApiKeyState; use crate::ui::InstructionListItem; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; @@ -33,6 +39,9 @@ const OLLAMA_SITE: &str = "https://ollama.com/"; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("ollama"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Ollama"); +const API_KEY_ENV_VAR_NAME: &str = "OLLAMA_API_KEY"; +static API_KEY_ENV_VAR: LazyLock = env_var!(API_KEY_ENV_VAR_NAME); + #[derive(Default, Debug, Clone, PartialEq)] pub struct OllamaSettings { pub api_url: String, @@ -63,25 +72,61 @@ pub struct OllamaLanguageModelProvider { } pub struct State { + api_key_state: ApiKeyState, http_client: Arc, - available_models: Vec, + fetched_models: Vec, fetch_model_task: Option>>, - _subscription: Subscription, } impl State { fn is_authenticated(&self) -> bool { - !self.available_models.is_empty() + !self.fetched_models.is_empty() + } + + fn set_api_key(&mut self, api_key: Option, cx: &mut Context) -> Task> { + let api_url = OllamaLanguageModelProvider::api_url(cx); + let task = self + .api_key_state + .store(api_url, api_key, |this| &mut this.api_key_state, cx); + + self.fetched_models.clear(); + cx.spawn(async move |this, cx| { + let result = task.await; + this.update(cx, |this, cx| this.restart_fetch_models_task(cx)) + .ok(); + result + }) + } + + fn authenticate(&mut self, cx: &mut Context) -> Task> { + let api_url = OllamaLanguageModelProvider::api_url(cx); + let task = self.api_key_state.load_if_needed( + api_url, + &API_KEY_ENV_VAR, + |this| &mut this.api_key_state, + cx, + ); + + // Always try to fetch models - if no API key is needed (local Ollama), it will work + // If API key is needed and provided, it will work + // If API key is needed and not provided, it will fail gracefully + 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) -> Task> { - let settings = &AllLanguageModelSettings::get_global(cx).ollama; let http_client = Arc::clone(&self.http_client); - let api_url = settings.api_url.clone(); + let api_url = OllamaLanguageModelProvider::api_url(cx); + let api_key = self.api_key_state.key(&api_url); // As a proxy for the server being "authenticated", we'll check if its up by fetching the models cx.spawn(async move |this, cx| { - let models = get_models(http_client.as_ref(), &api_url, None).await?; + let models = + get_models(http_client.as_ref(), &api_url, api_key.as_deref(), None).await?; let tasks = models .into_iter() @@ -92,9 +137,12 @@ impl State { .map(|model| { let http_client = Arc::clone(&http_client); let api_url = api_url.clone(); + let api_key = api_key.clone(); async move { let name = model.name.as_str(); - let capabilities = show_model(http_client.as_ref(), &api_url, name).await?; + let capabilities = + show_model(http_client.as_ref(), &api_url, api_key.as_deref(), name) + .await?; let ollama_model = ollama::Model::new( name, None, @@ -119,7 +167,7 @@ impl State { ollama_models.sort_by(|a, b| a.name.cmp(&b.name)); this.update(cx, |this, cx| { - this.available_models = ollama_models; + this.fetched_models = ollama_models; cx.notify(); }) }) @@ -129,15 +177,6 @@ impl State { let task = self.fetch_models(cx); self.fetch_model_task.replace(task); } - - fn authenticate(&mut self, cx: &mut Context) -> Task> { - if self.is_authenticated() { - return Task::ready(Ok(())); - } - - let fetch_models_task = self.fetch_models(cx); - cx.spawn(async move |_this, _cx| Ok(fetch_models_task.await?)) - } } impl OllamaLanguageModelProvider { @@ -145,30 +184,47 @@ impl OllamaLanguageModelProvider { let this = Self { http_client: http_client.clone(), state: cx.new(|cx| { - let subscription = cx.observe_global::({ - let mut settings = AllLanguageModelSettings::get_global(cx).ollama.clone(); + cx.observe_global::({ + let mut last_settings = OllamaLanguageModelProvider::settings(cx).clone(); move |this: &mut State, cx| { - let new_settings = &AllLanguageModelSettings::get_global(cx).ollama; - if &settings != new_settings { - settings = new_settings.clone(); - this.restart_fetch_models_task(cx); + let current_settings = OllamaLanguageModelProvider::settings(cx); + let settings_changed = current_settings != &last_settings; + if settings_changed { + let url_changed = last_settings.api_url != current_settings.api_url; + last_settings = current_settings.clone(); + if url_changed { + this.fetched_models.clear(); + this.authenticate(cx).detach(); + } cx.notify(); } } - }); + }) + .detach(); State { http_client, - available_models: Default::default(), + fetched_models: Default::default(), fetch_model_task: None, - _subscription: subscription, + api_key_state: ApiKeyState::new(Self::api_url(cx)), } }), }; - this.state - .update(cx, |state, cx| state.restart_fetch_models_task(cx)); this } + + fn settings(cx: &App) -> &OllamaSettings { + &AllLanguageModelSettings::get_global(cx).ollama + } + + fn api_url(cx: &App) -> SharedString { + let api_url = &Self::settings(cx).api_url; + if api_url.is_empty() { + OLLAMA_API_URL.into() + } else { + SharedString::new(api_url.as_str()) + } + } } impl LanguageModelProviderState for OllamaLanguageModelProvider { @@ -208,16 +264,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { let mut models: HashMap = HashMap::new(); // Add models from the Ollama API - for model in self.state.read(cx).available_models.iter() { + for model in self.state.read(cx).fetched_models.iter() { models.insert(model.name.clone(), model.clone()); } // Override with available models from settings - for model in AllLanguageModelSettings::get_global(cx) - .ollama - .available_models - .iter() - { + for model in &OllamaLanguageModelProvider::settings(cx).available_models { models.insert( model.name.clone(), ollama::Model { @@ -240,6 +292,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), + state: self.state.clone(), }) as Arc }) .collect::>(); @@ -267,7 +320,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { } fn reset_credentials(&self, cx: &mut App) -> Task> { - self.state.update(cx, |state, cx| state.fetch_models(cx)) + self.state + .update(cx, |state, cx| state.set_api_key(None, cx)) } } @@ -276,6 +330,7 @@ pub struct OllamaLanguageModel { model: ollama::Model, http_client: Arc, request_limiter: RateLimiter, + state: gpui::Entity, } impl OllamaLanguageModel { @@ -454,15 +509,17 @@ impl LanguageModel for OllamaLanguageModel { let request = self.to_ollama_request(request); let http_client = self.http_client.clone(); - let Ok(api_url) = cx.update(|cx| { - let settings = &AllLanguageModelSettings::get_global(cx).ollama; - settings.api_url.clone() + let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| { + let api_url = OllamaLanguageModelProvider::api_url(cx); + (state.api_key_state.key(&api_url), api_url) }) else { return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed(); }; let future = self.request_limiter.stream(async move { - let stream = stream_chat_completion(http_client.as_ref(), &api_url, request).await?; + let stream = + stream_chat_completion(http_client.as_ref(), &api_url, api_key.as_deref(), request) + .await?; let stream = map_to_language_model_completion_events(stream); Ok(stream) }); @@ -574,138 +631,305 @@ fn map_to_language_model_completion_events( } struct ConfigurationView { + api_key_editor: gpui::Entity, + api_url_editor: gpui::Entity, state: gpui::Entity, - loading_models_task: Option>, } impl ConfigurationView { pub fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { - let loading_models_task = Some(cx.spawn_in(window, { - let state = state.clone(); - async move |this, cx| { - if let Some(task) = state - .update(cx, |state, cx| state.authenticate(cx)) - .log_err() - { - task.await.log_err(); - } - this.update(cx, |this, cx| { - this.loading_models_task = None; - cx.notify(); - }) - .log_err(); - } - })); + let api_key_editor = + cx.new(|cx| SingleLineInput::new(window, cx, "63e02e...").label("API key")); + + let api_url_editor = cx.new(|cx| { + let input = SingleLineInput::new(window, cx, OLLAMA_API_URL).label("API URL"); + input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx); + input + }); + + cx.observe(&state, |_, _, cx| { + cx.notify(); + }) + .detach(); Self { + api_key_editor, + api_url_editor, state, - loading_models_task, } } fn retry_connection(&self, cx: &mut App) { self.state - .update(cx, |state, cx| state.fetch_models(cx)) - .detach_and_log_err(cx); + .update(cx, |state, cx| state.restart_fetch_models_task(cx)); } -} -impl Render for ConfigurationView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_authenticated = self.state.read(cx).is_authenticated(); + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string(); + if api_key.is_empty() { + return; + } - let ollama_intro = - "Get up & running with Llama 3.3, Mistral, Gemma 2, and other LLMs with Ollama."; + // url changes can cause the editor to be displayed again + self.api_key_editor + .update(cx, |input, cx| input.set_text("", window, cx)); - if self.loading_models_task.is_some() { - div().child(Label::new("Loading models...")).into_any() - } else { + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state + .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))? + .await + }) + .detach_and_log_err(cx); + } + + fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context) { + self.api_key_editor + .update(cx, |input, cx| input.set_text("", window, cx)); + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state + .update(cx, |state, cx| state.set_api_key(None, cx))? + .await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn save_api_url(&mut self, cx: &mut Context) { + let api_url = self.api_url_editor.read(cx).text(cx).trim().to_string(); + let current_url = OllamaLanguageModelProvider::api_url(cx); + if !api_url.is_empty() && &api_url != ¤t_url { + let fs = ::global(cx); + update_settings_file::(fs, cx, move |settings, _| { + if let Some(settings) = settings.ollama.as_mut() { + settings.api_url = Some(api_url); + } else { + settings.ollama = Some(crate::settings::OllamaSettingsContent { + api_url: Some(api_url), + available_models: None, + }); + } + }); + } + } + + fn reset_api_url(&mut self, window: &mut Window, cx: &mut Context) { + self.api_url_editor + .update(cx, |input, cx| input.set_text("", window, cx)); + let fs = ::global(cx); + update_settings_file::(fs, cx, |settings, _cx| { + if let Some(settings) = settings.ollama.as_mut() { + settings.api_url = Some(OLLAMA_API_URL.into()); + } + }); + cx.notify(); + } + + fn render_instructions() -> Div { + v_flex() + .gap_2() + .child(Label::new( + "Run LLMs locally on your machine with Ollama, or connect to an Ollama server. \ + Can provide access to Llama, Mistral, Gemma, and hundreds of other models.", + )) + .child(Label::new("To use local Ollama:")) + .child( + List::new() + .child(InstructionListItem::new( + "Download and install Ollama from", + Some("ollama.com"), + Some("https://ollama.com/download"), + )) + .child(InstructionListItem::text_only( + "Start Ollama and download a model: `ollama run gpt-oss:20b`", + )) + .child(InstructionListItem::text_only( + "Click 'Connect' below to start using Ollama in Zed", + )), + ) + .child(Label::new( + "Alternatively, you can connect to an Ollama server by specifying its \ + URL and API key (may not be required):", + )) + } + + fn render_api_key_editor(&self, cx: &Context) -> Div { + let state = self.state.read(cx); + let env_var_set = state.api_key_state.is_from_env_var(); + + if !state.api_key_state.has_key() { v_flex() - .gap_2() + .on_action(cx.listener(Self::save_api_key)) + .child(self.api_key_editor.clone()) + .child( + Label::new( + format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.") + ) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + h_flex() + .p_3() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().elevated_surface_background) .child( - v_flex().gap_1().child(Label::new(ollama_intro)).child( - List::new() - .child(InstructionListItem::text_only("Ollama must be running with at least one model installed to use it in the assistant.")) - .child(InstructionListItem::text_only( - "Once installed, try `ollama run llama3.2`", - )), - ), + h_flex() + .gap_2() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child( + Label::new( + if env_var_set { + format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.") + } else { + "API key configured".to_string() + } + ) + ) ) + .child( + Button::new("reset-api-key", "Reset API Key") + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .layer(ElevationIndex::ModalSurface) + .when(env_var_set, |this| { + this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), + ) + } + } + + fn render_api_url_editor(&self, cx: &Context) -> Div { + let api_url = OllamaLanguageModelProvider::api_url(cx); + let custom_api_url_set = api_url != OLLAMA_API_URL; + + if custom_api_url_set { + h_flex() + .p_3() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().elevated_surface_background) .child( h_flex() - .w_full() - .justify_between() .gap_2() - .child( - h_flex() - .w_full() - .gap_2() - .map(|this| { - if is_authenticated { - this.child( - Button::new("ollama-site", "Ollama") - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE)) - .into_any_element(), - ) - } else { - this.child( - Button::new( - "download_ollama_button", - "Download Ollama", - ) + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(v_flex().gap_1().child(Label::new(api_url))), + ) + .child( + Button::new("reset-api-url", "Reset API URL") + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .layer(ElevationIndex::ModalSurface) + .on_click( + cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)), + ), + ) + } else { + v_flex() + .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| { + this.save_api_url(cx); + cx.notify(); + })) + .gap_2() + .child(self.api_url_editor.clone()) + } + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_authenticated = self.state.read(cx).is_authenticated(); + + v_flex() + .gap_2() + .child(Self::render_instructions()) + .child(self.render_api_url_editor(cx)) + .child(self.render_api_key_editor(cx)) + .child( + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex() + .w_full() + .gap_2() + .map(|this| { + if is_authenticated { + this.child( + Button::new("ollama-site", "Ollama") + .style(ButtonStyle::Subtle) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE)) + .into_any_element(), + ) + } else { + this.child( + Button::new("download_ollama_button", "Download Ollama") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _, cx| { cx.open_url(OLLAMA_DOWNLOAD_URL) }) .into_any_element(), - ) - } - }) - .child( - Button::new("view-models", "View All Models") - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)), - ), - ) - .map(|this| { - if is_authenticated { - this.child( - ButtonLike::new("connected") - .disabled(true) - .cursor_style(gpui::CursorStyle::Arrow) - .child( - h_flex() - .gap_2() - .child(Indicator::dot().color(Color::Success)) - .child(Label::new("Connected")) - .into_any_element(), - ), - ) - } else { - this.child( - Button::new("retry_ollama_models", "Connect") - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon(IconName::PlayFilled) - .on_click(cx.listener(move |this, _, _, cx| { + ) + } + }) + .child( + Button::new("view-models", "View All Models") + .style(ButtonStyle::Subtle) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)), + ), + ) + .map(|this| { + if is_authenticated { + this.child( + ButtonLike::new("connected") + .disabled(true) + .cursor_style(gpui::CursorStyle::Arrow) + .child( + h_flex() + .gap_2() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new("Connected")) + .into_any_element(), + ), + ) + } else { + this.child( + Button::new("retry_ollama_models", "Connect") + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon(IconName::PlayOutlined) + .on_click( + cx.listener(move |this, _, _, cx| { this.retry_connection(cx) - })), - ) - } - }) - ) - .into_any() - } + }), + ), + ) + } + }), + ) } } diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index c61108d8bd59375256b7eb8b511527e8a0a119c2..dfafac02d8b25176f1aff5191487a12be914aab1 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -279,14 +279,19 @@ pub async fn complete( pub async fn stream_chat_completion( client: &dyn HttpClient, api_url: &str, + api_key: Option<&str>, request: ChatRequest, ) -> Result>> { let uri = format!("{api_url}/api/chat"); - let request_builder = http::Request::builder() + let mut request_builder = http::Request::builder() .method(Method::POST) .uri(uri) .header("Content-Type", "application/json"); + if let Some(api_key) = api_key { + request_builder = request_builder.header("Authorization", format!("Bearer {api_key}")) + } + let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; let mut response = client.send(request).await?; if response.status().is_success() { @@ -313,14 +318,19 @@ pub async fn stream_chat_completion( pub async fn get_models( client: &dyn HttpClient, api_url: &str, + api_key: Option<&str>, _: Option, ) -> Result> { let uri = format!("{api_url}/api/tags"); - let request_builder = HttpRequest::builder() + let mut request_builder = HttpRequest::builder() .method(Method::GET) .uri(uri) .header("Accept", "application/json"); + if let Some(api_key) = api_key { + request_builder = request_builder.header("Authorization", format!("Bearer {api_key}")); + } + let request = request_builder.body(AsyncBody::default())?; let mut response = client.send(request).await?; @@ -340,15 +350,25 @@ pub async fn get_models( } /// Fetch details of a model, used to determine model capabilities -pub async fn show_model(client: &dyn HttpClient, api_url: &str, model: &str) -> Result { +pub async fn show_model( + client: &dyn HttpClient, + api_url: &str, + api_key: Option<&str>, + model: &str, +) -> Result { let uri = format!("{api_url}/api/show"); - let request = HttpRequest::builder() + let mut request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) - .header("Content-Type", "application/json") - .body(AsyncBody::from( - serde_json::json!({ "model": model }).to_string(), - ))?; + .header("Content-Type", "application/json"); + + if let Some(api_key) = api_key { + request_builder = request_builder.header("Authorization", format!("Bearer {api_key}")) + } + + let request = request_builder.body(AsyncBody::from( + serde_json::json!({ "model": model }).to_string(), + ))?; let mut response = client.send(request).await?; let mut body = String::new(); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 98aaeef2126d559efa7696143faca13d39e11e62..09f67cc9c123a968705a834f9d1c5a2e855a782f 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -376,6 +376,20 @@ If the model is tagged with `thinking` in the Ollama catalog, set this option an The `supports_images` option enables the model's vision capabilities, allowing it to process images included in the conversation context. If the model is tagged with `vision` in the Ollama catalog, set this option and you can use it in Zed. +#### Ollama Authentication + +In addition to running Ollama on your own hardware, which generally does not require authentication, Zed also supports connecting to remote Ollama instances. API keys are required for authentication. + +One such service is [Ollama Turbo])(https://ollama.com/turbo). To configure Zed to use Ollama turbo: + +1. Sign in to your Ollama account and subscribe to Ollama Turbo +2. Visit [ollama.com/settings/keys](https://ollama.com/settings/keys) and create an API key +3. Open the settings view (`agent: open settings`) and go to the Ollama section +4. Paste your API key and press enter. +5. For the API URL enter `https://ollama.com` + +Zed will also use the `OLLAMA_API_KEY` environment variables if defined. + ### OpenAI {#openai} 1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys)