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()))); } }
