This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-24631-ollama-doctor-tui
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e0391d6570d4a15cfe6d0563e4c5fad3b6b66688
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 6 17:02:56 2026 +0200

    CAMEL-24631: Ollama detection in camel doctor and TUI, LLM_BASE_URL support 
for OpenAI-compatible servers
    
    - camel doctor detects Ollama at localhost:11434 and lists pulled models
    - TUI doctor popup shows Ollama status and model count; AI provider row
      recognises local Ollama so it no longer warns when no API key is set
    - F8 AI panel shows a rich setup guide (Ollama install for macOS+Linux,
      memory table, OpenAI-compatible server section) when no provider is found
    - LlmClient respects LLM_BASE_URL / OPENAI_BASE_URL so LLM_API_KEY can
      point at any OpenAI-compatible server (LM Studio, vLLM, llama.cpp, …)
    - Docs: camel-jbang-ai.adoc and camel-jbang-tui.adoc updated with Ollama
      native install guide, model memory table (incl. hermes3), and
      OpenAI-compatible server section
    
    CAMEL-24631: remove small models from docs/guides and add DoctorPopup 
small-model warning
    
    - Remove models <14B (llama3.2:3b, llama3.2:8b, hermes3:8b) from all
      guides; only list ≥14B models suitable for tool calling
    - Add prominent tool-calling warning to camel-jbang-tui.adoc and
      camel-jbang-ai.adoc explaining models <14B don't reliably call tools
    - DoctorPopup: show WARN icon and "F8 needs ≥14B" hint when all pulled
      Ollama models are smaller than 14B parameters
    - DoctorPopup: update no-models hint to suggest qwen2.5:14b
    - AiPanel setup guide: update model table to ≥14B only with tool-calling
      warning blockquote
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-ai.adoc         |  68 +++++++++--
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  67 +++++++++++
 .../camel/dsl/jbang/core/commands/Doctor.java      |  58 ++++++++++
 .../camel/dsl/jbang/core/commands/LlmClient.java   |   8 +-
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  78 ++++++++++++-
 .../dsl/jbang/core/commands/tui/DoctorPopup.java   | 126 ++++++++++++++++++++-
 6 files changed, 392 insertions(+), 13 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
index b68e7245cafe..baa4f1e33852 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
@@ -118,9 +118,10 @@ All commands auto-detect the LLM provider. The detection 
order is:
 2. `CLOUD_ML_REGION` + `ANTHROPIC_VERTEX_PROJECT_ID` → Vertex AI (`ask` and 
`explain` only)
 3. `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT` → Azure OpenAI (uses the 
`api-key` header; optional `AZURE_OPENAI_DEPLOYMENT_NAME` and 
`AZURE_OPENAI_API_VERSION`)
 4. `GEMINI_API_KEY` environment variable → Google Gemini native API 
(`generativelanguage.googleapis.com`). With `--api-type=gemini`, 
`GOOGLE_API_KEY` is also accepted.
-5. `OPENAI_API_KEY` or `LLM_API_KEY` → OpenAI-compatible API
-6. Ollama running via `camel infra` → local Ollama
-7. Ollama at `localhost:11434` → local Ollama
+5. `OPENAI_API_KEY` → OpenAI API (`api.openai.com`)
+6. `LLM_API_KEY` + optional `LLM_BASE_URL` (or `OPENAI_BASE_URL`) → any 
OpenAI-compatible API
+7. Ollama running via `camel infra` → local Ollama
+8. Ollama at `localhost:11434` → local Ollama
 
 Override with explicit options:
 
@@ -134,16 +135,65 @@ camel ask "check health" --api-type=ollama 
--model=llama3.1
 
 === Using a local model with Ollama
 
-Start Ollama as a dev service and the CLI detects it automatically:
+Install Ollama natively for the best performance — the native binary uses GPU 
acceleration
+(Metal on macOS, CUDA/ROCm on Linux). Running Ollama through Docker (`camel 
infra run ollama`)
+bypasses the GPU and makes inference much slower.
 
 [source,bash]
 ----
