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

davsclaus pushed a commit to branch fix/CAMEL-23688-llm-url
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c3d56648335370081c70e4e586eb6828d86f5efb
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 08:06:21 2026 +0200

    CAMEL-23688: camel-jbang - An LLM given with --url is the only one used
    
    When the LLM given with --url did not answer, LlmClient.detectEndpoint
    went on to detect another provider: the Ollama of camel infra or of the
    host, or a hosted provider with an API key in the environment, and sent
    the prompts there. Now an explicit URL is the only endpoint the client
    tries, so camel ask, camel explain and camel overview --ai fail, and the
    AI panel of the TUI says which configured URL does not answer. Without a
    URL the provider is detected as before (camel doctor and the Ollama tab
    of the TUI probe without one).
    
    The tests no longer depend on whether the host runs Ollama: an Ollama of
    camel infra (a pid file and a fake server) is always there for the
    fallback to find.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  7 ++
 .../ROOT/pages/camel-jbang-ai-providers.adoc       |  4 +
 .../camel/dsl/jbang/core/commands/LlmClient.java   |  6 +-
 .../core/commands/LlmClientExplicitUrlTest.java    | 98 ++++++++++++++++++++++
 .../dsl/jbang/core/commands/OverviewTest.java      |  4 +-
 .../jbang/core/common/OllamaDoctorSupportTest.java | 25 ++----
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  7 +-
 7 files changed, 126 insertions(+), 25 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 81cf732576ea..28697555d493 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -521,6 +521,13 @@ or follows, like Enter does.
 * `camel ask` with a question prints only the answer on stdout. The model it 
uses, the integration it asks about, the
   note about a small model, and the output of `--show-tools` and 
`--show-stats` go to stderr.
 
+=== camel-jbang - an LLM given with --url is the only one used
+
+`camel ask`, `camel explain` and `camel overview --ai` fail when the LLM given 
with `--url` does not answer: they went
+on to another provider, such as the Ollama of the host, or a hosted provider 
with an API key in the environment, and
+sent the prompts there. The AI panel of the Camel TUI does the same with the 
URL of its settings. Without `--url` the
+provider is detected as before.
+
 === camel-jbang - exit codes and errors of more commands
 
 * `camel export` does not clean the export directory when it is the current 
directory, or one that contains it, unless
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai-providers.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai-providers.adoc
index 945508eb7c9e..52a2eb131920 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai-providers.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai-providers.adoc
@@ -30,6 +30,10 @@ camel ask "check health" --api-type=gemini 
--model=gemini-2.0-flash
 camel ask "check health" --api-type=ollama --model=llama3.1
 ----
 
