This commit is contained in:
Umesh Yadav 2025-08-26 20:35:16 +01:00 committed by GitHub
commit f0de9537e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 504 additions and 103 deletions

1
Cargo.lock generated
View file

@ -9143,6 +9143,7 @@ dependencies = [
"credentials_provider", "credentials_provider",
"deepseek", "deepseek",
"editor", "editor",
"fs",
"futures 0.3.31", "futures 0.3.31",
"google_ai", "google_ai",
"gpui", "gpui",

View file

@ -29,6 +29,7 @@ copilot.workspace = true
credentials_provider.workspace = true credentials_provider.workspace = true
deepseek = { workspace = true, features = ["schemars"] } deepseek = { workspace = true, features = ["schemars"] }
editor.workspace = true editor.workspace = true
fs.workspace = true
futures.workspace = true futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] } google_ai = { workspace = true, features = ["schemars"] }
gpui.workspace = true gpui.workspace = true

View file

@ -1,4 +1,6 @@
use anyhow::{Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use credentials_provider::CredentialsProvider;
use fs::Fs;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream}; use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use futures::{Stream, TryFutureExt, stream}; use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, Subscription, Task}; use gpui::{AnyView, App, AsyncApp, Context, Subscription, Task};
@ -10,17 +12,20 @@ use language_model::{
LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
}; };
use menu;
use ollama::{ use ollama::{
ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool, ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OLLAMA_API_KEY_VAR,
OllamaToolCall, get_models, show_model, stream_chat_completion, OLLAMA_API_URL, OllamaFunctionTool, OllamaToolCall, get_models, show_model,
stream_chat_completion,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore, update_settings_file};
use std::pin::Pin; use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use ui::{ButtonLike, Indicator, List, prelude::*}; use ui::{ButtonLike, ElevationIndex, Indicator, List, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use util::ResultExt; use util::ResultExt;
use crate::AllLanguageModelSettings; use crate::AllLanguageModelSettings;
@ -67,21 +72,61 @@ pub struct State {
available_models: Vec<ollama::Model>, available_models: Vec<ollama::Model>,
fetch_model_task: Option<Task<Result<()>>>, fetch_model_task: Option<Task<Result<()>>>,
_subscription: Subscription, _subscription: Subscription,
api_key: Option<String>,
api_key_from_env: bool,
} }
impl State { impl State {
fn is_authenticated(&self) -> bool { fn is_authenticated(&self) -> bool {
!self.available_models.is_empty() !self.available_models.is_empty() || self.api_key.is_some()
}
fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.ollama
.api_url
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
.delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
cx.notify();
})
})
}
fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.ollama
.api_url
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
.write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
this.api_key = Some(api_key);
cx.notify();
})
})
} }
fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).ollama; let settings = &AllLanguageModelSettings::get_global(cx).ollama;
let http_client = Arc::clone(&self.http_client); let http_client = Arc::clone(&self.http_client);
let api_url = settings.api_url.clone(); let api_url = settings.api_url.clone();
let api_key = self.api_key.clone();
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models // As a proxy for the server being "authenticated", we'll check if its up by fetching the models
cx.spawn(async move |this, cx| { 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.clone(), None).await?;
let tasks = models let tasks = models
.into_iter() .into_iter()
@ -92,9 +137,11 @@ impl State {
.map(|model| { .map(|model| {
let http_client = Arc::clone(&http_client); let http_client = Arc::clone(&http_client);
let api_url = api_url.clone(); let api_url = api_url.clone();
let api_key = api_key.clone();
async move { async move {
let name = model.name.as_str(); 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, name).await?;
let ollama_model = ollama::Model::new( let ollama_model = ollama::Model::new(
name, name,
None, None,
@ -135,8 +182,38 @@ impl State {
return Task::ready(Ok(())); return Task::ready(Ok(()));
} }
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.ollama
.api_url
.clone();
let fetch_models_task = self.fetch_models(cx); let fetch_models_task = self.fetch_models(cx);
cx.spawn(async move |_this, _cx| Ok(fetch_models_task.await?)) cx.spawn(async move |this, cx| {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(OLLAMA_API_KEY_VAR) {
(Some(api_key), true)
} else {
match credentials_provider.read_credentials(&api_url, cx).await {
Ok(Some((_, api_key))) => (
Some(String::from_utf8(api_key).context("invalid Ollama API key")?),
false,
),
Ok(None) => (None, false),
Err(_) => (None, false),
}
};
this.update(cx, |this, cx| {
this.api_key = api_key;
this.api_key_from_env = from_env;
cx.notify();
})?;
// 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
let _ = fetch_models_task.await;
Ok(())
})
} }
} }
@ -162,6 +239,8 @@ impl OllamaLanguageModelProvider {
available_models: Default::default(), available_models: Default::default(),
fetch_model_task: None, fetch_model_task: None,
_subscription: subscription, _subscription: subscription,
api_key: None,
api_key_from_env: false,
} }
}), }),
}; };
@ -240,6 +319,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
model, model,
http_client: self.http_client.clone(), http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4), request_limiter: RateLimiter::new(4),
state: self.state.clone(),
}) as Arc<dyn LanguageModel> }) as Arc<dyn LanguageModel>
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -267,7 +347,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
} }
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> { 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.reset_api_key(cx))
} }
} }
@ -276,6 +356,7 @@ pub struct OllamaLanguageModel {
model: ollama::Model, model: ollama::Model,
http_client: Arc<dyn HttpClient>, http_client: Arc<dyn HttpClient>,
request_limiter: RateLimiter, request_limiter: RateLimiter,
state: gpui::Entity<State>,
} }
impl OllamaLanguageModel { impl OllamaLanguageModel {
@ -424,15 +505,19 @@ impl LanguageModel for OllamaLanguageModel {
let request = self.to_ollama_request(request); let request = self.to_ollama_request(request);
let http_client = self.http_client.clone(); let http_client = self.http_client.clone();
let Ok(api_url) = cx.update(|cx| { let Ok((api_url, api_key)) = cx.update(|cx| {
let settings = &AllLanguageModelSettings::get_global(cx).ollama; let settings = &AllLanguageModelSettings::get_global(cx).ollama;
settings.api_url.clone() (
settings.api_url.clone(),
self.state.read(cx).api_key.clone(),
)
}) else { }) else {
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed(); return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
}; };
let future = self.request_limiter.stream(async move { 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, request).await?;
let stream = map_to_language_model_completion_events(stream); let stream = map_to_language_model_completion_events(stream);
Ok(stream) Ok(stream)
}); });
@ -541,12 +626,44 @@ fn map_to_language_model_completion_events(
} }
struct ConfigurationView { struct ConfigurationView {
api_key_editor: gpui::Entity<SingleLineInput>,
api_url_editor: gpui::Entity<SingleLineInput>,
state: gpui::Entity<State>, state: gpui::Entity<State>,
loading_models_task: Option<Task<()>>, loading_models_task: Option<Task<()>>,
} }
impl ConfigurationView { impl ConfigurationView {
pub fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
SingleLineInput::new(
window,
cx,
"ol-000000000000000000000000000000000000000000000000",
)
.label("API key")
});
let api_url = AllLanguageModelSettings::get_global(cx)
.ollama
.api_url
.clone();
let api_url_editor = cx.new(|cx| {
let input = SingleLineInput::new(window, cx, OLLAMA_API_URL).label("API URL");
if !api_url.is_empty() {
input.editor.update(cx, |editor, cx| {
editor.set_text(&*api_url, window, cx);
});
}
input
});
cx.observe(&state, |_, _, cx| {
cx.notify();
})
.detach();
let loading_models_task = Some(cx.spawn_in(window, { let loading_models_task = Some(cx.spawn_in(window, {
let state = state.clone(); let state = state.clone();
async move |this, cx| { async move |this, cx| {
@ -565,6 +682,8 @@ impl ConfigurationView {
})); }));
Self { Self {
api_key_editor,
api_url_editor,
state, state,
loading_models_task, loading_models_task,
} }
@ -575,103 +694,348 @@ impl ConfigurationView {
.update(cx, |state, cx| state.fetch_models(cx)) .update(cx, |state, cx| state.fetch_models(cx))
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let api_key = self
.api_key_editor
.read(cx)
.editor()
.read(cx)
.text(cx)
.trim()
.to_string();
// Don't proceed if no API key is provided and we're not authenticated
if api_key.is_empty() && !self.state.read(cx).is_authenticated() {
return;
}
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
if !api_key.is_empty() {
state
.update(cx, |state, cx| state.set_api_key(api_key, cx))?
.await
} else {
Ok(())
}
})
.detach_and_log_err(cx);
cx.notify();
}
fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.api_key_editor.update(cx, |input, cx| {
input.editor.update(cx, |editor, cx| {
editor.set_text("", window, cx);
});
});
let state = self.state.clone();
cx.spawn_in(window, async move |_, cx| {
state.update(cx, |state, cx| state.reset_api_key(cx))?.await
})
.detach_and_log_err(cx);
cx.notify();
}
fn should_render_api_key_editor(&self, cx: &mut Context<Self>) -> bool {
self.state.read(cx).api_key.is_none()
}
fn save_api_url(&mut self, cx: &mut Context<Self>) {
let api_url = self
.api_url_editor
.read(cx)
.editor()
.read(cx)
.text(cx)
.trim()
.to_string();
let current_url = AllLanguageModelSettings::get_global(cx)
.ollama
.api_url
.clone();
let effective_current_url = if current_url.is_empty() {
OLLAMA_API_URL
} else {
&current_url
};
if !api_url.is_empty() && api_url != effective_current_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.editor.update(cx, |editor, cx| {
editor.set_text(OLLAMA_API_URL, 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 = None;
}
});
cx.notify();
}
} }
impl Render for ConfigurationView { impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_authenticated = self.state.read(cx).is_authenticated(); let is_authenticated = self.state.read(cx).is_authenticated();
let env_var_set = self.state.read(cx).api_key_from_env;
let ollama_intro =
"Get up & running with Llama 3.3, Mistral, Gemma 2, and other LLMs with Ollama.";
if self.loading_models_task.is_some() { if self.loading_models_task.is_some() {
div().child(Label::new("Loading models...")).into_any() div()
} else {
v_flex()
.gap_2()
.child( .child(
v_flex().gap_1().child(Label::new(ollama_intro)).child( v_flex()
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`",
)),
),
)
.child(
h_flex()
.w_full()
.justify_between()
.gap_2() .gap_2()
.child( .child(
h_flex() h_flex()
.w_full()
.gap_2() .gap_2()
.map(|this| { .child(Indicator::dot().color(Color::Accent))
if is_authenticated { .child(Label::new("Connecting to Ollama...")),
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",
)
.style(ButtonStyle::Subtle)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Small)
.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| { .child(
if is_authenticated { Label::new("Checking for available models and server status")
this.child( .size(LabelSize::Small)
ButtonLike::new("connected") .color(Color::Muted),
.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| {
this.retry_connection(cx)
})),
)
}
})
) )
.into_any() .into_any()
} else {
v_flex()
.child(
if !is_authenticated {
v_flex().child(
Label::new("Run powerful language models locally on your machine with Ollama. Get started with Llama 3.3, Mistral, Gemma 2, and hundreds of other models.")
.size(LabelSize::Small)
.color(Color::Muted)
)
.v_flex()
.gap_2()
.child(
Label::new("Getting Started")
.size(LabelSize::Small)
.color(Color::Default)
)
.child(
List::new()
.child(InstructionListItem::text_only("1. Download and install Ollama from ollama.com"))
.child(InstructionListItem::text_only("2. Start Ollama and download a model: `ollama run gpt-oss:20b`"))
.child(InstructionListItem::text_only("3. Click 'Connect' below to start using Ollama in Zed"))
).child(
Label::new("API Keys and API URLs are optional, Zed will default to local ollama usage.")
.size(LabelSize::Small)
.color(Color::Muted)
)
.into_any()
} else {
div().into_any()
}
)
.child(
if self.should_render_api_key_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(self.api_key_editor.clone())
.child(
Label::new(
format!("You can also assign the {OLLAMA_API_KEY_VAR} environment variable and restart Zed.")
)
.size(LabelSize::XSmall)
.color(Color::Muted),
).into_any()
} else {
v_flex()
.child(
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()
.gap_2()
.child(Indicator::dot().color(Color::Success))
.child(
Label::new(
if env_var_set {
format!("API key set in {OLLAMA_API_KEY_VAR} 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 {OLLAMA_API_KEY_VAR} environment variable.")))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
)
)
.into_any()
}
)
.child({
let custom_api_url_set = AllLanguageModelSettings::get_global(cx).ollama.api_url != OLLAMA_API_URL;
if custom_api_url_set {
v_flex()
.gap_2()
.child(
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()
.gap_2()
.child(Indicator::dot().color(Color::Success))
.child(
v_flex()
.gap_1()
.child(
Label::new(
format!("API URL configured. {}", &AllLanguageModelSettings::get_global(cx).ollama.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)
}))
)
)
.into_any()
} else {
v_flex()
.child(
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())
)
.into_any()
}
})
.child(
v_flex()
.gap_2()
.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::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::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(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::PlayOutlined)
.on_click(cx.listener(move |this, _, _, cx| {
this.retry_connection(cx)
})),
)
}
})
)
)
.into_any()
} }
} }
} }

View file

@ -6,6 +6,7 @@ use serde_json::Value;
use std::time::Duration; use std::time::Duration;
pub const OLLAMA_API_URL: &str = "http://localhost:11434"; pub const OLLAMA_API_URL: &str = "http://localhost:11434";
pub const OLLAMA_API_KEY_VAR: &str = "OLLAMA_API_KEY";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@ -275,14 +276,19 @@ pub async fn complete(
pub async fn stream_chat_completion( pub async fn stream_chat_completion(
client: &dyn HttpClient, client: &dyn HttpClient,
api_url: &str, api_url: &str,
api_key: Option<String>,
request: ChatRequest, request: ChatRequest,
) -> Result<BoxStream<'static, Result<ChatResponseDelta>>> { ) -> Result<BoxStream<'static, Result<ChatResponseDelta>>> {
let uri = format!("{api_url}/api/chat"); let uri = format!("{api_url}/api/chat");
let request_builder = http::Request::builder() let mut request_builder = http::Request::builder()
.method(Method::POST) .method(Method::POST)
.uri(uri) .uri(uri)
.header("Content-Type", "application/json"); .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 request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?; let mut response = client.send(request).await?;
if response.status().is_success() { if response.status().is_success() {
@ -309,14 +315,19 @@ pub async fn stream_chat_completion(
pub async fn get_models( pub async fn get_models(
client: &dyn HttpClient, client: &dyn HttpClient,
api_url: &str, api_url: &str,
api_key: Option<String>,
_: Option<Duration>, _: Option<Duration>,
) -> Result<Vec<LocalModelListing>> { ) -> Result<Vec<LocalModelListing>> {
let uri = format!("{api_url}/api/tags"); let uri = format!("{api_url}/api/tags");
let request_builder = HttpRequest::builder() let mut request_builder = HttpRequest::builder()
.method(Method::GET) .method(Method::GET)
.uri(uri) .uri(uri)
.header("Accept", "application/json"); .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 request = request_builder.body(AsyncBody::default())?;
let mut response = client.send(request).await?; let mut response = client.send(request).await?;
@ -336,15 +347,25 @@ pub async fn get_models(
} }
/// Fetch details of a model, used to determine model capabilities /// 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<String>,
model: &str,
) -> Result<ModelShow> {
let uri = format!("{api_url}/api/show"); let uri = format!("{api_url}/api/show");
let request = HttpRequest::builder() let mut request_builder = HttpRequest::builder()
.method(Method::POST) .method(Method::POST)
.uri(uri) .uri(uri)
.header("Content-Type", "application/json") .header("Content-Type", "application/json");
.body(AsyncBody::from(
serde_json::json!({ "model": model }).to_string(), 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 response = client.send(request).await?;
let mut body = String::new(); let mut body = String::new();

View file

@ -378,6 +378,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. 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. 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 Ollama 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} ### OpenAI {#openai}
1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys) 1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys)