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 a2ab828025e8 CAMEL-24345: camel-google-vertexai - apply the
streamOutputMode and jsonMode options
a2ab828025e8 is described below
commit a2ab828025e8e94a8fcf27364a9be1cc58faeb75
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 6 13:42:14 2026 +0200
CAMEL-24345: camel-google-vertexai - apply the streamOutputMode and
jsonMode options
Both options were declared and documented but never read.
streamOutputMode=chunks
now produces a List<String> with one element per streamed chunk instead of
the
concatenated text; the default complete is unchanged. jsonMode=true sets the
response MIME type to application/json. Also documents generateChat and
generateCode as aliases of generateText, and fixes a NullPointerException in
buildRawPredictRequestBody when no body is set.
Closes #25362
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../camel-google/camel-google-vertexai/pom.xml | 5 ++
.../google/vertexai/GoogleVertexAIOperations.java | 6 +-
.../google/vertexai/GoogleVertexAIProducer.java | 34 ++++++---
.../GoogleVertexAIProducerOptionsTest.java | 81 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 12 ++++
5 files changed, 128 insertions(+), 10 deletions(-)
diff --git a/components/camel-google/camel-google-vertexai/pom.xml
b/components/camel-google/camel-google-vertexai/pom.xml
index a5ef89f507c8..d24191d022b4 100644
--- a/components/camel-google/camel-google-vertexai/pom.xml
+++ b/components/camel-google/camel-google-vertexai/pom.xml
@@ -103,5 +103,10 @@
<artifactId>camel-test-junit6</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
index 3644c852e615..429d3d791d20 100644
---
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
+++
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIOperations.java
@@ -30,7 +30,8 @@ public enum GoogleVertexAIOperations {
generateText,
/**
- * Generate chat response using Gemini models with conversation history.
+ * Generate a chat response using Gemini models. Alias of {@link
#generateText}: the request is built from the same
+ * prompt and configuration, the operation name only documents the intent
of the route.
*/
generateChat,
@@ -50,7 +51,8 @@ public enum GoogleVertexAIOperations {
generateEmbeddings,
/**
- * Generate code using Gemini or code-specialized models.
+ * Generate code using Gemini or code-specialized models. Alias of {@link
#generateText}: the model is selected with
+ * the modelId option, the operation name only documents the intent of the
route.
*/
generateCode,
diff --git
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
index 25fabca07a4d..5416933feceb 100644
---
a/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
+++
b/components/camel-google/camel-google-vertexai/src/main/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducer.java
@@ -51,6 +51,8 @@ public class GoogleVertexAIProducer extends DefaultProducer {
private static final Logger LOG =
LoggerFactory.getLogger(GoogleVertexAIProducer.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final String CHUNKS_OUTPUT_MODE = "chunks";
+ private static final String JSON_MIME_TYPE = "application/json";
private final GoogleVertexAIEndpoint endpoint;
@@ -154,22 +156,25 @@ public class GoogleVertexAIProducer extends
DefaultProducer {
try (ResponseStream<GenerateContentResponse> stream
= client.models.generateContentStream(modelId, prompt,
config)) {
- StringBuilder fullText = new StringBuilder();
- int chunkCount = 0;
+ List<String> chunks = new ArrayList<>();
GenerateContentResponse lastResponse = null;
for (GenerateContentResponse chunk : stream) {
- chunkCount++;
lastResponse = chunk;
String chunkText = chunk.text();
if (chunkText != null) {
- fullText.append(chunkText);
+ chunks.add(chunkText);
}
}
Message message = getMessageForResponse(exchange);
- message.setBody(fullText.toString());
- message.setHeader(GoogleVertexAIConstants.CHUNK_COUNT, chunkCount);
+ if
(CHUNKS_OUTPUT_MODE.equalsIgnoreCase(determineStreamOutputMode(exchange))) {
+ // one element per chunk, so the route can split them
+ message.setBody(chunks);
+ } else {
+ message.setBody(String.join("", chunks));
+ }
+ message.setHeader(GoogleVertexAIConstants.CHUNK_COUNT,
chunks.size());
if (lastResponse != null) {
setMetadataHeaders(exchange, lastResponse);
@@ -177,6 +182,14 @@ public class GoogleVertexAIProducer extends
DefaultProducer {
}
}
+ String determineStreamOutputMode(Exchange exchange) {
+ String mode =
exchange.getIn().getHeader(GoogleVertexAIConstants.STREAM_OUTPUT_MODE,
String.class);
+ if (mode == null) {
+ mode = endpoint.getConfiguration().getStreamOutputMode();
+ }
+ return mode;
+ }
+
private void generateImage(Exchange exchange) throws Exception {
String prompt = getPrompt(exchange);
@@ -505,7 +518,8 @@ public class GoogleVertexAIProducer extends DefaultProducer
{
}
throw new IllegalArgumentException(
- "Request body must be a JSON String, Map, or plain text
prompt. Got: " + body.getClass().getName());
+ "Request body must be a JSON String, Map, or plain text
prompt. Got: "
+ + (body == null ? "no body" :
body.getClass().getName()));
}
/**
@@ -630,7 +644,7 @@ public class GoogleVertexAIProducer extends DefaultProducer
{
return prompt;
}
- private GenerateContentConfig buildConfig(Exchange exchange) {
+ GenerateContentConfig buildConfig(Exchange exchange) {
GoogleVertexAIConfiguration config = endpoint.getConfiguration();
GenerateContentConfig.Builder configBuilder =
GenerateContentConfig.builder();
@@ -676,6 +690,10 @@ public class GoogleVertexAIProducer extends
DefaultProducer {
configBuilder.candidateCount(candidateCount);
}
+ if (config.isJsonMode()) {
+ configBuilder.responseMimeType(JSON_MIME_TYPE);
+ }
+
return configBuilder.build();
}
diff --git
a/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
b/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
new file mode 100644
index 000000000000..e3e7c74de94f
--- /dev/null
+++
b/components/camel-google/camel-google-vertexai/src/test/java/org/apache/camel/component/google/vertexai/GoogleVertexAIProducerOptionsTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.component.google.vertexai;
+
+import com.google.genai.types.GenerateContentConfig;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that the producer applies the options that describe the shape of
the request and of the response.
+ */
+class GoogleVertexAIProducerOptionsTest {
+
+ private static final String BASE =
"google-vertexai:my-project:us-central1:gemini-2.0-flash";
+
+ private DefaultCamelContext context;
+
+ @AfterEach
+ void tearDown() {
+ if (context != null) {
+ context.stop();
+ }
+ }
+
+ private GoogleVertexAIProducer producer(String query) throws Exception {
+ context = new DefaultCamelContext();
+ context.start();
+ GoogleVertexAIEndpoint endpoint = context.getEndpoint(BASE + query,
GoogleVertexAIEndpoint.class);
+ return new GoogleVertexAIProducer(endpoint);
+ }
+
+ @Test
+ void jsonModeAsksTheModelForJson() throws Exception {
+ GenerateContentConfig config =
producer("?jsonMode=true").buildConfig(new DefaultExchange(context));
+
+ assertThat(config.responseMimeType()).contains("application/json");
+ }
+
+ @Test
+ void jsonModeIsOffByDefault() throws Exception {
+ GenerateContentConfig config = producer("").buildConfig(new
DefaultExchange(context));
+
+ assertThat(config.responseMimeType()).isEmpty();
+ }
+
+ @Test
+ void theStreamOutputModeComesFromTheConfigurationOrTheHeader() throws
Exception {
+ GoogleVertexAIProducer producer = producer("?streamOutputMode=chunks");
+
+ Exchange exchange = new DefaultExchange(context);
+
assertThat(producer.determineStreamOutputMode(exchange)).isEqualTo("chunks");
+
+ // the header wins over the endpoint option
+ exchange.getIn().setHeader(GoogleVertexAIConstants.STREAM_OUTPUT_MODE,
"complete");
+
assertThat(producer.determineStreamOutputMode(exchange)).isEqualTo("complete");
+ }
+
+ @Test
+ void theStreamOutputModeDefaultsToComplete() throws Exception {
+ assertThat(producer("").determineStreamOutputMode(new
DefaultExchange(context))).isEqualTo("complete");
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index caed74bf9189..c79eee332eb9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -1722,3 +1722,15 @@ Because the Gmail API does not return the parsed
`payload` for the `RAW` format,
and `...MessageId` headers — those values are part of the RFC 2822 content
that is now in the body.
`CamelGoogleMailStreamId`, `...ThreadId` and `...LabelIds` are still set.
Routes that need the parsed
headers should keep the default `raw=false`.
+
+=== camel-google-vertexai - the streamOutputMode and jsonMode options are now
applied
+
+Both options were declared and documented but never read, so they had no
effect. They now do what
+they say:
+
+* `streamOutputMode=chunks` makes `generateChatStreaming` produce a
`List<String>`, one element per
+ streamed chunk, instead of the concatenated text. The default `complete` is
unchanged, so routes
+ that never set the option keep receiving a `String`. The value can also be
set per message with the
+ `CamelGoogleVertexAIStreamOutputMode` header.
+* `jsonMode=true` sets the response MIME type of the request to
`application/json`, so the model is
+ asked to answer with JSON. The default `false` leaves the request untouched.