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, 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) -> IconName {
225        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        for setting_model in &OllamaLanguageModelProvider::settings(cx).available_models {
253            let setting_base = setting_model.name.split(':').next().unwrap();
254            if let Some(model) = models
255                .values_mut()
256                .find(|m| m.name.split(':').next().unwrap() == setting_base)
257            {
258                model.max_tokens = setting_model.max_tokens;
259                model.display_name = setting_model.display_name.clone();
260                model.keep_alive = setting_model.keep_alive.clone();
261                model.supports_tools = setting_model.supports_tools;
262                model.supports_vision = setting_model.supports_images;
263                model.supports_thinking = setting_model.supports_thinking;
264            } else {
265                models.insert(
266                    setting_model.name.clone(),
267                    ollama::Model {
268                        name: setting_model.name.clone(),
269                        display_name: setting_model.display_name.clone(),
270                        max_tokens: setting_model.max_tokens,
271                        keep_alive: setting_model.keep_alive.clone(),
272                        supports_tools: setting_model.supports_tools,
273                        supports_vision: setting_model.supports_images,
274                        supports_thinking: setting_model.supports_thinking,
275                    },
276                );
277            }
278        }
279
280        let mut models = models
281            .into_values()
282            .map(|model| {
283                Arc::new(OllamaLanguageModel {
284                    id: LanguageModelId::from(model.name.clone()),
285                    model,
286                    http_client: self.http_client.clone(),
287                    request_limiter: RateLimiter::new(4),
288                    state: self.state.clone(),
289                }) as Arc<dyn LanguageModel>
290            })
291            .collect::<Vec<_>>();
292        models.sort_by_key(|model| model.name());
293        models
294    }
295
296    fn is_authenticated(&self, cx: &App) -> bool {
297        self.state.read(cx).is_authenticated()
298    }
299
300    fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
301        self.state.update(cx, |state, cx| state.authenticate(cx))
302    }
303
304    fn configuration_view(
305        &self,
306        _target_agent: language_model::ConfigurationViewTargetAgent,
307        window: &mut Window,
308        cx: &mut App,
309    ) -> AnyView {
310        let state = self.state.clone();
311        cx.new(|cx| ConfigurationView::new(state, window, cx))
312            .into()
313    }
314
315    fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
316        self.state
317            .update(cx, |state, cx| state.set_api_key(None, cx))
318    }
319}
320
321pub struct OllamaLanguageModel {
322    id: LanguageModelId,
323    model: ollama::Model,
324    http_client: Arc<dyn HttpClient>,
325    request_limiter: RateLimiter,
326    state: Entity<State>,
327}
328
329impl OllamaLanguageModel {
330    fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
331        let supports_vision = self.model.supports_vision.unwrap_or(false);
332
333        let mut messages = Vec::with_capacity(request.messages.len());
334
335        for mut msg in request.messages.into_iter() {
336            let images = if supports_vision {
337                msg.content
338                    .iter()
339                    .filter_map(|content| match content {
340                        MessageContent::Image(image) => Some(image.source.to_string()),
341                        _ => None,
342                    })
343                    .collect::<Vec<String>>()
344            } else {
345                vec![]
346            };
347
348            match msg.role {
349                Role::User => {
350                    for tool_result in msg
351                        .content
352                        .extract_if(.., |x| matches!(x, MessageContent::ToolResult(..)))
353                    {
354                        match tool_result {
355                            MessageContent::ToolResult(tool_result) => {
356                                messages.push(ChatMessage::Tool {
357                                    tool_name: tool_result.tool_name.to_string(),
358                                    content: tool_result.content.to_str().unwrap_or("").to_string(),
359                                })
360                            }
361                            _ => unreachable!("Only tool result should be extracted"),
362                        }
363                    }
364                    if !msg.content.is_empty() {
365                        messages.push(ChatMessage::User {
366                            content: msg.string_contents(),
367                            images: if images.is_empty() {
368                                None
369                            } else {
370                                Some(images)
371                            },
372                        })
373                    }
374                }
375                Role::Assistant => {
376                    let content = msg.string_contents();
377                    let mut thinking = None;
378                    let mut tool_calls = Vec::new();
379                    for content in msg.content.into_iter() {
380                        match content {
381                            MessageContent::Thinking { text, .. } if !text.is_empty() => {
382                                thinking = Some(text)
383                            }
384                            MessageContent::ToolUse(tool_use) => {
385                                tool_calls.push(OllamaToolCall {
386                                    id: Some(tool_use.id.to_string()),
387                                    function: OllamaFunctionCall {
388                                        name: tool_use.name.to_string(),
389                                        arguments: tool_use.input,
390                                    },
391                                });
392                            }
393                            _ => (),
394                        }
395                    }
396                    messages.push(ChatMessage::Assistant {
397                        content,
398                        tool_calls: Some(tool_calls),
399                        images: if images.is_empty() {
400                            None
401                        } else {
402                            Some(images)
403                        },
404                        thinking,
405                    })
406                }
407                Role::System => messages.push(ChatMessage::System {
408                    content: msg.string_contents(),
409                }),
410            }
411        }
412        ChatRequest {
413            model: self.model.name.clone(),
414            messages,
415            keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
416            stream: true,
417            options: Some(ChatOptions {
418                num_ctx: Some(self.model.max_tokens),
419                stop: Some(request.stop),
420                temperature: request.temperature.or(Some(1.0)),
421                ..Default::default()
422            }),
423            think: self
424                .model
425                .supports_thinking
426                .map(|supports_thinking| supports_thinking && request.thinking_allowed),
427            tools: if self.model.supports_tools.unwrap_or(false) {
428                request.tools.into_iter().map(tool_into_ollama).collect()
429            } else {
430                vec![]
431            },
432        }
433    }
434}
435
436impl LanguageModel for OllamaLanguageModel {
437    fn id(&self) -> LanguageModelId {
438        self.id.clone()
439    }
440
441    fn name(&self) -> LanguageModelName {
442        LanguageModelName::from(self.model.display_name().to_string())
443    }
444
445    fn provider_id(&self) -> LanguageModelProviderId {
446        PROVIDER_ID
447    }
448
449    fn provider_name(&self) -> LanguageModelProviderName {
450        PROVIDER_NAME
451    }
452
453    fn supports_tools(&self) -> bool {
454        self.model.supports_tools.unwrap_or(false)
455    }
456
457    fn supports_images(&self) -> bool {
458        self.model.supports_vision.unwrap_or(false)
459    }
460
461    fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
462        match choice {
463            LanguageModelToolChoice::Auto => false,
464            LanguageModelToolChoice::Any => false,
465            LanguageModelToolChoice::None => false,
466        }
467    }
468
469    fn telemetry_id(&self) -> String {
470        format!("ollama/{}", self.model.id())
471    }
472
473    fn max_token_count(&self) -> u64 {
474        self.model.max_token_count()
475    }
476
477    fn count_tokens(
478        &self,
479        request: LanguageModelRequest,
480        _cx: &App,
481    ) -> BoxFuture<'static, Result<u64>> {
482        // There is no endpoint for this _yet_ in Ollama
483        // see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
484        let token_count = request
485            .messages
486            .iter()
487            .map(|msg| msg.string_contents().chars().count())
488            .sum::<usize>()
489            / 4;
490
491        async move { Ok(token_count as u64) }.boxed()
492    }
493
494    fn stream_completion(
495        &self,
496        request: LanguageModelRequest,
497        cx: &AsyncApp,
498    ) -> BoxFuture<
499        'static,
500        Result<
501            BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
502            LanguageModelCompletionError,
503        >,
504    > {
505        let request = self.to_ollama_request(request);
506
507        let http_client = self.http_client.clone();
508        let Ok((api_key, api_url)) = self.state.read_with(cx, |state, cx| {
509            let api_url = OllamaLanguageModelProvider::api_url(cx);
510            (state.api_key_state.key(&api_url), api_url)
511        }) else {
512            return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
513        };
514
515        let future = self.request_limiter.stream(async move {
516            let stream =
517                stream_chat_completion(http_client.as_ref(), &api_url, api_key.as_deref(), request)
518                    .await?;
519            let stream = map_to_language_model_completion_events(stream);
520            Ok(stream)
521        });
522
523        future.map_ok(|f| f.boxed()).boxed()
524    }
525}
526
527fn map_to_language_model_completion_events(
528    stream: Pin<Box<dyn Stream<Item = anyhow::Result<ChatResponseDelta>> + Send>>,
529) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
530    // Used for creating unique tool use ids
531    static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0);
532
533    struct State {
534        stream: Pin<Box<dyn Stream<Item = anyhow::Result<ChatResponseDelta>> + Send>>,
535        used_tools: bool,
536    }
537
538    // We need to create a ToolUse and Stop event from a single
539    // response from the original stream
540    let stream = stream::unfold(
541        State {
542            stream,
543            used_tools: false,
544        },
545        async move |mut state| {
546            let response = state.stream.next().await?;
547
548            let delta = match response {
549                Ok(delta) => delta,
550                Err(e) => {
551                    let event = Err(LanguageModelCompletionError::from(anyhow!(e)));
552                    return Some((vec![event], state));
553                }
554            };
555
556            let mut events = Vec::new();
557
558            match delta.message {
559                ChatMessage::User { content, images: _ } => {
560                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
561                }
562                ChatMessage::System { content } => {
563                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
564                }
565                ChatMessage::Tool { content, .. } => {
566                    events.push(Ok(LanguageModelCompletionEvent::Text(content)));
567                }
568                ChatMessage::Assistant {
569                    content,
570                    tool_calls,
571                    images: _,
572                    thinking,
573                } => {
574                    if let Some(text) = thinking {
575                        events.push(Ok(LanguageModelCompletionEvent::Thinking {
576                            text,
577                            signature: None,
578                        }));
579                    }
580
581                    if let Some(tool_call) = tool_calls.and_then(|v| v.into_iter().next()) {
582                        let OllamaToolCall { id, function } = tool_call;
583                        let id = id.unwrap_or_else(|| {
584                            format!(
585                                "{}-{}",
586                                &function.name,
587                                TOOL_CALL_COUNTER.fetch_add(1, Ordering::Relaxed)
588                            )
589                        });
590                        let event = LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
591                            id: LanguageModelToolUseId::from(id),
592                            name: Arc::from(function.name),
593                            raw_input: function.arguments.to_string(),
594                            input: function.arguments,
595                            is_input_complete: true,
596                            thought_signature: None,
597                        });
598                        events.push(Ok(event));
599                        state.used_tools = true;
600                    } else if !content.is_empty() {
601                        events.push(Ok(LanguageModelCompletionEvent::Text(content)));
602                    }
603                }
604            };
605
606            if delta.done {
607                events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
608                    input_tokens: delta.prompt_eval_count.unwrap_or(0),
609                    output_tokens: delta.eval_count.unwrap_or(0),
610                    cache_creation_input_tokens: 0,
611                    cache_read_input_tokens: 0,
612                })));
613                if state.used_tools {
614                    state.used_tools = false;
615                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
616                } else {
617                    events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
618                }
619            }
620
621            Some((events, state))
622        },
623    );
624
625    stream.flat_map(futures::stream::iter)
626}
627
628struct ConfigurationView {
629    api_key_editor: Entity<InputField>,
630    api_url_editor: Entity<InputField>,
631    state: Entity<State>,
632}
633
634impl ConfigurationView {
635    pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
636        let api_key_editor = cx.new(|cx| InputField::new(window, cx, "63e02e...").label("API key"));
637
638        let api_url_editor = cx.new(|cx| {
639            let input = InputField::new(window, cx, OLLAMA_API_URL).label("API URL");
640            input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx);
641            input
642        });
643
644        cx.observe(&state, |_, _, cx| {
645            cx.notify();
646        })
647        .detach();
648
649        Self {
650            api_key_editor,
651            api_url_editor,
652            state,
653        }
654    }
655
656    fn retry_connection(&self, cx: &mut App) {
657        self.state
658            .update(cx, |state, cx| state.restart_fetch_models_task(cx));
659    }
660
661    fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
662        let api_key = self.api_key_editor.read(cx).text(cx).trim().to_string();
663        if api_key.is_empty() {
664            return;
665        }
666
667        // url changes can cause the editor to be displayed again
668        self.api_key_editor
669            .update(cx, |input, cx| input.set_text("", window, cx));
670
671        let state = self.state.clone();
672        cx.spawn_in(window, async move |_, cx| {
673            state
674                .update(cx, |state, cx| state.set_api_key(Some(api_key), cx))?
675                .await
676        })
677        .detach_and_log_err(cx);
678    }
679
680    fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
681        self.api_key_editor
682            .update(cx, |input, cx| input.set_text("", window, cx));
683
684        let state = self.state.clone();
685        cx.spawn_in(window, async move |_, cx| {
686            state
687                .update(cx, |state, cx| state.set_api_key(None, cx))?
688                .await
689        })
690        .detach_and_log_err(cx);
691
692        cx.notify();
693    }
694
695    fn save_api_url(&mut self, cx: &mut Context<Self>) {
696        let api_url = self.api_url_editor.read(cx).text(cx).trim().to_string();
697        let current_url = OllamaLanguageModelProvider::api_url(cx);
698        if !api_url.is_empty() && &api_url != &current_url {
699            let fs = <dyn Fs>::global(cx);
700            update_settings_file(fs, cx, move |settings, _| {
701                settings
702                    .language_models
703                    .get_or_insert_default()
704                    .ollama
705                    .get_or_insert_default()
706                    .api_url = Some(api_url);
707            });
708        }
709    }
710
711    fn reset_api_url(&mut self, window: &mut Window, cx: &mut Context<Self>) {
712        self.api_url_editor
713            .update(cx, |input, cx| input.set_text("", window, cx));
714        let fs = <dyn Fs>::global(cx);
715        update_settings_file(fs, cx, |settings, _cx| {
716            if let Some(settings) = settings
717                .language_models
718                .as_mut()
719                .and_then(|models| models.ollama.as_mut())
720            {
721                settings.api_url = Some(OLLAMA_API_URL.into());
722            }
723        });
724        cx.notify();
725    }
726
727    fn render_instructions(cx: &mut Context<Self>) -> Div {
728        v_flex()
729            .gap_2()
730            .child(Label::new(
731                "Run LLMs locally on your machine with Ollama, or connect to an Ollama server. \
732                Can provide access to Llama, Mistral, Gemma, and hundreds of other models.",
733            ))
734            .child(Label::new("To use local Ollama:"))
735            .child(
736                List::new()
737                    .child(
738                        ListBulletItem::new("")
739                            .child(Label::new("Download and install Ollama from"))
740                            .child(ButtonLink::new("ollama.com", "https://ollama.com/download")),
741                    )
742                    .child(
743                        ListBulletItem::new("")
744                            .child(Label::new("Start Ollama and download a model:"))
745                            .child(Label::new("ollama run gpt-oss:20b").inline_code(cx)),
746                    )
747                    .child(ListBulletItem::new(
748                        "Click 'Connect' below to start using Ollama in Zed",
749                    )),
750            )
751            .child(Label::new(
752                "Alternatively, you can connect to an Ollama server by specifying its \
753                URL and API key (may not be required):",
754            ))
755    }
756
757    fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
758        let state = self.state.read(cx);
759        let env_var_set = state.api_key_state.is_from_env_var();
760        let configured_card_label = if env_var_set {
761            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable.")
762        } else {
763            "API key configured".to_string()
764        };
765
766        if !state.api_key_state.has_key() {
767            v_flex()
768              .on_action(cx.listener(Self::save_api_key))
769              .child(self.api_key_editor.clone())
770              .child(
771                  Label::new(
772                      format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.")
773                  )
774                  .size(LabelSize::Small)
775                  .color(Color::Muted),
776              )
777              .into_any_element()
778        } else {
779            ConfiguredApiCard::new(configured_card_label)
780                .disabled(env_var_set)
781                .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
782                .when(env_var_set, |this| {
783                    this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
784                })
785                .into_any_element()
786        }
787    }
788
789    fn render_api_url_editor(&self, cx: &Context<Self>) -> Div {
790        let api_url = OllamaLanguageModelProvider::api_url(cx);
791        let custom_api_url_set = api_url != OLLAMA_API_URL;
792
793        if custom_api_url_set {
794            h_flex()
795                .p_3()
796                .justify_between()
797                .rounded_md()
798                .border_1()
799                .border_color(cx.theme().colors().border)
800                .bg(cx.theme().colors().elevated_surface_background)
801                .child(
802                    h_flex()
803                        .gap_2()
804                        .child(Icon::new(IconName::Check).color(Color::Success))
805                        .child(v_flex().gap_1().child(Label::new(api_url))),
806                )
807                .child(
808                    Button::new("reset-api-url", "Reset API URL")
809                        .label_size(LabelSize::Small)
810                        .icon(IconName::Undo)
811                        .icon_size(IconSize::Small)
812                        .icon_position(IconPosition::Start)
813                        .layer(ElevationIndex::ModalSurface)
814                        .on_click(
815                            cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
816                        ),
817                )
818        } else {
819            v_flex()
820                .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| {
821                    this.save_api_url(cx);
822                    cx.notify();
823                }))
824                .gap_2()
825                .child(self.api_url_editor.clone())
826        }
827    }
828}
829
830impl Render for ConfigurationView {
831    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
832        let is_authenticated = self.state.read(cx).is_authenticated();
833
834        v_flex()
835            .gap_2()
836            .child(Self::render_instructions(cx))
837            .child(self.render_api_url_editor(cx))
838            .child(self.render_api_key_editor(cx))
839            .child(
840                h_flex()
841                    .w_full()
842                    .justify_between()
843                    .gap_2()
844                    .child(
845                        h_flex()
846                            .w_full()
847                            .gap_2()
848                            .map(|this| {
849                                if is_authenticated {
850                                    this.child(
851                                        Button::new("ollama-site", "Ollama")
852                                            .style(ButtonStyle::Subtle)
853                                            .icon(IconName::ArrowUpRight)
854                                            .icon_size(IconSize::XSmall)
855                                            .icon_color(Color::Muted)
856                                            .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE))
857                                            .into_any_element(),
858                                    )
859                                } else {
860                                    this.child(
861                                        Button::new("download_ollama_button", "Download Ollama")
862                                            .style(ButtonStyle::Subtle)
863                                            .icon(IconName::ArrowUpRight)
864                                            .icon_size(IconSize::XSmall)
865                                            .icon_color(Color::Muted)
866                                            .on_click(move |_, _, cx| {
867                                                cx.open_url(OLLAMA_DOWNLOAD_URL)
868                                            })
869                                            .into_any_element(),
870                                    )
871                                }
872                            })
873                            .child(
874                                Button::new("view-models", "View All Models")
875                                    .style(ButtonStyle::Subtle)
876                                    .icon(IconName::ArrowUpRight)
877                                    .icon_size(IconSize::XSmall)
878                                    .icon_color(Color::Muted)
879                                    .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
880                            ),
881                    )
882                    .map(|this| {
883                        if is_authenticated {
884                            this.child(
885                                ButtonLike::new("connected")
886                                    .disabled(true)
887                                    .cursor_style(CursorStyle::Arrow)
888                                    .child(
889                                        h_flex()
890                                            .gap_2()
891                                            .child(Icon::new(IconName::Check).color(Color::Success))
892                                            .child(Label::new("Connected"))
893                                            .into_any_element(),
894                                    )
895                                    .child(
896                                        IconButton::new("refresh-models", IconName::RotateCcw)
897                                            .tooltip(Tooltip::text("Refresh Models"))
898                                            .on_click(cx.listener(|this, _, _, cx| {
899                                                this.state.update(cx, |state, _| {
900                                                    state.fetched_models.clear();
901                                                });
902                                                this.retry_connection(cx);
903                                            })),
904                                    ),
905                            )
906                        } else {
907                            this.child(
908                                Button::new("retry_ollama_models", "Connect")
909                                    .icon_position(IconPosition::Start)
910                                    .icon_size(IconSize::XSmall)
911                                    .icon(IconName::PlayOutlined)
912                                    .on_click(
913                                        cx.listener(move |this, _, _, cx| {
914                                            this.retry_connection(cx)
915                                        }),
916                                    ),
917                            )
918                        }
919                    }),
920            )
921    }
922}
923
924fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
925    ollama::OllamaTool::Function {
926        function: OllamaFunctionTool {
927            name: tool.name,
928            description: Some(tool.description),
929            parameters: Some(tool.input_schema),
930        },
931    }
932}