-camel infra run ollama
+# macOS
+brew install ollama
+
+# Linux
+curl -fsSL https://ollama.com/install.sh | sh
+
+# Pull a model and start asking
+ollama pull qwen2.5:32b
 camel ask "what routes are running?"
 ----
 
-The default model is `llama3.2`. When using Ollama, the CLI checks what models 
are available
-locally and auto-selects a suitable one if a better model is installed.
+Ollama at `localhost:11434` is auto-detected — no environment variable needed.
+The CLI checks what models are available and auto-selects a suitable one.
+
+==== Model requirements
+
+`camel ask` and the TUI F8 panel rely on tool calling to inspect your running 
Camel process.
+Models smaller than ~14B do not reliably invoke tools and answer from training 
knowledge instead.
+Use at least a 14B model; 32B is recommended.
+
+[options="header"]
+|===
+| Model | RAM (Q4) | Notes
+| `qwen2.5:14b` | ~9 GB | Minimum recommended
+| `qwen2.5:32b` | ~20 GB | Best balance of speed and quality
+| `deepseek-r1:32b` | ~20 GB | Strong reasoning
+| `hermes3:70b` | ~43 GB | Excellent tool calling, needs 64 GB+
+| `llama3.3:70b` | ~43 GB | Best open model, needs 64 GB+
+|===
+
+On Apple Silicon, all RAM is unified — a 64 GB M-series Mac can run 
`llama3.3:70b` comfortably alongside the OS and other dev tools.
+
+=== Using an OpenAI-compatible local server
+
+Many local LLM servers expose an OpenAI-compatible API. Use `LLM_API_KEY` and 
`LLM_BASE_URL`
+to point the CLI at any of them:
+
+[source,bash]
+----
+export LLM_API_KEY=any-value      # required but can be any non-empty string
+export LLM_BASE_URL=http://localhost:1234   # your server's base URL
+camel ask "what routes are running?"
+----
+
+`OPENAI_BASE_URL` is accepted as an alternative to `LLM_BASE_URL` (common in 
other tools).
+
+Common OpenAI-compatible servers:
 
-TIP: For best results with the `ask` command's tool-calling capabilities,
-use a model that supports function calling well (e.g., `llama3.1`, `qwen3`, 
`mistral-nemo`).
+[options="header"]
+|===
+| Server | Default port | Notes
+| https://lmstudio.ai[LM Studio] | 1234 | GUI app, Mac/Windows/Linux
+| https://github.com/vllm-project/vllm[vLLM] | 8000 | Production-grade, NVIDIA 
GPU
+| https://github.com/ggerganov/llama.cpp[llama.cpp server] | 8080 | Runs on 
CPU and GPU
+| https://gpt4all.io[GPT4All] | 4891 | Desktop app
+|===
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index eac4a6e96dc5..174c298f9810 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -738,6 +738,73 @@ server that lets AI coding assistants interact with the 
dashboard.
 
 The TUI is fully functional on its own -- AI integration is entirely optional.
 