+With `--url`, the CLI uses that endpoint and no other: when it does not 
answer, the command fails instead of going
+on to another provider, so the prompts never reach a provider you did not 
choose. The same holds for the URL set in
+the settings of the Camel TUI.
+
 NOTE: `camel harden` only detects Ollama (from `camel infra` or at 
`localhost:11434`).
 For another provider, give it the endpoint: `--url=https://api.openai.com 
--api-type=openai --api-key=sk-...`
 (any OpenAI-compatible API).
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 e83e8d02a1e6..2f8d4206f88b 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
@@ -342,8 +342,10 @@ public class LlmClient {
 
     public boolean detectEndpoint() {
         boolean found;
-        if (tryExplicitUrl()) {
-            found = true;
+        if (url != null && !url.isBlank()) {
+            // the endpoint the user named, and only that one: never another 
provider, which may be a hosted one the
+            // user did not choose to send the prompts to
+            found = tryExplicitUrl();
         } else if (apiType != null) {
             found = switch (apiType) {
                 case anthropic -> tryAnthropicOrVertex();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientExplicitUrlTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientExplicitUrlTest.java
new file mode 100644
index 000000000000..6f12727ddd23
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientExplicitUrlTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.commands.LlmClient.ApiType;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * An endpoint the user names with --url is the only one the client uses: when 
it does not answer, the client does not
+ * go on to another provider. Here there is always another one to find, an 
Ollama started with camel infra run.
+ */
+class LlmClientExplicitUrlTest {
+
+    private static final String NOTHING_THERE = "http://127.0.0.1:1";;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private FakeLlmServer infraOllama;
+
+    @BeforeEach
+    void start() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+        // what camel infra run ollama leaves for the CLI to find
+        infraOllama = new FakeLlmServer("unused");
+        
Files.writeString(CommandLineHelper.getCamelDir().resolve("infra-ollama-4711.json"),
+                "{\"baseUrl\":\"" + infraOllama.url() + "\"}");
+    }
+
+    @AfterEach
+    void stop() {
+        infraOllama.close();
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void withoutAUrlTheOllamaOfCamelInfraIsFound() {
+        LlmClient client = LlmClient.create().withApiType(ApiType.ollama);
+
+        assertThat(client.detectEndpoint()).isTrue();
+        assertThat(client.endpointUrl()).isEqualTo(infraOllama.url());
+    }
+
+    @Test
+    void anUnreachableUrlOfAnApiType() {
+        LlmClient client = 
LlmClient.create().withApiType(ApiType.ollama).withUrl(NOTHING_THERE);
+
+        assertThat(client.detectEndpoint()).isFalse();
+        // not the url of another Ollama
+        assertThat(client.endpointUrl()).isEqualTo(NOTHING_THERE);
+    }
+
+    @Test
+    void anUnreachableUrlWithoutAnApiType() {
+        LlmClient client = LlmClient.create().withUrl(NOTHING_THERE);
+
+        assertThat(client.detectEndpoint()).isFalse();
+        assertThat(client.endpointUrl()).isEqualTo(NOTHING_THERE);
+        assertThat(client.apiType()).isNull();
+    }
+
+    @Test
+    void aUrlThatAnswersIsUsed() throws Exception {
+        try (FakeLlmServer server = new FakeLlmServer("unused")) {
+            LlmClient client = LlmClient.create().withUrl(server.url());
+
+            assertThat(client.detectEndpoint()).isTrue();
+            assertThat(client.endpointUrl()).isEqualTo(server.url());
+            assertThat(client.apiType()).isEqualTo(ApiType.ollama);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
index 823e90ca27bf..0d7226d8134b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
@@ -41,7 +41,6 @@ import picocli.CommandLine;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
 
 /**
  * camel overview: the overview of the routes of a project, the summary file, 
and what an LLM adds to it (a local server
@@ -181,8 +180,7 @@ class OverviewTest extends CamelCommandBaseTestSupport {
 
     @Test
     void aiWithoutAnLlmIsAFailure() {
-        // an --url that does not answer is not the end of it: the client goes 
on to the Ollama of the host (CAMEL-24772)
-        
assumeFalse(LlmClient.create().withApiType(LlmClient.ApiType.ollama).detectEndpoint(),
 "the host runs Ollama");
+        // an --url that does not answer is a failure: the client does not go 
on to another provider
         Overview command = command("--ai", "--api-type=ollama", 
"--url=http://127.0.0.1:1";);
 
         assertThatThrownBy(command::doCall)
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
index 5041d7e854f7..f9832e580ae3 100644
--- 
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
@@ -41,29 +41,18 @@ class OllamaDoctorSupportTest {
     }
 
     /**
-     * {@code detect} is a discovery: an explicit URL that does not answer is 
not the end of it, the client goes on to
-     * the {@code camel infra run ollama} PID files and the default {@code 
localhost:11434}, which is what a doctor
-     * probe is for. So the outcome for an unreachable URL depends on the 
host: nothing found when the host has no
-     * Ollama, the host's own Ollama otherwise, and never the unreachable URL 
itself. Asserting only "not running"
-     * failed on every machine with Ollama running (CAMEL-24772).
+     * An explicit URL that does not answer is not running, also on a host 
that runs Ollama: the client does not go on
+     * to the {@code camel infra run ollama} PID files or the default {@code 
localhost:11434}, which only a probe
+     * without a URL looks for.
      */
     @Test
-    void anUnreachableUrlFallsBackToDiscoveryOfTheHostsOwnOllama() {
-        LlmClient discovery = 
LlmClient.create().withApiType(LlmClient.ApiType.ollama);
-        boolean hostHasOllama = discovery.detectEndpoint();
-
+    void anUnreachableUrlIsNotRunning() {
         LlmClient client = 
LlmClient.create().withApiType(LlmClient.ApiType.ollama).withUrl("http://127.0.0.1:1";);
         OllamaDoctorSupport.Status status = OllamaDoctorSupport.detect(client);
 
-        if (hostHasOllama) {
-            assertThat(status.running()).isTrue();
-            
assertThat(status.baseUrl()).isEqualTo(discovery.endpointUrl()).isNotEqualTo("http://127.0.0.1:1";);
-            assertThat(status.models()).isNotNull();
-        } else {
-            assertThat(status.running()).isFalse();
-            assertThat(status.baseUrl()).isNull();
-            assertThat(status.models()).isEmpty();
-        }
+        assertThat(status.running()).isFalse();
+        assertThat(status.baseUrl()).isNull();
+        assertThat(status.models()).isEmpty();
     }
 
     @Test
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 3c3400e18c5d..823241088841 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
@@ -644,9 +644,12 @@ class AiPanel {
                 
toolSet.setMode(normalizeToolMode(TuiSettings.load().getAiTools()));
             }
             client = created;
+            // a url that is set is the only one tried, so say which one does 
not answer
+            String configuredUrl = client.endpointUrl();
             if (!client.detectEndpoint()) {
-                initError
-                        = "No LLM service reachable. Set ANTHROPIC_API_KEY, 
AZURE_OPENAI_*, GEMINI_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or start 
Ollama.";
+                initError = configuredUrl != null && !configuredUrl.isBlank()
+                        ? "No LLM service reachable at " + configuredUrl + " 
(the URL of the AI settings)."
+                        : "No LLM service reachable. Set ANTHROPIC_API_KEY, 
AZURE_OPENAI_*, GEMINI_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or start 
Ollama.";
                 client = null;
                 return;
             }

Reply via email to