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

gnodet 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 3aabdf62e021 CAMEL-23728: camel-jbang - Fix Vertex AI model mapping 
and error handling (#23910)
3aabdf62e021 is described below

commit 3aabdf62e021279befcc5b768625642675d11179
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jun 10 16:44:17 2026 +0200

    CAMEL-23728: camel-jbang - Fix Vertex AI model mapping and error handling 
(#23910)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../camel/dsl/jbang/core/commands/LlmClient.java   | 61 ++++++++++++++--
 .../jbang/core/commands/LlmClientVertexTest.java   | 84 ++++++++++++++++++++++
 2 files changed, 140 insertions(+), 5 deletions(-)

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 85e282924048..ac87f338677c 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
@@ -30,6 +30,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -51,6 +52,12 @@ class LlmClient {
     private static final int CONNECT_TIMEOUT_SECONDS = 10;
     private static final int HEALTH_CHECK_TIMEOUT_SECONDS = 5;
 
+    // Pre-4.6 Claude models require a @date suffix on Vertex AI
+    private static final Map<String, String> VERTEX_MODEL_MAP = Map.of(
+            "claude-sonnet-4-5", "claude-sonnet-4-5@20250929",
+            "claude-opus-4-5", "claude-opus-4-5@20251101",
+            "claude-haiku-4-5", "claude-haiku-4-5@20251001");
+
     enum ApiType {
         ollama,
         openai,
@@ -363,7 +370,8 @@ class LlmClient {
                     builder.build(), HttpResponse.BodyHandlers.ofLines());
 
             if (response.statusCode() != 200) {
-                handleErrorStatus(response.statusCode(), "Streaming request 
failed");
+                String errorBody = 
response.body().collect(Collectors.joining("\n"));
+                handleErrorStatus(response.statusCode(), errorBody);
                 return new ChatResponse(null, List.of(), "error", false);
             }
 
@@ -609,9 +617,10 @@ class LlmClient {
 
     private String resolveAnthropicUrl() {
         if (isVertexAi()) {
+            String vertexModel = resolveVertexModel(model);
             return String.format(
                     
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:rawPredict";,
-                    vertexRegion, vertexProjectId, vertexRegion, model);
+                    vertexRegion, vertexProjectId, vertexRegion, vertexModel);
         }
         String base = url != null ? url : DEFAULT_ANTHROPIC_URL;
         if (base.endsWith("/")) {
@@ -642,6 +651,13 @@ class LlmClient {
         return vertexRegion != null && vertexProjectId != null;
     }
 
+    static String resolveVertexModel(String model) {
+        if (model == null || model.contains("@")) {
+            return model;
+        }
+        return VERTEX_MODEL_MAP.getOrDefault(model, model);
+    }
+
     private String getGcloudAccessToken() {
         try {
             ProcessBuilder pb = new ProcessBuilder("gcloud", "auth", 
"print-access-token");
@@ -925,7 +941,8 @@ class LlmClient {
                     builder.build(), HttpResponse.BodyHandlers.ofLines());
 
             if (response.statusCode() != 200) {
-                handleErrorStatus(response.statusCode(), "Streaming request 
failed");
+                String errorBody = 
response.body().collect(Collectors.joining("\n"));
+                handleErrorStatus(response.statusCode(), errorBody);
                 return null;
             }
 
@@ -972,7 +989,8 @@ class LlmClient {
                     builder.build(), HttpResponse.BodyHandlers.ofLines());
 
             if (response.statusCode() != 200) {
-                handleErrorStatus(response.statusCode(), "Streaming request 
failed");
+                String errorBody = 
response.body().collect(Collectors.joining("\n"));
+                handleErrorStatus(response.statusCode(), errorBody);
                 return null;
             }
 
@@ -1211,15 +1229,48 @@ class LlmClient {
         printer.println("LLM returned status: " + statusCode);
         switch (statusCode) {
             case 401 -> printer.println("Authentication failed. Check your API 
key.");
+            case 404 -> {
+                if (isVertexAi()) {
+                    printer.println("Model not found. Check that the model 
identifier is valid for Vertex AI.");
+                } else {
+                    printer.println("Endpoint not found.");
+                }
+            }
             case 429 -> printer.println("Rate limit exceeded.");
             default -> {
             }
         }
         if (body != null && !body.isBlank()) {
-            printer.println(body);
+            String errorMessage = extractErrorMessage(body);
+            if (errorMessage != null) {
+                printer.println(errorMessage);
+            }
         }
     }
 
+    static String extractErrorMessage(String body) {
+        String trimmed = body.strip();
+        if (trimmed.startsWith("{")) {
+            try {
+                JsonObject json = (JsonObject) Jsoner.deserialize(trimmed);
+                Object error = json.get("error");
+                if (error instanceof JsonObject) {
+                    String message = ((JsonObject) error).getString("message");
+                    if (message != null && !message.isBlank()) {
+                        return message;
+                    }
+                } else if (error instanceof String && !((String) 
error).isBlank()) {
+                    return (String) error;
+                }
+                return trimmed;
+            } catch (Exception e) {
+                return trimmed;
+            }
+        }
+        // Non-JSON response (e.g., HTML error page) — don't dump it
+        return null;
+    }
+
     // ---- OpenAI message helpers ----
 
     static JsonObject createOpenAiMessage(String role, String content) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientVertexTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientVertexTest.java
new file mode 100644
index 000000000000..dfa76a5cf343
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/LlmClientVertexTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class LlmClientVertexTest {
+
+    // ---- resolveVertexModel tests ----
+
+    @Test
+    void mapsPreFourSixModels() {
+        assertEquals("claude-sonnet-4-5@20250929", 
LlmClient.resolveVertexModel("claude-sonnet-4-5"));
+        assertEquals("claude-haiku-4-5@20251001", 
LlmClient.resolveVertexModel("claude-haiku-4-5"));
+        assertEquals("claude-opus-4-5@20251101", 
LlmClient.resolveVertexModel("claude-opus-4-5"));
+    }
+
+    @Test
+    void passesThroughDatelessModels() {
+        assertEquals("claude-sonnet-4-6", 
LlmClient.resolveVertexModel("claude-sonnet-4-6"));
+        assertEquals("claude-opus-4-6", 
LlmClient.resolveVertexModel("claude-opus-4-6"));
+        assertEquals("claude-opus-4-7", 
LlmClient.resolveVertexModel("claude-opus-4-7"));
+    }
+
+    @Test
+    void passesThroughAlreadyVersionedModels() {
+        assertEquals("claude-haiku-4-5@20251001", 
LlmClient.resolveVertexModel("claude-haiku-4-5@20251001"));
+        assertEquals("claude-sonnet-4-5@20250929", 
LlmClient.resolveVertexModel("claude-sonnet-4-5@20250929"));
+    }
+
+    @Test
+    void handlesNullModel() {
+        assertNull(LlmClient.resolveVertexModel(null));
+    }
+
+    // ---- extractErrorMessage tests ----
+
+    @Test
+    void extractsJsonErrorMessage() {
+        String json = "{\"error\":{\"message\":\"Model not 
found\",\"type\":\"not_found_error\"}}";
+        assertEquals("Model not found", LlmClient.extractErrorMessage(json));
+    }
+
+    @Test
+    void extractsJsonStringError() {
+        String json = "{\"error\":\"Something went wrong\"}";
+        assertEquals("Something went wrong", 
LlmClient.extractErrorMessage(json));
+    }
+
+    @Test
+    void returnsNullForHtmlResponse() {
+        String html = "<!DOCTYPE html><html><body><p>404 Not 
Found</p></body></html>";
+        assertNull(LlmClient.extractErrorMessage(html));
+    }
+
+    @Test
+    void returnsNullForHtmlWithWhitespace() {
+        String html = "\n<!DOCTYPE html>\n<html 
lang=en>\n<p>404.</p>\n</html>\n";
+        assertNull(LlmClient.extractErrorMessage(html));
+    }
+
+    @Test
+    void returnsJsonBodyWhenNoErrorField() {
+        String json = "{\"status\":\"error\",\"code\":404}";
+        assertEquals(json, LlmClient.extractErrorMessage(json));
+    }
+}

Reply via email to