+=== Choosing an AI provider
+
+Press *F8* to open the built-in AI prompt panel. The panel auto-detects a 
provider in this order:
+
+1. `ANTHROPIC_API_KEY` → Anthropic Claude
+2. `CLOUD_ML_REGION` + `ANTHROPIC_VERTEX_PROJECT_ID` → Vertex AI
+3. `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT` → Azure OpenAI
+4. `GEMINI_API_KEY` → Google Gemini
+5. `OPENAI_API_KEY` → OpenAI
+6. `LLM_API_KEY` + optional `LLM_BASE_URL` → any OpenAI-compatible server
+7. Ollama at `localhost:11434` → local Ollama (auto-detected, no key needed)
+
+Press *Ctrl+P* inside the AI panel to switch provider or model at any time.
+
+==== Using Ollama (local, no API key)
+
+Install Ollama natively for best performance — the native binary uses GPU 
acceleration
+(Metal on macOS, CUDA/ROCm on Linux):
+
+[source,bash]
+----
+# macOS
+brew install ollama
+
+# Linux
+curl -fsSL https://ollama.com/install.sh | sh
+
+# Pull a model — then open the TUI and press F8
+ollama pull qwen2.5:32b
+camel tui
+----
+
+Ollama at `localhost:11434` is auto-detected. No configuration needed.
+
+IMPORTANT: The F8 AI panel works by invoking built-in tools to inspect your 
running Camel process.
+Models smaller than ~14B do not reliably call tools and answer from training 
knowledge instead.
+Use at least a 14B model; 32B is recommended.
+
+*Models that work well* (tool-calling capable, ≥14B, default Q4_K_M 
quantization):
+
+[options="header"]
+|===
+| Model | RAM | Notes
+| `qwen2.5:14b` | ~9 GB | Minimum recommended
+| `qwen2.5:32b` | ~20 GB | Best balance of speed and quality
+| `deepseek-r1:32b` | ~20 GB | Strong reasoning
+| `hermes3:70b` | ~43 GB | Excellent tool calling, needs 64 GB+
+| `llama3.3:70b` | ~43 GB | Best open model, needs 64 GB+
+|===
+
+NOTE: `camel infra run ollama` runs Ollama in Docker and bypasses GPU 
acceleration,
+making inference significantly slower. Native install is preferred for 
development.
+
+==== Using an OpenAI-compatible local server
+
+Set `LLM_API_KEY` and `LLM_BASE_URL` to connect to any OpenAI-compatible server
+(LM Studio, vLLM, llama.cpp, GPT4All, …):
+
+[source,bash]
+----
+export LLM_API_KEY=any-value
+export LLM_BASE_URL=http://localhost:1234
+camel tui
+----
+
+`OPENAI_BASE_URL` is also accepted as an alternative to `LLM_BASE_URL`.
+
 === Why This Matters
 
 When an AI agent connects to the TUI via MCP, it gains the same level of 
