Cargo.lock 🔗
@@ -9179,6 +9179,7 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
+ "fs",
"futures 0.3.31",
"google_ai",
"gpui",
Umesh Yadav , Peter Tripp , Oliver Azevedo Barnes , and Michael Sloan created
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 <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Oliver Azevedo Barnes <oliver@liquidvoting.io>
Co-authored-by: Michael Sloan <michael@zed.dev>
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(-)
@@ -9179,6 +9179,7 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
+ "fs",
"futures 0.3.31",
"google_ai",
"gpui",
@@ -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
@@ -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<EnvVar> = 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<dyn HttpClient>,
- available_models: Vec<ollama::Model>,
+ fetched_models: Vec<ollama::Model>,
fetch_model_task: Option<Task<Result<()>>>,
- _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<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
+ 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<Self>) -> Task<Result<(), AuthenticateError>> {
+ 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<Self>) -> Task<Result<()>> {
- 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<Self>) -> Task<Result<(), AuthenticateError>> {
- 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::<SettingsStore>({
- let mut settings = AllLanguageModelSettings::get_global(cx).ollama.clone();
+ cx.observe_global::<SettingsStore>({
+ 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<String, ollama::Model> = 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<dyn LanguageModel>
})
.collect::<Vec<_>>();
@@ -267,7 +320,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
- 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<dyn HttpClient>,
request_limiter: RateLimiter,
+ state: gpui::Entity<State>,
}
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<SingleLineInput>,
+ api_url_editor: gpui::Entity<SingleLineInput>,
state: gpui::Entity<State>,
- loading_models_task: Option<Task<()>>,
}
impl ConfigurationView {
pub fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) -> 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<Self>) {
+ 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>) {
+ 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<Self>) {
+ 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 = <dyn Fs>::global(cx);
+ update_settings_file::<AllLanguageModelSettings>(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>) {
+ self.api_url_editor
+ .update(cx, |input, cx| input.set_text("", window, cx));
+ let fs = <dyn Fs>::global(cx);
+ update_settings_file::<AllLanguageModelSettings>(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<Self>) -> 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<Self>) -> 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<Self>) -> 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()
- }
+ }),
+ ),
+ )
+ }
+ }),
+ )
}
}
@@ -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<BoxStream<'static, Result<ChatResponseDelta>>> {
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<Duration>,
) -> Result<Vec<LocalModelListing>> {
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<ModelShow> {
+pub async fn show_model(
+ client: &dyn HttpClient,
+ api_url: &str,
+ api_key: Option<&str>,
+ model: &str,
+) -> Result<ModelShow> {
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();
@@ -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)