Improve Ollama tool use (#30120)

Richard Feldman and Antonio Scandurra created

<img width="458" alt="Screenshot 2025-05-07 at 9 37 39 AM"
src="https://github.com/user-attachments/assets/80f8a9b8-6a13-4e84-b91d-140e11475638"
/>

<img width="603" alt="Screenshot 2025-05-07 at 9 37 33 AM"
src="https://github.com/user-attachments/assets/7fe67a68-3885-4a0e-a282-aad37e92068b"
/>


Release Notes:

- Ollama models no longer require the supports_tools field in settings
(defaults to false)

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/agent/src/profile_selector.rs                | 65 +++++++-------
crates/assistant_settings/src/assistant_settings.rs |  2 
crates/language_models/src/provider/ollama.rs       | 12 +
crates/ollama/src/ollama.rs                         |  4 
4 files changed, 44 insertions(+), 39 deletions(-)

Detailed changes

crates/agent/src/profile_selector.rs 🔗

@@ -156,46 +156,47 @@ impl Render for ProfileSelector {
             .default_model()
             .map_or(false, |default| default.model.supports_tools());
 
-        let this = cx.entity().clone();
-        let focus_handle = self.focus_handle.clone();
-
-        let trigger_button = if supports_tools {
-            Button::new("profile-selector-model", selected_profile)
+        if supports_tools {
+            let this = cx.entity().clone();
+            let focus_handle = self.focus_handle.clone();
+            let trigger_button = Button::new("profile-selector-model", selected_profile)
                 .label_size(LabelSize::Small)
                 .color(Color::Muted)
                 .icon(IconName::ChevronDown)
                 .icon_size(IconSize::XSmall)
                 .icon_position(IconPosition::End)
-                .icon_color(Color::Muted)
+                .icon_color(Color::Muted);
+
+            PopoverMenu::new("profile-selector")
+                .trigger_with_tooltip(trigger_button, {
+                    let focus_handle = focus_handle.clone();
+                    move |window, cx| {
+                        Tooltip::for_action_in(
+                            "Toggle Profile Menu",
+                            &ToggleProfileSelector,
+                            &focus_handle,
+                            window,
+                            cx,
+                        )
+                    }
+                })
+                .anchor(if self.documentation_side == DocumentationSide::Left {
+                    gpui::Corner::BottomRight
+                } else {
+                    gpui::Corner::BottomLeft
+                })
+                .with_handle(self.menu_handle.clone())
+                .menu(move |window, cx| {
+                    Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
+                })
+                .into_any_element()
         } else {
-            Button::new("tools-not-supported-button", "No Tools")
+            Button::new("tools-not-supported-button", "Tools Unsupported")
                 .disabled(true)
                 .label_size(LabelSize::Small)
                 .color(Color::Muted)
-                .tooltip(Tooltip::text("The current model does not support tools."))
-        };
-
-        PopoverMenu::new("profile-selector")
-            .trigger_with_tooltip(trigger_button, {
-                let focus_handle = focus_handle.clone();
-                move |window, cx| {
-                    Tooltip::for_action_in(
-                        "Toggle Profile Menu",
-                        &ToggleProfileSelector,
-                        &focus_handle,
-                        window,
-                        cx,
-                    )
-                }
-            })
-            .anchor(if self.documentation_side == DocumentationSide::Left {
-                gpui::Corner::BottomRight
-            } else {
-                gpui::Corner::BottomLeft
-            })
-            .with_handle(self.menu_handle.clone())
-            .menu(move |window, cx| {
-                Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
-            })
+                .tooltip(Tooltip::text("This model does not support tools."))
+                .into_any_element()
+        }
     }
 }

crates/language_models/src/provider/ollama.rs 🔗

@@ -52,7 +52,7 @@ pub struct AvailableModel {
     /// The number of seconds to keep the connection open after the last request
     pub keep_alive: Option<KeepAlive>,
     /// Whether the model supports tools
-    pub supports_tools: bool,
+    pub supports_tools: Option<bool>,
 }
 
 pub struct OllamaLanguageModelProvider {
@@ -93,8 +93,12 @@ impl State {
                     async move {
                         let name = model.name.as_str();
                         let capabilities = show_model(http_client.as_ref(), &api_url, name).await?;
-                        let ollama_model =
-                            ollama::Model::new(name, None, None, capabilities.supports_tools());
+                        let ollama_model = ollama::Model::new(
+                            name,
+                            None,
+                            None,
+                            Some(capabilities.supports_tools()),
+                        );
                         Ok(ollama_model)
                     }
                 });
@@ -317,7 +321,7 @@ impl LanguageModel for OllamaLanguageModel {
     }
 
     fn supports_tools(&self) -> bool {
-        self.model.supports_tools
+        self.model.supports_tools.unwrap_or(false)
     }
 
     fn telemetry_id(&self) -> String {

crates/ollama/src/ollama.rs 🔗

@@ -37,7 +37,7 @@ pub struct Model {
     pub display_name: Option<String>,
     pub max_tokens: usize,
     pub keep_alive: Option<KeepAlive>,
-    pub supports_tools: bool,
+    pub supports_tools: Option<bool>,
 }
 
 fn get_max_tokens(name: &str) -> usize {
@@ -67,7 +67,7 @@ impl Model {
         name: &str,
         display_name: Option<&str>,
         max_tokens: Option<usize>,
-        supports_tools: bool,
+        supports_tools: Option<bool>,
     ) -> Self {
         Self {
             name: name.to_owned(),