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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new c211e71acf1b CAMEL-24631: Detect Ollama in camel doctor and TUI, 
LLM_BASE_URL, small-model warning, and setup guide (#26162)
c211e71acf1b is described below

commit c211e71acf1bd3419b07b9e37cd43b8d0ab1a1b8
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 7 13:22:41 2026 +0200

    CAMEL-24631: Detect Ollama in camel doctor and TUI, LLM_BASE_URL, 
small-model warning, and setup guide (#26162)
    
    * CAMEL-24631: Detect Ollama in camel doctor and TUI doctor popup
    
    Add OllamaDoctorSupport to probe localhost:11434 and camel infra Ollama
    PID files, list pulled models, and format doctor output consistently.
    
    - camel doctor prints Ollama running state and model names
    - TUI doctor popup adds an Ollama row with model count and names
    - AI provider row treats running Ollama as a valid local provider
    - Expose LlmClient.endpointUrl() for resolved endpoint reporting
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * CAMEL-24631: Probe Ollama once in TUI doctor and harden tests
    
    Cache OllamaDoctorSupport.detect() result when opening the TUI doctor
    popup to avoid duplicate localhost probes. Replace flaky detect() test
    that assumed no local Ollama with an explicit HTTP-server probe test.
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * CAMEL-24631: Address doctor review feedback
    
    - Probe Ollama once per TUI doctor open; injectable detect(LlmClient) for 
tests
    - Warn when Ollama runs but no models are pulled; truncate long model hints
    - Use generic ollama serve hint; drop unrelated generated file churn
    - Expand tests for not-running detect, empty models, and truncation
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * CAMEL-24631: Skip integration tests during CI regen build
    
    regen.sh uses -DskipTests but Failsafe ITs still ran during install,
    causing unrelated modules (camel-docling, camel-spring-ai-image) to fail
    when Ollama/Docling test infra is unavailable. Add -DskipITs to match
    the regen intent of code generation only.
    
    Co-authored-by: Cursor Agent <[email protected]>
    
    * Regen
    
    * CAMEL-24631: add LLM_BASE_URL support, small-model warning, AiPanel setup 
guide, and docs
    
    - LlmClient: LLM_BASE_URL / OPENAI_BASE_URL env vars route LLM_API_KEY to 
any
      OpenAI-compatible server (LM Studio, vLLM, llama.cpp, GPT4All, …)
    - OllamaDoctorSupport: isSmallModel() / allModelsSmall() helpers; TUI shows 
WARN
      and upgrade hint when all pulled models are <14B (too small for reliable 
tool calling)
    - AiPanel: renders a guided setup page (Ollama install steps, model table, 
cloud
      provider table) instead of a bare error when no LLM provider is reachable
    - Docs: camel-jbang-ai.adoc and camel-jbang-tui.adoc — native Ollama 
install,
      ≥14B model requirements, tool-calling warning, OpenAI-compatible server 
section
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * Regen
    
    * CAMEL-24631: Address review feedback — fix LLM_BASE_URL security, 
allModelsSmall logic, and add tests
    
    - LlmClient: check OPENAI_BASE_URL first; only fall back to LLM_BASE_URL 
when OPENAI_API_KEY is not set, so a real OpenAI API key is never redirected to 
an unintended server
    - OllamaDoctorSupport: add isLargeModel() helper and rewrite 
allModelsSmall() to use noneMatch(isLargeModel) so models without an explicit 
numeric size tag (e.g. llama3.2:latest) are treated as "not known large" rather 
than "small = false", giving users a correct small-model warning
    - OllamaDoctorSupportTest: add 8 unit tests covering isSmallModel and 
allModelsSmall edge cases (small tags, large tags, non-numeric tags, model 
without colon, empty/null list, mixed sizes, unknown size tag)
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24631: Disable SpringAiImageOllamaIT — too slow and 
resource-intensive
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../springai/image/SpringAiImageOllamaIT.java      |   4 +-
 .../modules/ROOT/pages/camel-jbang-ai.adoc         |  68 ++++++-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  67 +++++++
 .../camel/dsl/jbang/core/commands/Doctor.java      |  14 ++
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  19 +-
 .../dsl/jbang/core/common/OllamaDoctorSupport.java | 147 +++++++++++++++
 .../camel/dsl/jbang/core/commands/DoctorTest.java  |  33 ++++
 .../jbang/core/common/OllamaDoctorSupportTest.java | 197 +++++++++++++++++++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  78 +++++++-
 .../dsl/jbang/core/commands/tui/DoctorPopup.java   | 100 +++++++++--
 .../core/commands/tui/DoctorPopupOllamaTest.java   | 112 ++++++++++++
 etc/scripts/regen.sh                               |   4 +-
 12 files changed, 809 insertions(+), 34 deletions(-)

diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-image/src/test/java/org/apache/camel/component/springai/image/SpringAiImageOllamaIT.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-image/src/test/java/org/apache/camel/component/springai/image/SpringAiImageOllamaIT.java
index d7de8f9e3638..79a53a35b0c2 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-image/src/test/java/org/apache/camel/component/springai/image/SpringAiImageOllamaIT.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-image/src/test/java/org/apache/camel/component/springai/image/SpringAiImageOllamaIT.java
@@ -30,9 +30,9 @@ import 
org.apache.camel.test.infra.ollama.services.OllamaService;
 import org.apache.camel.test.infra.ollama.services.OllamaServiceConfiguration;
 import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
 import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
-import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.io.TempDir;
 import org.slf4j.Logger;
@@ -54,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat;
  * Since Spring AI 2.0 the OpenAI models are built on the official openai-java 
SDK, so the Ollama endpoint is configured
  * through the SDK client options rather than through a Spring RestClient.
  */
-@DisabledIfEnvironmentVariable(named = "CI", matches = "true", disabledReason 
= "Disabled on CI (requires Ollama with flux2-klein model)")
+@Disabled("Test is too slow and resource-intensive — requires pulling a 4B 
parameter model")
 @Timeout(180)
 public class SpringAiImageOllamaIT extends CamelTestSupport {
 
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..d6dcda2070c3 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
@@ -27,6 +27,7 @@ import java.util.Set;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.InstallDetector;
+import org.apache.camel.dsl.jbang.core.common.OllamaDoctorSupport;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.tooling.maven.MavenDownloaderImpl;
 import org.apache.camel.tooling.maven.MavenResolutionException;
@@ -55,6 +56,7 @@ public class Doctor extends CamelCommand {
         checkJBang();
         checkMavenRepository();
         checkContainerRuntime();
+        checkOllama();
         checkCommonPorts();
         checkDiskSpace();
 
@@ -133,6 +135,18 @@ public class Doctor extends CamelCommand {
         printer().printf("  Container:   Not found (optional — needed for 
running external infra services)%n");
     }
 
+    private void checkOllama() {
+        printOllamaStatus(OllamaDoctorSupport.detect());
+    }
+
+    void printOllamaStatus(OllamaDoctorSupport.Status status) {
+        if (status.running()) {
+            printer().printf("  Ollama:      %s%n", 
OllamaDoctorSupport.cliRunningLine(status));
+        } else {
+            printer().printf("  Ollama:      %s%n", 
OllamaDoctorSupport.cliNotDetectedLine());
+        }
+    }
+
     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..4ada1d9fb0c6 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
@@ -187,6 +187,13 @@ public class LlmClient {
         return apiType;
     }
 
+    /**
+     * Resolved LLM endpoint URL after {@link #detectEndpoint()}, or the 
configured URL when set explicitly.
+     */
+    public String endpointUrl() {
+        return url;
+    }
+
     // -- Builder --
 
     public static LlmClient create() {
@@ -1795,7 +1802,17 @@ 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("OPENAI_BASE_URL");
+                if (baseUrl == null || baseUrl.isBlank()) {
+                    // Only consult LLM_BASE_URL when the key came from 
LLM_API_KEY to avoid
+                    // redirecting a real OPENAI_API_KEY to an unintended 
server
+                    if (System.getenv("OPENAI_API_KEY") == null || 
System.getenv("OPENAI_API_KEY").isBlank()) {
+                        baseUrl = System.getenv("LLM_BASE_URL");
+                    }
+                }
+                url = (baseUrl != null && !baseUrl.isBlank()) ? 
stripTrailingSlash(baseUrl) : "https://api.openai.com";;
             }
             return true;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupport.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupport.java
new file mode 100644
index 000000000000..b3ca9888eea3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupport.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+
+/**
+ * Detects a local Ollama instance for {@code camel doctor} and the TUI doctor 
popup.
+ */
+public final class OllamaDoctorSupport {
+
+    private static final String DEFAULT_OLLAMA_DISPLAY = "localhost:11434";
+
+    private OllamaDoctorSupport() {
+    }
+
+    public record Status(boolean running, String baseUrl, List<String> models) 
{
+
+        public static Status notRunning() {
+            return new Status(false, null, List.of());
+        }
+    }
+
+    /**
+     * Probes {@code camel infra run ollama} PID files and the default {@code 
http://localhost:11434} endpoint.
+     */
+    public static Status detect() {
+        return detect(LlmClient.create());
+    }
+
+    static Status detect(LlmClient client) {
+        client.withApiType(LlmClient.ApiType.ollama);
+        if (!client.detectEndpoint()) {
+            return Status.notRunning();
+        }
+        return new Status(true, client.endpointUrl(), client.listModels());
+    }
+
+    public static String formatDisplayHost(String baseUrl) {
+        if (baseUrl == null || baseUrl.isBlank()) {
+            return DEFAULT_OLLAMA_DISPLAY;
+        }
+        String host = baseUrl;
+        if (host.startsWith("http://";)) {
+            host = host.substring("http://".length());
+        } else if (host.startsWith("https://";)) {
+            host = host.substring("https://".length());
+        }
+        while (host.endsWith("/")) {
+            host = host.substring(0, host.length() - 1);
+        }
+        return host.isBlank() ? DEFAULT_OLLAMA_DISPLAY : host;
+    }
+
+    public static String formatModels(List<String> models) {
+        if (models == null || models.isEmpty()) {
+            return "no models pulled";
+        }
+        return models.stream().collect(Collectors.joining(", "));
+    }
+
+    public static String cliRunningLine(Status status) {
+        return "Running at " + formatDisplayHost(status.baseUrl()) + " — 
models: " + formatModels(status.models());
+    }
+
+    public static String cliNotDetectedLine() {
+        return "Not detected (optional — start for local AI with F8 in TUI)";
+    }
+
+    public static String tuiRunningSummary(Status status, int maxLength) {
+        String summary = formatDisplayHost(status.baseUrl()) + " (" + 
modelCountLabel(status.models()) + ")";
+        if (maxLength > 0 && summary.length() > maxLength) {
+            return summary.substring(0, Math.max(0, maxLength - 3)) + "...";
+        }
+        return summary;
+    }
+
+    public static String modelCountLabel(List<String> models) {
+        if (models == null || models.isEmpty()) {
+            return "no models";
+        }
+        int count = models.size();
+        return count + (count == 1 ? " model" : " models");
+    }
+
+    /**
+     * Returns true when the model tag suggests fewer than 14B parameters. 
Tool-calling in the TUI F8 panel requires at
+     * least 14B.
+     */
+    public static boolean isSmallModel(String 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;
+    }
+
+    /**
+     * Returns true when the model tag confirms >= 14B parameters.
+     */
+    static boolean isLargeModel(String 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 {
+                return Integer.parseInt(tag.substring(0, b)) >= 14;
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true when no model in the list is known to be >= 14B. Models 
without an explicit numeric size tag (e.g.
+     * {@code llama3.2:latest}) are treated as unknown and counted as "not 
large", so callers should warn when this
+     * returns true.
+     */
+    public static boolean allModelsSmall(List<String> models) {
+        return models != null && !models.isEmpty() && 
models.stream().noneMatch(OllamaDoctorSupport::isLargeModel);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
index f58668110499..4dc89d813382 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DoctorTest.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.Optional;
 
 import org.apache.camel.dsl.jbang.core.common.InstallDetector;
+import org.apache.camel.dsl.jbang.core.common.OllamaDoctorSupport;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -93,6 +94,38 @@ class DoctorTest extends CamelCommandBaseTestSupport {
         Assertions.assertTrue(lines.size() >= 7, "Doctor should output at 
least 7 lines (banner + checks)");
     }
 
+    @Test
+    void shouldReportOllamaSection() throws Exception {
+        Doctor command = new Doctor(new CamelJBangMain().withPrinter(printer));
+        command.doCall();
+
+        assertThat(printer.getOutput()).contains("Ollama:");
+    }
+
+    @Test
+    void shouldReportOllamaNotDetectedWhenUnavailable() throws Exception {
+        Doctor command = new Doctor(new CamelJBangMain().withPrinter(printer));
+        command.printOllamaStatus(OllamaDoctorSupport.Status.notRunning());
+
+        assertThat(printer.getOutput())
+                .contains("Ollama:")
+                .contains("Not detected");
+    }
+
+    @Test
+    void shouldPrintRunningOllamaWithModels() throws Exception {
+        Doctor command = new Doctor(new CamelJBangMain().withPrinter(printer));
+        OllamaDoctorSupport.Status status = new OllamaDoctorSupport.Status(
+                true, "http://localhost:11434";, List.of("qwen2.5:32b", 
"llama3.2:latest"));
+
+        command.printOllamaStatus(status);
+
+        assertThat(printer.getOutput())
+                .contains("Ollama:")
+                .contains("Running at localhost:11434")
+                .contains("qwen2.5:32b, llama3.2:latest");
+    }
+
     @Test
     void skipsInstallCheckOutsideLauncher() throws Exception {
         System.clearProperty("camel.launcher");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupportTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupportTest.java
new file mode 100644
index 000000000000..9ae9131d2f9c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/OllamaDoctorSupportTest.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OllamaDoctorSupportTest {
+
+    private HttpServer server;
+
+    @AfterEach
+    void stopServer() {
+        if (server != null) {
+            server.stop(0);
+        }
+    }
+
+    @Test
+    void detectReturnsNotRunningWhenEndpointUnreachable() {
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.ollama).withUrl("http://127.0.0.1:1";);
+
+        OllamaDoctorSupport.Status status = OllamaDoctorSupport.detect(client);
+
+        assertThat(status.running()).isFalse();
+        assertThat(status.baseUrl()).isNull();
+        assertThat(status.models()).isEmpty();
+    }
+
+    @Test
+    void detectUsesLlmClientProbeLogic() throws IOException {
+        String baseUrl = 
startOllamaServer("{\"models\":[{\"name\":\"qwen2.5:32b\"}]}");
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.ollama).withUrl(baseUrl);
+
+        assertThat(client.detectEndpoint()).isTrue();
+        assertThat(client.listModels()).containsExactly("qwen2.5:32b");
+    }
+
+    @Test
+    void detectListsModelsFromRunningOllama() throws IOException {
+        String baseUrl = 
startOllamaServer("{\"models\":[{\"name\":\"qwen2.5:32b\"},{\"name\":\"llama3.2:latest\"}]}");
+
+        OllamaDoctorSupport.Status status = detectAt(baseUrl);
+
+        assertThat(status.running()).isTrue();
+        assertThat(status.baseUrl()).isEqualTo(baseUrl);
+        assertThat(status.models()).containsExactly("qwen2.5:32b", 
"llama3.2:latest");
+    }
+
+    @Test
+    void detectReportsRunningWhenNoModelsPulled() throws IOException {
+        String baseUrl = startOllamaServer("{\"models\":[]}");
+
+        OllamaDoctorSupport.Status status = detectAt(baseUrl);
+
+        assertThat(status.running()).isTrue();
+        assertThat(status.models()).isEmpty();
+        
assertThat(OllamaDoctorSupport.formatModels(status.models())).isEqualTo("no 
models pulled");
+    }
+
+    @Test
+    void cliRunningLineMatchesDoctorFormat() throws IOException {
+        String baseUrl = 
startOllamaServer("{\"models\":[{\"name\":\"qwen2.5:32b\"},{\"name\":\"llama3.2:latest\"}]}");
+        OllamaDoctorSupport.Status status = detectAt(baseUrl);
+
+        assertThat(OllamaDoctorSupport.cliRunningLine(status))
+                .isEqualTo("Running at 127.0.0.1:" + displayPort(baseUrl)
+                           + " — models: qwen2.5:32b, llama3.2:latest");
+    }
+
+    @Test
+    void formatDisplayHostStripsSchemeAndTrailingSlash() {
+        
assertThat(OllamaDoctorSupport.formatDisplayHost("http://localhost:11434/";))
+                .isEqualTo("localhost:11434");
+        
assertThat(OllamaDoctorSupport.formatDisplayHost("https://127.0.0.1:11434";))
+                .isEqualTo("127.0.0.1:11434");
+    }
+
+    @Test
+    void tuiRunningSummaryIncludesModelCount() {
+        OllamaDoctorSupport.Status status
+                = new OllamaDoctorSupport.Status(true, 
"http://localhost:11434";, List.of("a", "b"));
+
+        assertThat(OllamaDoctorSupport.tuiRunningSummary(status, 30))
+                .isEqualTo("localhost:11434 (2 models)");
+        
assertThat(OllamaDoctorSupport.modelCountLabel(List.of("one"))).isEqualTo("1 
model");
+    }
+
+    // --- isSmallModel ---
+
+    @Test
+    void isSmallModelReturnsTrueForSmallTag() {
+        assertThat(OllamaDoctorSupport.isSmallModel("qwen2.5:7b")).isTrue();
+        assertThat(OllamaDoctorSupport.isSmallModel("gemma3:1b-it")).isTrue();
+    }
+
+    @Test
+    void isSmallModelReturnsFalseForLargeTag() {
+        assertThat(OllamaDoctorSupport.isSmallModel("qwen2.5:32b")).isFalse();
+        assertThat(OllamaDoctorSupport.isSmallModel("llama3.1:70b")).isFalse();
+    }
+
+    @Test
+    void isSmallModelReturnsFalseForNonNumericTag() {
+        
assertThat(OllamaDoctorSupport.isSmallModel("llama3.2:latest")).isFalse();
+        
assertThat(OllamaDoctorSupport.isSmallModel("phi4-mini:latest")).isFalse();
+    }
+
+    @Test
+    void isSmallModelReturnsFalseForModelWithoutColon() {
+        
assertThat(OllamaDoctorSupport.isSmallModel("model-without-colon")).isFalse();
+    }
+
+    // --- allModelsSmall ---
+
+    @Test
+    void allModelsSmallReturnsTrueWhenAllTagsAreSmall() {
+        assertThat(OllamaDoctorSupport.allModelsSmall(List.of("qwen2.5:7b", 
"gemma3:1b-it"))).isTrue();
+    }
+
+    @Test
+    void allModelsSmallReturnsFalseWhenAnyTagIsLarge() {
+        assertThat(OllamaDoctorSupport.allModelsSmall(List.of("qwen2.5:7b", 
"qwen2.5:32b"))).isFalse();
+    }
+
+    @Test
+    void allModelsSmallReturnsTrueForUnknownSizeTag() {
+        // llama3.2:latest is 3B but has no numeric size tag — treated as "not 
known to be large"
+        
assertThat(OllamaDoctorSupport.allModelsSmall(List.of("llama3.2:latest"))).isTrue();
+    }
+
+    @Test
+    void allModelsSmallReturnsFalseForEmptyList() {
+        assertThat(OllamaDoctorSupport.allModelsSmall(List.of())).isFalse();
+        assertThat(OllamaDoctorSupport.allModelsSmall(null)).isFalse();
+    }
+
+    private OllamaDoctorSupport.Status detectAt(String baseUrl) {
+        LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.ollama).withUrl(baseUrl);
+        assertThat(client.detectEndpoint()).isTrue();
+        return new OllamaDoctorSupport.Status(true, client.endpointUrl(), 
client.listModels());
+    }
+
+    private String startOllamaServer(String apiTagsBody) throws IOException {
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext("/", exchange -> {
+            String path = exchange.getRequestURI().getPath();
+            String body;
+            int status;
+            if ("/api/tags".equals(path)) {
+                body = apiTagsBody;
+                status = 200;
+            } else if ("/".equals(path)) {
+                body = "Ollama is running";
+                status = 200;
+            } else {
+                body = "";
+                status = 404;
+            }
+            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+            exchange.sendResponseHeaders(status, bytes.length);
+            try (OutputStream os = exchange.getResponseBody()) {
+                os.write(bytes);
+            }
+        });
+        server.start();
+        return "http://127.0.0.1:"; + server.getAddress().getPort();
+    }
+
+    private static int displayPort(String baseUrl) {
+        return Integer.parseInt(baseUrl.substring(baseUrl.lastIndexOf(':') + 
1));
+    }
+}
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..f5f00b2da491 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
@@ -39,6 +39,7 @@ import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.OllamaDoctorSupport;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.tooling.maven.MavenDownloaderImpl;
 import org.apache.camel.tooling.maven.MavenResolutionException;
@@ -70,9 +71,11 @@ class DoctorPopup {
         checkJBang(lines);
         checkMavenRepository(lines);
         checkContainerRuntime(lines);
+        OllamaDoctorSupport.Status ollamaStatus = OllamaDoctorSupport.detect();
+        checkOllama(lines, ollamaStatus);
         checkCommonPorts(lines);
         checkDiskSpace(lines);
-        checkAiProvider(lines);
+        checkAiProvider(lines, ollamaStatus);
         checkMcpConnection(lines);
         visible = true;
     }
@@ -282,29 +285,28 @@ class DoctorPopup {
         return v != null && !v.isBlank();
     }
 
-    private void checkAiProvider(List<Line> result) {
-        String provider = null;
-        if (envSet("ANTHROPIC_API_KEY")) {
-            provider = "Anthropic";
-        } else if (envSet("CLOUD_ML_REGION") && 
envSet("ANTHROPIC_VERTEX_PROJECT_ID")) {
-            provider = "Vertex AI";
-        } else if (envSet("AZURE_OPENAI_API_KEY") && 
envSet("AZURE_OPENAI_ENDPOINT")) {
-            provider = "Azure OpenAI";
-        } else if (envSet("GEMINI_API_KEY")) {
-            provider = "Gemini";
-        } else if (envSet("OPENAI_API_KEY")) {
-            provider = "OpenAI";
-        } else if (envSet("WATSONX_APIKEY")) {
-            provider = "watsonx.ai";
-        } else if (envSet("LLM_API_KEY")) {
-            provider = "Custom (LLM_API_KEY)";
+    private void checkAiProvider(List<Line> result, OllamaDoctorSupport.Status 
ollamaStatus) {
+        addAiProviderLines(result, resolveCloudAiProvider(), ollamaStatus);
+    }
+
+    static void addAiProviderLines(List<Line> result, String cloudProvider, 
OllamaDoctorSupport.Status ollama) {
+        String provider = cloudProvider;
+        boolean ollamaReady = ollama.running() && ollama.models() != null && 
!ollama.models().isEmpty();
+        if (provider == null && ollama.running()) {
+            provider = ollamaReady ? "Ollama (local)" : "Ollama (no models)";
         }
         if (provider != null) {
+            String emoji = (cloudProvider != null || ollamaReady) ? 
TuiIcons.OK : TuiIcons.WARN;
             result.add(Line.from(
                     Span.raw(TuiIcons.indent(TuiIcons.MCP)),
                     Span.styled(String.format("%-14s", "AI"), Theme.muted()),
                     Span.raw(String.format("%-30s", provider)),
-                    Span.raw(" " + TuiIcons.OK)));
+                    Span.raw(" " + emoji)));
+            if (ollama.running() && !ollamaReady && cloudProvider == null) {
+                result.add(Line.from(Span.styled(
+                        "                    Run: ollama pull <model> for 
local AI (F8)",
+                        Style.EMPTY.dim())));
+            }
         } else {
             result.add(Line.from(
                     Span.raw(TuiIcons.indent(TuiIcons.MCP)),
@@ -312,7 +314,67 @@ class DoctorPopup {
                     Span.raw(String.format("%-30s", "No API key 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, 
AZURE_OPENAI_*, GEMINI_API_KEY, OPENAI_API_KEY,"
+                                             + " WATSONX_APIKEY, or start 
Ollama",
+                    Style.EMPTY.dim())));
+        }
+    }
+
+    static String resolveCloudAiProvider() {
+        if (envSet("ANTHROPIC_API_KEY")) {
+            return "Anthropic";
+        }
+        if (envSet("CLOUD_ML_REGION") && 
envSet("ANTHROPIC_VERTEX_PROJECT_ID")) {
+            return "Vertex AI";
+        }
+        if (envSet("AZURE_OPENAI_API_KEY") && envSet("AZURE_OPENAI_ENDPOINT")) 
{
+            return "Azure OpenAI";
+        }
+        if (envSet("GEMINI_API_KEY")) {
+            return "Gemini";
+        }
+        if (envSet("OPENAI_API_KEY")) {
+            return "OpenAI";
+        }
+        if (envSet("WATSONX_APIKEY")) {
+            return "watsonx.ai";
+        }
+        if (envSet("LLM_API_KEY")) {
+            return "Custom (LLM_API_KEY)";
+        }
+        return null;
+    }
+
+    private void checkOllama(List<Line> result, OllamaDoctorSupport.Status 
status) {
+        addOllamaLines(result, status);
+    }
+
+    static void addOllamaLines(List<Line> result, OllamaDoctorSupport.Status 
status) {
+        if (status.running()) {
+            boolean allSmall = 
OllamaDoctorSupport.allModelsSmall(status.models());
+            String icon = (status.models().isEmpty() || 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", 
OllamaDoctorSupport.tuiRunningSummary(status, 30))),
+                    Span.raw(" " + icon)));
+            String models = OllamaDoctorSupport.formatModels(status.models());
+            result.add(Line.from(Span.styled(
+                    "                    models: " + 
TuiHelper.truncate(models, 34),
+                    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 detected 
(optional)")),
+                    Span.raw(" " + TuiIcons.WARN)));
+            result.add(Line.from(Span.styled(
+                    "                    " + TuiHelper.truncate("Start Ollama 
(ollama serve) for local AI", 40),
                     Style.EMPTY.dim())));
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
new file mode 100644
index 000000000000..dbc20444463b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DoctorPopupOllamaTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.text.Line;
+import org.apache.camel.dsl.jbang.core.common.OllamaDoctorSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DoctorPopupOllamaTest {
+
+    @Test
+    void aiProviderRecognizesRunningOllamaWithoutCloudKeys() {
+        List<Line> lines = new ArrayList<>();
+        OllamaDoctorSupport.Status ollama
+                = new OllamaDoctorSupport.Status(true, 
"http://localhost:11434";, List.of("qwen2.5:32b"));
+
+        DoctorPopup.addAiProviderLines(lines, null, ollama);
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("Ollama 
(local)"));
+    }
+
+    @Test
+    void aiProviderPrefersCloudKeyOverOllama() {
+        List<Line> lines = new ArrayList<>();
+        OllamaDoctorSupport.Status ollama
+                = new OllamaDoctorSupport.Status(true, 
"http://localhost:11434";, List.of("llama3.2:latest"));
+
+        DoctorPopup.addAiProviderLines(lines, "OpenAI", ollama);
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("OpenAI"));
+        assertThat(lineTexts(lines)).noneMatch(text -> text.contains("Ollama 
(local)"));
+    }
+
+    @Test
+    void aiProviderWarnsWhenNoCloudKeyAndOllamaDown() {
+        List<Line> lines = new ArrayList<>();
+
+        DoctorPopup.addAiProviderLines(lines, null, 
OllamaDoctorSupport.Status.notRunning());
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("No API 
key configured"));
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("start 
Ollama"));
+    }
+
+    @Test
+    void ollamaRowShowsModelsWhenRunning() {
+        List<Line> lines = new ArrayList<>();
+        OllamaDoctorSupport.Status status = new OllamaDoctorSupport.Status(
+                true, "http://localhost:11434";, List.of("qwen2.5:32b", 
"llama3.2:latest"));
+
+        DoctorPopup.addOllamaLines(lines, status);
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("2 
models"));
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("models: 
qwen2.5:32b, llama3.2:latest"));
+    }
+
+    @Test
+    void aiProviderWarnsWhenOllamaRunningWithoutModels() {
+        List<Line> lines = new ArrayList<>();
+        OllamaDoctorSupport.Status ollama = new 
OllamaDoctorSupport.Status(true, "http://localhost:11434";, List.of());
+
+        DoctorPopup.addAiProviderLines(lines, null, ollama);
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("Ollama 
(no models)"));
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("ollama 
pull"));
+    }
+
+    @Test
+    void ollamaRowTruncatesLongModelList() {
+        List<Line> lines = new ArrayList<>();
+        OllamaDoctorSupport.Status status = new OllamaDoctorSupport.Status(
+                true,
+                "http://localhost:11434";,
+                List.of("model-one-with-a-long-name", 
"model-two-with-a-long-name", "model-three"));
+
+        DoctorPopup.addOllamaLines(lines, status);
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("models:") 
&& text.contains("..."));
+    }
+
+    @Test
+    void ollamaRowWarnsWhenNotRunning() {
+        List<Line> lines = new ArrayList<>();
+
+        DoctorPopup.addOllamaLines(lines, 
OllamaDoctorSupport.Status.notRunning());
+
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("Not 
detected"));
+        assertThat(lineTexts(lines)).anyMatch(text -> text.contains("ollama 
serve"));
+    }
+
+    private static List<String> lineTexts(List<Line> lines) {
+        return lines.stream().map(Object::toString).toList();
+    }
+}
diff --git a/etc/scripts/regen.sh b/etc/scripts/regen.sh
index 6e99507207fd..f6e417efca2e 100755
--- a/etc/scripts/regen.sh
+++ b/etc/scripts/regen.sh
@@ -25,8 +25,8 @@ cd `dirname "$0"`/../..
 git clean -fd
 rm -Rf **/src/generated/
 
-# Regenerate everything
-if ./mvnw -T1C --batch-mode -Pregen -DskipTests ${MAVEN_EXTRA_ARGS} install > 
build-regen.log 2>&1; then
+# Regenerate everything (-DskipTests skips Surefire; -DskipITs skips Failsafe 
ITs)
+if ./mvnw -T1C --batch-mode -Pregen -DskipTests -DskipITs ${MAVEN_EXTRA_ARGS} 
install > build-regen.log 2>&1; then
   echo "✅ mvn -Pregen succeeded."
 else
   echo "❌ mvn -Pregen failed. Last 50 lines of build-regen.log:"

Reply via email to