ollama.rs

   1use anyhow::{Result, anyhow};
   2use fs::Fs;
   3use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
   4use futures::{Stream, TryFutureExt, stream};
   5use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
   6use http_client::HttpClient;
   7use language_model::{
   8    ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
   9    LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
  10    LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
  11    LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
  12    LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
  13};
  14use menu;
  15use ollama::{
  16    ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, OLLAMA_API_URL, OllamaFunctionCall,
  17    OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion,
  18};
  19pub use settings::OllamaAvailableModel as AvailableModel;
  20use settings::{Settings, SettingsStore, update_settings_file};
  21use std::pin::Pin;
  22use std::sync::LazyLock;
  23use std::sync::atomic::{AtomicU64, Ordering};
  24use std::{collections::HashMap, sync::Arc};
  25use ui::{
  26    ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, List, ListBulletItem, Tooltip,
  27    prelude::*,
  28};
  29use ui_input::InputField;
  30
  31use crate::AllLanguageModelSettings;
  32
  33const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
  34const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
  35const OLLAMA_SITE: &str = "https://ollama.com/";
  36
  37const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("ollama");
  38const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Ollama");
  39
  40const API_KEY_ENV_VAR_NAME: &str = "OLLAMA_API_KEY";
  41static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
  42
  43#[derive(Default, Debug, Clone, PartialEq)]
  44pub struct OllamaSettings {
  45    pub api_url: String,
  46    pub auto_discover: bool,
  47    pub available_models: Vec<AvailableModel>,
  48}
  49
  50pub struct OllamaLanguageModelProvider {
  51    http_client: Arc<dyn HttpClient>,
  52    state: Entity<State>,
  53}
  54
  55pub struct State {
  56    api_key_state: ApiKeyState,
  57    http_client: Arc<dyn HttpClient>,
  58    fetched_models: Vec<ollama::Model>,
  59    fetch_model_task: Option<Task<Result<()>>>,
  60}
  61
  62impl State {
  63    fn is_authenticated(&self) -> bool {
  64        !self.fetched_models.is_empty()
  65    }
  66
  67    fn set_api_key(&mut self, api_key: Option<String>, cx: &mut Context<Self>) -> Task<Result<()>> {
  68        let api_url = OllamaLanguageModelProvider::api_url(cx);
  69        let task = self
  70            .api_key_state
  71            .store(api_url, api_key, |this| &mut this.api_key_state, cx);
  72
  73        self.fetched_models.clear();
  74        cx.spawn(async move |this, cx| {
  75            let result = task.await;
  76            this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
  77                .ok();
  78            result
  79        })
  80    }
  81
  82    fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
  83        let api_url = OllamaLanguageModelProvider::api_url(cx);
  84        let task = self
  85            .api_key_state
  86            .load_if_needed(api_url, |this| &mut this.api_key_state, cx);
  87
  88        // Always try to fetch models - if no API key is needed (local Ollama), it will work
  89        // If API key is needed and provided, it will work
  90        // If API key is needed and not provided, it will fail gracefully
  91        cx.spawn(async move |this, cx| {
  92            let result = task.await;
  93            this.update(cx, |this, cx| this.restart_fetch_models_task(cx))
  94                .ok();
  95            result
  96        })
  97    }
  98
  99    fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
 100        let http_client = Arc::clone(&self.http_client);
 101        let api_url = OllamaLanguageModelProvider::api_url(cx);
 102        let api_key = self.api_key_state.key(&api_url);
 103
 104        // As a proxy for the server being "authenticated", we'll check if its up by fetching the models
 105        cx.spawn(async move |this, cx| {
 106            let models = get_models(http_client.as_ref(), &api_url, api_key.as_deref()).await?;
 107
 108            let tasks = models
 109                .into_iter()
 110                // Since there is no metadata from the Ollama API
 111                // indicating which models are embedding models,
 112                // simply filter out models with "-embed" in their name
 113                .filter(|model| !model.name.contains("-embed"))
 114                .map(|model| {
 115                    let http_client = Arc::clone(&http_client);
 116                    let api_url = api_url.clone();
 117                    let api_key = api_key.clone();
 118                    async move {
 119                        let name = model.name.as_str();
 120                        let model =
 121                            show_model(http_client.as_ref(), &api_url, api_key.as_deref(), name)
 122                                .await?;
 123                        let ollama_model = ollama::Model::new(
 124                            name,
 125                            None,
 126                            model.context_length,
 127                            Some(model.supports_tools()),
 128                            Some(model.supports_vision()),
 129                            Some(model.supports_thinking()),
 130                        );
 131                        Ok(ollama_model)
 132                    }
 133                });
 134
 135            // Rate-limit capability fetches
 136            // since there is an arbitrary number of models available
 137            let mut ollama_models: Vec<_> = futures::stream::iter(tasks)
 138                .buffer_unordered(5)
 139                .collect::<Vec<Result<_>>>()
 140                .await
 141                .into_iter()
 142                .collect::<Result<Vec<_>>>()?;
 143
 144            ollama_models.sort_by(|a, b| a.name.cmp(&b.name));
 145
 146            this.update(cx, |this, cx| {
 147                this.fetched_models = ollama_models;
 148                cx.notify();
 149            })
 150        })
 151    }
 152
 153    fn restart_fetch_models_task(&mut self, cx: &mut Context<Self>) {
 154        let task = self.fetch_models(cx);
 155        self.fetch_model_task.replace(task);
 156    }
 157}
 158
 159impl OllamaLanguageModelProvider {
 160    pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
 161        let this = Self {
 162            http_client: http_client.clone(),
 163            state: cx.new(|cx| {
 164                cx.observe_global::<SettingsStore>({
 165                    let mut last_settings = OllamaLanguageModelProvider::settings(cx).clone();
 166                    move |this: &mut State, cx| {
 167                        let current_settings = OllamaLanguageModelProvider::settings(cx);
 168                        let settings_changed = current_settings != &last_settings;
 169                        if settings_changed {
 170                            let url_changed = last_settings.api_url != current_settings.api_url;
 171                            last_settings = current_settings.clone();
 172                            if url_changed {
 173                                this.fetched_models.clear();
 174                                this.authenticate(cx).detach();
 175                            }
 176                            cx.notify();
 177                        }
 178                    }
 179                })
 180                .detach();
 181
 182                State {
 183                    http_client,
 184                    fetched_models: Default::default(),
 185                    fetch_model_task: None,
 186                    api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
 187                }
 188            }),
 189        };
 190        this
 191    }
 192
 193    fn settings(cx: &App) -> &OllamaSettings {
 194        &AllLanguageModelSettings::get_global(cx).ollama
 195    }
 196
 197    fn api_url(cx: &App) -> SharedString {
 198        let api_url = &Self::settings(cx).api_url;
 199        if api_url.is_empty() {
 200            OLLAMA_API_URL.into()
 201        } else {
 202            SharedString::new(api_url.as_str())
 203        }
 204    }
 205}
 206
 207impl LanguageModelProviderState for OllamaLanguageModelProvider {
 208    type ObservableEntity = State;
 209
 210    fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
 211        Some(self.state.clone())
 212    }
 213}
 214
 215impl LanguageModelProvider for OllamaLanguageModelProvider {
 216    fn id(&self) -> LanguageModelProviderId {
 217        PROVIDER_ID
 218    }
 219
 220    fn name(&self) -> LanguageModelProviderName {
 221        PROVIDER_NAME
 222    }
 223
 224    fn icon(&self) -> IconOrSvg {
 225        IconOrSvg::Icon(IconName::AiOllama)
 226    }
 227
 228    fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
 229        // We shouldn't try to select default model, because it might lead to a load call for an unloaded model.
 230        // In a constrained environment where user might not have enough resources it'll be a bad UX to select something
 231        // to load by default.
 232        None
 233    }
 234
 235    fn default_fast_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
 236        // See explanation for default_model.
 237        None
 238    }
 239
 240    fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
 241        let mut models: HashMap<String, ollama::Model> = HashMap::new();
 242        let settings = OllamaLanguageModelProvider::settings(cx);
 243
 244        // Add models from the Ollama API
 245        if settings.auto_discover {
 246            for model in self.state.read(cx).fetched_models.iter() {
 247                models.insert(model.name.clone(), model.clone());
 248            }
 249        }
 250
 251        // Override with available models from settings
 252        merge_settings_into_models(&mut models, &settings.available_models);
 253
 254        let mut models = models
 255            .into_values()
 256            .map(|model| {
 257                Arc::new(OllamaLanguageModel {
 258                    id: LanguageModelId::from(model.name.clone()),
 259                    model,
 260                    http_client: self.http_client.clone(),
 261                    request_limiter: RateLimiter::new(4),
 262                    state: self.state.clone(),
 263                }) as Arc<dyn LanguageModel>
 264            })
 265            .collect::<Vec<_>>();
 266        models.sort_by_key(|model| model.name());
 267        models
 268    }
 269
 270    fn is_authenticated(&self, cx: &App) -> bool {
 271        self.state.read(cx).is_authenticated()
 272    }
 273
 274    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
 275        self.state.update(cx, |state, cx| state.authenticate(cx))
 276    }
 277
 278    fn configuration_view(
 279        &self,
 280        _target_agent: language_model::ConfigurationViewTargetAgent,
 281        window: &mut Window,
 282        cx: &mut App,
 283    ) -> AnyView {
 284        let state = self.state.clone();
 285        cx.new(|cx| ConfigurationView::new(state, window, cx))
 286            .into()
 287    }
 288
 289    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
 290        self.state
 291            .update(cx, |state, cx| state.set_api_key(None, cx))
 292    }
 293}
 294
 295pub struct OllamaLanguageModel {
 296    id: LanguageModelId,
 297    model: ollama::Model,
 298    http_client: Arc<dyn HttpClient>,
 299    request_limiter: RateLimiter,
 300    state: Entity<State>,
 301}
 302
 303impl OllamaLanguageModel {
 304    fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
 305        let supports_vision = self.model.supports_vision.unwrap_or(false);
 306
 307        let mut messages = Vec::with_capacity(request.messages.len());
 308
 309        for mut msg in request.messages.into_iter() {
 310            let images = if supports_vision {
 311                msg.content
 312                    .iter()
 313                    .filter_map(|content| match content {
 314                        MessageContent::Image(image) => Some(image.source.to_string()),
 315                        _ => None,
 316                    })
 317                    .collect::<Vec<String>>()
 318            } else {
 319                vec![]
 320            };
 321
 322            match msg.role {
 323                Role::User => {
 324                    for tool_result in msg
 325                        .content
 326                        .extract_if(.., |x| matches!(x, MessageContent::ToolResult(..)))
 327                    {
 328                        match tool_result {
 329                            MessageContent::ToolResult(tool_result) => {
 330                                messages.push(ChatMessage::Tool {
 331                                    tool_name: tool_result.tool_name.to_string(),
 332                                    content: tool_result.content.to_str().unwrap_or("").to_string(),
 333                                })
 334                            }
 335                            _ => unreachable!("Only tool result should be extracted"),
 336                        }
 337                    }
 338                    if !msg.content.is_empty() {
 339                        messages.push(ChatMessage::User {
 340                            content: msg.string_contents(),
 341                            images: if images.is_empty() {
 342                                None
 343                            } else {
 344                                Some(images)
 345                            },
 346                        })
 347                    }
 348                }
 349                Role::Assistant => {
 350                    let content = msg.string_contents();
 351                    let mut thinking = None;
 352                    let mut tool_calls = Vec::new();
 353                    for content in msg.content.into_iter() {
 354                        match content {
 355                            MessageContent::Thinking { text, .. } if !text.is_empty() => {
 356                                thinking = Some(text)
 357                            }
 358                            MessageContent::ToolUse(tool_use) => {
 359                                tool_calls.push(OllamaToolCall {
 360                                    id: Some(tool_use.id.to_string()),
 361                                    function: OllamaFunctionCall {
 362                                        name: tool_use.name.to_string(),
 363                                        arguments: tool_use.input,
 364                                    },
 365                                });
 366                            }
 367                            _ => (),
 368                        }
 369                    }
 370                    messages.push(ChatMessage::Assistant {
 371                        content,
 372                        tool_calls: Some(tool_calls),
 373                        images: if images.is_empty() {
 374                            None
 375                        } else {
 376                            Some(images)
 377                        },
 378                        thinking,
 379                    })
 380                }
 381                Role::System => messages.push(ChatMessage::System {
 382                    content: msg.string_contents(),
 383                }),
 384            }
 385        }
 386        ChatRequest {
 387            model: self.model.name.clone(),
 388            messages,
 389            keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
 390            stream: true,
 391            options: Some(ChatOptions {
 392                num_ctx: Some(self.model.max_tokens),
 393                stop: Some(request.stop),
 394                temperature: request.temperature.or(Some(1.0)),
 395                ..Default::default()
 396            }),
 397            think: self
 398                .model
 399                .supports_thinking
 400                .map(|supports_thinking| supports_thinking && request.thinking_allowed),
 401            tools: if self.model.supports_tools.unwrap_or(false) {
 402                request.tools.into_iter().map(tool_into_ollama).collect()
 403            } else {
 404                vec![]
 405            },
 406        }
 407    }
 408}
 409
 410impl LanguageModel for OllamaLanguageModel {
 411    fn id(&self) -> LanguageModelId {
 412        self.id.clone()
 413    }
 414
 415    fn name(&self) -> LanguageModelName {
 416        LanguageModelName::from(self.model.display_name().to_string())
 417    }
 418
 419    fn provider_id(&self) -> LanguageModelProviderId {
 420        PROVIDER_ID
 421    }
 422
 423    fn provider_name(&self) -> LanguageModelProviderName {
 424        PROVIDER_NAME
 425    }
 426
 427    fn supports_tools(&self) -> bool {
 428        self.model.supports_tools.unwrap_or(false)
 429    }
 430
 431    fn supports_images(&self) -> bool {
 432        self.model.supports_vision.unwrap_or(false)
 433    }
 434
 435    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
 436        match choice {
 437            LanguageModelToolChoice::Auto => false,
 438            LanguageModelToolChoice::Any => false,
 439            LanguageModelToolChoice::None => false,
 440        }
 441    }
 442
 443    fn telemetry_id(&self) -> String {
 444        format!("ollama/{}", self.model.id())
 445    }
 446
 447    fn max_token_count(&self) -> u64 {
 448        self.model.max_token_count()
 449    }
 450
 451    fn count_tokens(
 452        &self,
 453        request: LanguageModelRequest,
 454        _cx: &App,
 455    ) -> BoxFuture<'static, Result<u64>> {
 456        // There is no endpoint for this _yet_ in Ollama
 457        // see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
 458        let token_count = request
 459            .messages
 460            .iter()
 461            .map(|msg| msg.string_contents().chars().count())
 462            .sum::<usize>()
 463            / 4;
 464
 465        async move { Ok(token_count as u64) }.boxed()
 466    }
 467
 468    fn stream_completion(
 469        &self,
 470        request: LanguageModelRequest,
 471        cx: &AsyncApp,
 472    ) -> BoxFuture<
 473        'static,
 474        Result<
 475            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
 476            LanguageModelCompletionError,
 477        >,
 478    > {
 479        let request = self.to_ollama_request(request);
 480
 481        let http_client = self.http_client.clone();
 482        let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
 483            let api_url = OllamaLanguageModelProvider::api_url(cx);
 484            (state.api_key_state.key(&api_url), api_url)
 485        }) else {
 486            return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
 487        };
 488
 489        let future = self.request_limiter.stream(async move {
 490            let stream =
 491                stream_chat_completion(http_client.as_ref(), &api_url, api_key.as_deref(), request)
 492                    .await?;
 493            let stream = map_to_language_model_completion_events(stream);
 494            Ok(stream)
 495        });
 496
 497        future.map_ok(|f| f.boxed()).boxed()
 498    }
 499}
 500
 501fn map_to_language_model_completion_events(
 502    stream: Pin<Box<dyn Stream<Item = anyhow::Result<ChatResponseDelta>> + Send>>,
 503) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
 504    // Used for creating unique tool use ids
 505    static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0);
 506
 507    struct State {
 508        stream: Pin<Box<dyn Stream<Item = anyhow::Result<ChatResponseDelta>> + Send>>,
 509        used_tools: bool,
 510    }
 511
 512    // We need to create a ToolUse and Stop event from a single
 513    // response from the original stream
 514    let stream = stream::unfold(
 515        State {
 516            stream,
 517            used_tools: false,
 518        },
 519        async move |mut state| {
 520            let response = state.stream.next().await?;
 521
 522            let delta = match response {
 523                Ok(delta) => delta,
 524                Err(e) => {
 525                    let event = Err(LanguageModelCompletionError::from(anyhow!(e)));
 526                    return Some((vec![event], state));
 527                }
 528            };
 529
 530            let mut events = Vec::new();
 531
 532            match delta.message {
 533                ChatMessage::User { content, images: _ } => {
 534                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 535                }
 536                ChatMessage::System { content } => {
 537                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 538                }
 539                ChatMessage::Tool { content, .. } => {
 540                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 541                }
 542                ChatMessage::Assistant {
 543                    content,
 544                    tool_calls,
 545                    images: _,
 546                    thinking,
 547                } => {
 548                    if let Some(text) = thinking {
 549                        events.push(Ok(LanguageModelCompletionEvent::Thinking {
 550                            text,
 551                            signature: None,
 552                        }));
 553                    }
 554
 555                    if let Some(tool_call) = tool_calls.and_then(|v| v.into_iter().next()) {
 556                        let OllamaToolCall { id, function } = tool_call;
 557                        let id = id.unwrap_or_else(|| {
 558                            format!(
 559                                "{}-{}",
 560                                &function.name,
 561                                TOOL_CALL_COUNTER.fetch_add(1, Ordering::Relaxed)
 562                            )
 563                        });
 564                        let event = LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
 565                            id: LanguageModelToolUseId::from(id),
 566                            name: Arc::from(function.name),
 567                            raw_input: function.arguments.to_string(),
 568                            input: function.arguments,
 569                            is_input_complete: true,
 570                            thought_signature: None,
 571                        });
 572                        events.push(Ok(event));
 573                        state.used_tools = true;
 574                    } else if !content.is_empty() {
 575                        events.push(Ok(LanguageModelCompletionEvent::Text(content)));
 576                    }
 577                }
 578            };
 579
 580            if delta.done {
 581                events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
 582                    input_tokens: delta.prompt_eval_count.unwrap_or(0),
 583                    output_tokens: delta.eval_count.unwrap_or(0),
 584                    cache_creation_input_tokens: 0,
 585                    cache_read_input_tokens: 0,
 586                })));
 587                if state.used_tools {
 588                    state.used_tools = false;
 589                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
 590                } else {
 591                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
 592                }
 593            }
 594
 595            Some((events, state))
 596        },
 597    );
 598
 599    stream.flat_map(futures::stream::iter)
 600}
 601
 602struct ConfigurationView {
 603    api_key_editor: Entity<InputField>,
 604    api_url_editor: Entity<InputField>,
 605    state: Entity<State>,
 606}
 607
 608impl ConfigurationView {
 609    pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 610        let api_key_editor = cx.new(|cx| InputField::new(window, cx, "63e02e...").label("API key"));
 611
 612        let api_url_editor = cx.new(|cx| {
 613            let input = InputField::new(window, cx, OLLAMA_API_URL).label("API URL");
 614            input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx);
 615            input
 616        });
 617
 618        cx.observe(&state, |_, _, cx| {
 619            cx.notify();
 620        })
 621        .detach();
 622
 623        Self {
 624            api_key_editor,
 625            api_url_editor,
 626            state,
 627        }
 628    }
 629
 630    fn retry_connection(&self, cx: &mut App) {
 631        self.state
 632            .update(cx, |state, cx| state.restart_fetch_models_task(cx));
 633    }
 634
 635    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 636        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
 637        if api_key.is_empty() {
 638            return;
 639        }
 640
 641        // url changes can cause the editor to be displayed again
 642        self.api_key_editor
 643            .update(cx, |input, cx| input.set_text("", window, cx));
 644
 645        let state = self.state.clone();
 646        cx.spawn_in(window, async move |_, cx| {
 647            state
 648                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?
 649                .await
 650        })
 651        .detach_and_log_err(cx);
 652    }
 653
 654    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 655        self.api_key_editor
 656            .update(cx, |input, cx| input.set_text("", window, cx));
 657
 658        let state = self.state.clone();
 659        cx.spawn_in(window, async move |_, cx| {
 660            state
 661                .update(cx, |state, cx| state.set_api_key(None, cx))?
 662                .await
 663        })
 664        .detach_and_log_err(cx);
 665
 666        cx.notify();
 667    }
 668
 669    fn save_api_url(&mut self, cx: &mut Context<Self>) {
 670        let api_url = self.api_url_editor.read(cx).text(cx).trim().to_string();
 671        let current_url = OllamaLanguageModelProvider::api_url(cx);
 672        if !api_url.is_empty() && &api_url != &current_url {
 673            let fs = <dyn Fs>::global(cx);
 674            update_settings_file(fs, cx, move |settings, _| {
 675                settings
 676                    .language_models
 677                    .get_or_insert_default()
 678                    .ollama
 679                    .get_or_insert_default()
 680                    .api_url = Some(api_url);
 681            });
 682        }
 683    }
 684
 685    fn reset_api_url(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 686        self.api_url_editor
 687            .update(cx, |input, cx| input.set_text("", window, cx));
 688        let fs = <dyn Fs>::global(cx);
 689        update_settings_file(fs, cx, |settings, _cx| {
 690            if let Some(settings) = settings
 691                .language_models
 692                .as_mut()
 693                .and_then(|models| models.ollama.as_mut())
 694            {
 695                settings.api_url = Some(OLLAMA_API_URL.into());
 696            }
 697        });
 698        cx.notify();
 699    }
 700
 701    fn render_instructions(cx: &mut Context<Self>) -> Div {
 702        v_flex()
 703            .gap_2()
 704            .child(Label::new(
 705                "Run LLMs locally on your machine with Ollama, or connect to an Ollama server. \
 706                Can provide access to Llama, Mistral, Gemma, and hundreds of other models.",
 707            ))
 708            .child(Label::new("To use local Ollama:"))
 709            .child(
 710                List::new()
 711                    .child(
 712                        ListBulletItem::new("")
 713                            .child(Label::new("Download and install Ollama from"))
 714                            .child(ButtonLink::new("ollama.com", "https://ollama.com/download")),
 715                    )
 716                    .child(
 717                        ListBulletItem::new("")
 718                            .child(Label::new("Start Ollama and download a model:"))
 719                            .child(Label::new("ollama run gpt-oss:20b").inline_code(cx)),
 720                    )
 721                    .child(ListBulletItem::new(
 722                        "Click 'Connect' below to start using Ollama in Zed",
 723                    )),
 724            )
 725            .child(Label::new(
 726                "Alternatively, you can connect to an Ollama server by specifying its \
 727                URL and API key (may not be required):",
 728            ))
 729    }
 730
 731    fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
 732        let state = self.state.read(cx);
 733        let env_var_set = state.api_key_state.is_from_env_var();
 734        let configured_card_label = if env_var_set {
 735            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
 736        } else {
 737            "API key configured".to_string()
 738        };
 739
 740        if !state.api_key_state.has_key() {
 741            v_flex()
 742              .on_action(cx.listener(Self::save_api_key))
 743              .child(self.api_key_editor.clone())
 744              .child(
 745                  Label::new(
 746                      format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.")
 747                  )
 748                  .size(LabelSize::Small)
 749                  .color(Color::Muted),
 750              )
 751              .into_any_element()
 752        } else {
 753            ConfiguredApiCard::new(configured_card_label)
 754                .disabled(env_var_set)
 755                .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
 756                .when(env_var_set, |this| {
 757                    this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
 758                })
 759                .into_any_element()
 760        }
 761    }
 762
 763    fn render_api_url_editor(&self, cx: &Context<Self>) -> Div {
 764        let api_url = OllamaLanguageModelProvider::api_url(cx);
 765        let custom_api_url_set = api_url != OLLAMA_API_URL;
 766
 767        if custom_api_url_set {
 768            h_flex()
 769                .p_3()
 770                .justify_between()
 771                .rounded_md()
 772                .border_1()
 773                .border_color(cx.theme().colors().border)
 774                .bg(cx.theme().colors().elevated_surface_background)
 775                .child(
 776                    h_flex()
 777                        .gap_2()
 778                        .child(Icon::new(IconName::Check).color(Color::Success))
 779                        .child(v_flex().gap_1().child(Label::new(api_url))),
 780                )
 781                .child(
 782                    Button::new("reset-api-url", "Reset API URL")
 783                        .label_size(LabelSize::Small)
 784                        .icon(IconName::Undo)
 785                        .icon_size(IconSize::Small)
 786                        .icon_position(IconPosition::Start)
 787                        .layer(ElevationIndex::ModalSurface)
 788                        .on_click(
 789                            cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
 790                        ),
 791                )
 792        } else {
 793            v_flex()
 794                .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| {
 795                    this.save_api_url(cx);
 796                    cx.notify();
 797                }))
 798                .gap_2()
 799                .child(self.api_url_editor.clone())
 800        }
 801    }
 802}
 803
 804impl Render for ConfigurationView {
 805    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 806        let is_authenticated = self.state.read(cx).is_authenticated();
 807
 808        v_flex()
 809            .gap_2()
 810            .child(Self::render_instructions(cx))
 811            .child(self.render_api_url_editor(cx))
 812            .child(self.render_api_key_editor(cx))
 813            .child(
 814                h_flex()
 815                    .w_full()
 816                    .justify_between()
 817                    .gap_2()
 818                    .child(
 819                        h_flex()
 820                            .w_full()
 821                            .gap_2()
 822                            .map(|this| {
 823                                if is_authenticated {
 824                                    this.child(
 825                                        Button::new("ollama-site", "Ollama")
 826                                            .style(ButtonStyle::Subtle)
 827                                            .icon(IconName::ArrowUpRight)
 828                                            .icon_size(IconSize::XSmall)
 829                                            .icon_color(Color::Muted)
 830                                            .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE))
 831                                            .into_any_element(),
 832                                    )
 833                                } else {
 834                                    this.child(
 835                                        Button::new("download_ollama_button", "Download Ollama")
 836                                            .style(ButtonStyle::Subtle)
 837                                            .icon(IconName::ArrowUpRight)
 838                                            .icon_size(IconSize::XSmall)
 839                                            .icon_color(Color::Muted)
 840                                            .on_click(move |_, _, cx| {
 841                                                cx.open_url(OLLAMA_DOWNLOAD_URL)
 842                                            })
 843                                            .into_any_element(),
 844                                    )
 845                                }
 846                            })
 847                            .child(
 848                                Button::new("view-models", "View All Models")
 849                                    .style(ButtonStyle::Subtle)
 850                                    .icon(IconName::ArrowUpRight)
 851                                    .icon_size(IconSize::XSmall)
 852                                    .icon_color(Color::Muted)
 853                                    .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
 854                            ),
 855                    )
 856                    .map(|this| {
 857                        if is_authenticated {
 858                            this.child(
 859                                ButtonLike::new("connected")
 860                                    .disabled(true)
 861                                    .cursor_style(CursorStyle::Arrow)
 862                                    .child(
 863                                        h_flex()
 864                                            .gap_2()
 865                                            .child(Icon::new(IconName::Check).color(Color::Success))
 866                                            .child(Label::new("Connected"))
 867                                            .into_any_element(),
 868                                    )
 869                                    .child(
 870                                        IconButton::new("refresh-models", IconName::RotateCcw)
 871                                            .tooltip(Tooltip::text("Refresh Models"))
 872                                            .on_click(cx.listener(|this, _, _, cx| {
 873                                                this.state.update(cx, |state, _| {
 874                                                    state.fetched_models.clear();
 875                                                });
 876                                                this.retry_connection(cx);
 877                                            })),
 878                                    ),
 879                            )
 880                        } else {
 881                            this.child(
 882                                Button::new("retry_ollama_models", "Connect")
 883                                    .icon_position(IconPosition::Start)
 884                                    .icon_size(IconSize::XSmall)
 885                                    .icon(IconName::PlayOutlined)
 886                                    .on_click(
 887                                        cx.listener(move |this, _, _, cx| {
 888                                            this.retry_connection(cx)
 889                                        }),
 890                                    ),
 891                            )
 892                        }
 893                    }),
 894            )
 895    }
 896}
 897
 898fn merge_settings_into_models(
 899    models: &mut HashMap<String, ollama::Model>,
 900    available_models: &[AvailableModel],
 901) {
 902    for setting_model in available_models {
 903        if let Some(model) = models.get_mut(&setting_model.name) {
 904            model.max_tokens = setting_model.max_tokens;
 905            model.display_name = setting_model.display_name.clone();
 906            model.keep_alive = setting_model.keep_alive.clone();
 907            model.supports_tools = setting_model.supports_tools;
 908            model.supports_vision = setting_model.supports_images;
 909            model.supports_thinking = setting_model.supports_thinking;
 910        } else {
 911            models.insert(
 912                setting_model.name.clone(),
 913                ollama::Model {
 914                    name: setting_model.name.clone(),
 915                    display_name: setting_model.display_name.clone(),
 916                    max_tokens: setting_model.max_tokens,
 917                    keep_alive: setting_model.keep_alive.clone(),
 918                    supports_tools: setting_model.supports_tools,
 919                    supports_vision: setting_model.supports_images,
 920                    supports_thinking: setting_model.supports_thinking,
 921                },
 922            );
 923        }
 924    }
 925}
 926
 927fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
 928    ollama::OllamaTool::Function {
 929        function: OllamaFunctionTool {
 930            name: tool.name,
 931            description: Some(tool.description),
 932            parameters: Some(tool.input_schema),
 933        },
 934    }
 935}
 936
 937#[cfg(test)]
 938mod tests {
 939    use super::*;
 940
 941    #[test]
 942    fn test_merge_settings_preserves_display_names_for_similar_models() {
 943        // Regression test for https://github.com/zed-industries/zed/issues/43646
 944        // When multiple models share the same base name (e.g., qwen2.5-coder:1.5b and qwen2.5-coder:3b),
 945        // each model should get its own display_name from settings, not a random one.
 946
 947        let mut models: HashMap<String, ollama::Model> = HashMap::new();
 948        models.insert(
 949            "qwen2.5-coder:1.5b".to_string(),
 950            ollama::Model {
 951                name: "qwen2.5-coder:1.5b".to_string(),
 952                display_name: None,
 953                max_tokens: 4096,
 954                keep_alive: None,
 955                supports_tools: None,
 956                supports_vision: None,
 957                supports_thinking: None,
 958            },
 959        );
 960        models.insert(
 961            "qwen2.5-coder:3b".to_string(),
 962            ollama::Model {
 963                name: "qwen2.5-coder:3b".to_string(),
 964                display_name: None,
 965                max_tokens: 4096,
 966                keep_alive: None,
 967                supports_tools: None,
 968                supports_vision: None,
 969                supports_thinking: None,
 970            },
 971        );
 972
 973        let available_models = vec![
 974            AvailableModel {
 975                name: "qwen2.5-coder:1.5b".to_string(),
 976                display_name: Some("QWEN2.5 Coder 1.5B".to_string()),
 977                max_tokens: 5000,
 978                keep_alive: None,
 979                supports_tools: Some(true),
 980                supports_images: None,
 981                supports_thinking: None,
 982            },
 983            AvailableModel {
 984                name: "qwen2.5-coder:3b".to_string(),
 985                display_name: Some("QWEN2.5 Coder 3B".to_string()),
 986                max_tokens: 6000,
 987                keep_alive: None,
 988                supports_tools: Some(true),
 989                supports_images: None,
 990                supports_thinking: None,
 991            },
 992        ];
 993
 994        merge_settings_into_models(&mut models, &available_models);
 995
 996        let model_1_5b = models
 997            .get("qwen2.5-coder:1.5b")
 998            .expect("1.5b model missing");
 999        let model_3b = models.get("qwen2.5-coder:3b").expect("3b model missing");
1000
1001        assert_eq!(
1002            model_1_5b.display_name,
1003            Some("QWEN2.5 Coder 1.5B".to_string()),
1004            "1.5b model should have its own display_name"
1005        );
1006        assert_eq!(model_1_5b.max_tokens, 5000);
1007
1008        assert_eq!(
1009            model_3b.display_name,
1010            Some("QWEN2.5 Coder 3B".to_string()),
1011            "3b model should have its own display_name"
1012        );
1013        assert_eq!(model_3b.max_tokens, 6000);
1014    }
1015}