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