visibility that you
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
index 98b25fd985de..a72a6981a78b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Doctor.java
@@ -19,7 +19,13 @@ package org.apache.camel.dsl.jbang.core.commands;
 import java.io.File;
 import java.io.OutputStream;
 import java.net.ServerSocket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
 import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -31,6 +37,9 @@ import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.tooling.maven.MavenDownloaderImpl;
 import org.apache.camel.tooling.maven.MavenResolutionException;
 import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine.Command;
 
 @Command(name = "doctor", description = "Checks the environment and reports 
potential issues",
@@ -55,6 +64,7 @@ public class Doctor extends CamelCommand {
         checkJBang();
         checkMavenRepository();
         checkContainerRuntime();
+        checkOllama();
         checkCommonPorts();
         checkDiskSpace();
 
@@ -133,6 +143,54 @@ public class Doctor extends CamelCommand {
         printer().printf("  Container:   Not found (optional — needed for 
running external infra services)%n");
     }
 
+    private void checkOllama() {
+        try {
+            HttpClient client = HttpClient.newBuilder()
+                    .connectTimeout(Duration.ofSeconds(3))
+                    .build();
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create("http://localhost:11434/api/tags";))
+                    .timeout(Duration.ofSeconds(3))
+                    .GET()
+                    .build();
+            HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
+            if (response.statusCode() == 200) {
+                List<String> models = parseOllamaModels(response.body());
+                if (models.isEmpty()) {
+                    printer().printf("  Ollama:      Running at 
localhost:11434 — no models pulled yet%n");
+                } else {
+                    printer().printf("  Ollama:      Running at 
localhost:11434 — models: %s%n",
+                            String.join(", ", models));
+                }
+            } else {
+                printer().printf("  Ollama:      Not detected (optional — 
start for local AI with F8 in TUI)%n");
+            }
+        } catch (Exception e) {
+            printer().printf("  Ollama:      Not detected (optional — start 
for local AI with F8 in TUI)%n");
+        }
+    }
+
+    private List<String> parseOllamaModels(String json) {
+        List<String> names = new ArrayList<>();
+        try {
+            JsonObject root = (JsonObject) Jsoner.deserialize(json);
+            JsonArray models = (JsonArray) root.get("models");
+            if (models != null) {
+                for (Object entry : models) {
+                    if (entry instanceof JsonObject model) {
+                        Object name = model.get("name");
+                        if (name != null) {
+                            names.add(name.toString());
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // ignore parse errors — not critical
+        }
+        return names;
+    }
+
     private void checkCommonPorts() {
         StringBuilder conflicts = new StringBuilder();
         for (int port : new int[] { 8080, 8443, 9090 }) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
index 94f94b788887..8ebc1b78fcb1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/LlmClient.java
@@ -1795,7 +1795,13 @@ public class LlmClient {
             apiKey = key;
             openAiAuthMode = OpenAiAuthMode.bearer;
             if (url == null || url.isBlank()) {
-                url = "https://api.openai.com";;
+                // LLM_BASE_URL / OPENAI_BASE_URL let users point at any 
OpenAI-compatible
+                // server (LM Studio, vLLM, LocalAI, Jan, …) without a CLI flag
+                String baseUrl = System.getenv("LLM_BASE_URL");
+                if (baseUrl == null || baseUrl.isBlank()) {
+                    baseUrl = System.getenv("OPENAI_BASE_URL");
+                }
+                url = (baseUrl != null && !baseUrl.isBlank()) ? 
stripTrailingSlash(baseUrl) : "https://api.openai.com";;
             }
             return true;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index fda8cd5ff4dd..904455a6b085 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -1054,7 +1054,11 @@ class AiPanel {
         StringBuilder md = new StringBuilder();
 
         if (initError != null) {
-            md.append("**Error:** ").append(initError).append("\n\n");
+            if (initError.startsWith("No LLM service reachable")) {
+                md.append(buildAiSetupGuide());
+            } else {
+                md.append("**Error:** ").append(initError).append("\n\n");
+            }
         } else if (conversation.isEmpty() && !thinking.get() && 
!slashHintsVisible) {
             frame.renderWidget(
                     Paragraph.from(Line.from(Span.styled("Ask a question about 
your Camel application...", Style.EMPTY.dim()))),
@@ -1556,6 +1560,78 @@ class AiPanel {
         frame.renderWidget(Paragraph.from(new dev.tamboui.text.Text(lines, 
dev.tamboui.layout.Alignment.LEFT)), area);
     }
 
+    private String buildAiSetupGuide() {
+        return """
+                ## AI Assistant — Getting Started
+
+                No LLM provider was detected. Choose one of the options below, 
then press **F8** to reopen this panel.
+
+                > **Tool calling is required.** This panel inspects your Camel 
process by
+                > invoking built-in tools. Models smaller than ~14B do not 
reliably call
+                > tools and will answer from training knowledge instead — use 
at least 14B,
+                > 32B recommended.
+
+                ---
+
+                ### Option A: Local — Ollama (no API key needed)
+
+                Run models entirely on your machine — no data leaves your host.
+
+                ```
+                # macOS
+                brew install ollama
+
+                # Linux
+                curl -fsSL https://ollama.com/install.sh | sh
+
+                # then on both:
+                ollama serve             # start the daemon (skip if 
auto-started)
+                ollama pull qwen2.5:32b  # recommended
+                ```
+
+                Ollama is auto-detected at `localhost:11434` — no 
configuration needed.
+
+                **Models that work well** (tool-calling capable, ≥14B):
+
+                | Model | RAM | Notes |
+                |---|---|---|
+                | qwen2.5:14b | ~9 GB | Minimum recommended |
+                | qwen2.5:32b | ~20 GB | Best balance of speed and quality |
+                | deepseek-r1:32b | ~20 GB | Strong reasoning |
+                | hermes3:70b  | ~43 GB | Excellent tool calling, needs 64 GB+ 
|
+                | llama3.3:70b | ~43 GB | Best open model, needs 64 GB+ |
+
+                **Tip:** Install Ollama natively — `camel infra run ollama` 
uses Docker and
+                loses GPU acceleration (Metal on macOS, CUDA on Linux), making 
inference
+                much slower. Native install is always preferred for 
development use.
+
+                ---
+
+                ### Option B: Cloud provider (API key required)
+
+                Set one environment variable before starting the TUI:
+
+                | Variable | Provider |
+                |---|---|
+                | `ANTHROPIC_API_KEY` | Claude |
+                | `OPENAI_API_KEY` | OpenAI (GPT-4o etc.) |
+                | `GEMINI_API_KEY` | Gemini |
+                | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_ENDPOINT` | Azure 
OpenAI |
+                | `WATSONX_APIKEY` | IBM watsonx.ai |
+
+                For any **OpenAI-compatible** server (LM Studio, vLLM, 
llama.cpp, GPT4All, …):
+
+                ```
+                export LLM_API_KEY=any-value
+                export LLM_BASE_URL=http://localhost:1234
+                ```
+
+                `OPENAI_BASE_URL` is also supported as an alternative to 
`LLM_BASE_URL`.
+
+                Or press **Ctrl+P** to select and configure a provider now.
+                """;
+    }
+
     private String buildSystemPrompt() {
         StringBuilder sb = new StringBuilder();
         sb.append("You are an Apache Camel assistant running inside the Camel 
TUI terminal console. ");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
index 9623f115eac6..0c3351790e56 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopup.java
@@ -19,6 +19,11 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.io.File;
 import java.io.OutputStream;
 import java.net.ServerSocket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -42,6 +47,9 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.tooling.maven.MavenDownloaderImpl;
 import org.apache.camel.tooling.maven.MavenResolutionException;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
 
@@ -70,6 +78,7 @@ class DoctorPopup {
         checkJBang(lines);
         checkMavenRepository(lines);
         checkContainerRuntime(lines);
+        checkOllama(lines);
         checkCommonPorts(lines);
         checkDiskSpace(lines);
         checkAiProvider(lines);
@@ -277,6 +286,117 @@ class DoctorPopup {
         }
     }
 
+    private void checkOllama(List<Line> result) {
+        try {
+            HttpClient client = HttpClient.newBuilder()
+                    .connectTimeout(Duration.ofSeconds(3))
+                    .build();
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create("http://localhost:11434/api/tags";))
+                    .timeout(Duration.ofSeconds(3))
+                    .GET()
+                    .build();
+            HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
+            if (response.statusCode() == 200) {
+                List<String> models = parseOllamaModels(response.body());
+                if (models.isEmpty()) {
+                    result.add(Line.from(
+                            Span.raw(TuiIcons.indent(TuiIcons.MCP)),
+                            Span.styled(String.format("%-14s", "Ollama"), 
Theme.muted()),
+                            Span.raw(String.format("%-30s", "Running — no 
models pulled yet")),
+                            Span.raw(" " + TuiIcons.WARN)));
+                    result.add(Line.from(Span.styled("                    Run: 
ollama pull qwen2.5:14b",
+                            Style.EMPTY.dim())));
+                } else {
+                    boolean allSmall = 
models.stream().allMatch(DoctorPopup::isSmallModel);
+                    String icon = allSmall ? TuiIcons.WARN : TuiIcons.OK;
+                    result.add(Line.from(
+                            Span.raw(TuiIcons.indent(TuiIcons.MCP)),
+                            Span.styled(String.format("%-14s", "Ollama"), 
Theme.muted()),
+                            Span.raw(String.format("%-30s", models.size() + " 
model(s) available")),
+                            Span.raw(" " + icon)));
+                    result.add(Line.from(Span.styled(
+                            "                    " + 
TuiHelper.truncate(String.join(", ", models), 44),
+                            Style.EMPTY.dim())));
+                    if (allSmall) {
+                        result.add(Line.from(Span.styled(
+                                "                    F8 needs ≥14B — run: 
ollama pull qwen2.5:14b",
+                                Style.EMPTY.dim())));
+                    }
+                }
+            } else {
+                result.add(Line.from(
+                        Span.raw(TuiIcons.indent(TuiIcons.MCP)),
+                        Span.styled(String.format("%-14s", "Ollama"), 
Theme.muted()),
+                        Span.raw(String.format("%-30s", "Not running 
(optional)")),
+                        Span.raw(" " + TuiIcons.WARN)));
+                result.add(Line.from(Span.styled("                    Run: 
ollama serve",
+                        Style.EMPTY.dim())));
+            }
+        } catch (Exception e) {
+            result.add(Line.from(
+                    Span.raw(TuiIcons.indent(TuiIcons.MCP)),
+                    Span.styled(String.format("%-14s", "Ollama"), 
Theme.muted()),
+                    Span.raw(String.format("%-30s", "Not running (optional)")),
+                    Span.raw(" " + TuiIcons.WARN)));
+            result.add(Line.from(Span.styled("                    Run: ollama 
serve",
+                    Style.EMPTY.dim())));
+        }
+    }
+
+    private boolean isOllamaRunning() {
+        try {
+            HttpClient client = HttpClient.newBuilder()
+                    .connectTimeout(Duration.ofSeconds(2))
+                    .build();
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create("http://localhost:11434/api/tags";))
+                    .timeout(Duration.ofSeconds(2))
+                    .GET()
+                    .build();
+            return client.send(request, 
HttpResponse.BodyHandlers.discarding()).statusCode() == 200;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private static boolean isSmallModel(String name) {
+        // heuristic: extract the size suffix like :3b, :7b, :8b, :11b from 
the model name
+        int colon = name.lastIndexOf(':');
+        String tag = colon >= 0 ? name.substring(colon + 1).toLowerCase() : "";
+        if (tag.matches("\\d+b.*")) {
+            int b = tag.indexOf('b');
+            try {
+                int params = Integer.parseInt(tag.substring(0, b));
+                return params < 14;
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private List<String> parseOllamaModels(String json) {
+        List<String> names = new ArrayList<>();
+        try {
+            JsonObject root = (JsonObject) Jsoner.deserialize(json);
+            JsonArray models = (JsonArray) root.get("models");
+            if (models != null) {
+                for (Object entry : models) {
+                    if (entry instanceof JsonObject model) {
+                        Object name = model.get("name");
+                        if (name != null) {
+                            names.add(name.toString());
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // ignore parse errors
+        }
+        return names;
+    }
+
     private static boolean envSet(String name) {
         String v = System.getenv(name);
         return v != null && !v.isBlank();
@@ -298,6 +418,8 @@ class DoctorPopup {
             provider = "watsonx.ai";
         } else if (envSet("LLM_API_KEY")) {
             provider = "Custom (LLM_API_KEY)";
+        } else if (isOllamaRunning()) {
+            provider = "Ollama (local)";
         }
         if (provider != null) {
             result.add(Line.from(
@@ -309,10 +431,10 @@ class DoctorPopup {
             result.add(Line.from(
                     Span.raw(TuiIcons.indent(TuiIcons.MCP)),
                     Span.styled(String.format("%-14s", "AI"), Theme.muted()),
-                    Span.raw(String.format("%-30s", "No API key configured")),
+                    Span.raw(String.format("%-30s", "No AI provider 
configured")),
                     Span.raw(" " + TuiIcons.WARN)));
             result.add(Line.from(Span.styled(
-                    "                    Set ANTHROPIC_API_KEY, 
AZURE_OPENAI_*, GEMINI_API_KEY, OPENAI_API_KEY, or WATSONX_APIKEY",
+                    "                    Set ANTHROPIC_API_KEY, 
OPENAI_API_KEY, GEMINI_API_KEY, AZURE_OPENAI_*, WATSONX_APIKEY, 
LLM_API_KEY+LLM_BASE_URL, or start Ollama",
                     Style.EMPTY.dim())));
         }
     }

Reply via email to