davsclaus commented on code in PR #25116:
URL: https://github.com/apache/camel/pull/25116#discussion_r3653221102
##########
components/camel-ai/camel-openai/src/main/docs/openai-component.adoc:
##########
@@ -38,6 +38,10 @@ openai:operation[?options]
Supported operations:
* `chat-completion` - Generate chat completions using language models
+* `responses` - Call the OpenAI Responses API (hosted tools, server-side
conversation state; non-streaming)
+
+See xref:ROOT:openai-responses.adoc[Responses API operation] for usage
(`previousResponseId`, builtin tools, MCP pass-through).
Review Comment:
This xref uses the `ROOT:` prefix, but existing sub-pages use `others:`.
This is the CI-breaking error (`target of xref not found:
ROOT:openai-responses.adoc`).
```suggestion
See xref:others:openai-responses.adoc[Responses API operation] for usage
(`previousResponseId`, builtin tools, MCP pass-through).
```
Also, the Sub-Pages section at the bottom of this file should include a
corresponding entry for `openai-responses.adoc`, consistent with how
`openai-mcp.adoc`, `openai-providers.adoc`, and `openai-operations.adoc` are
listed there.
##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java:
##########
@@ -93,8 +95,12 @@ public final class OpenAIConstants {
public static final String AGENTIC_TOTAL_TOKENS =
"CamelOpenAIAgenticTotalTokens";
// Output Exchange Properties
- @Metadata(description = "The complete OpenAI response object", javaType =
"com.openai.models.ChatCompletion")
+ @Metadata(description = "The complete OpenAI chat completion response
object",
+ javaType = "com.openai.models.chat.completions.ChatCompletion")
Review Comment:
This changes the `javaType` for the existing `CamelOpenAIResponse` header
from `com.openai.models.ChatCompletion` to
`com.openai.models.chat.completions.ChatCompletion`. This is an OpenAI SDK
package relocation that affects existing `chat-completion` users of
`storeFullResponse=true`.
Please either:
- Add a separate upgrade guide entry documenting this SDK type change, or
- Split this into its own commit so it's clear this is a distinct change
from the `responses` addition
##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesSupport.java:
##########
@@ -0,0 +1,217 @@
+/*
+ * 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.openai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.core.JsonValue;
+import com.openai.models.responses.FileSearchTool;
+import com.openai.models.responses.Response;
+import com.openai.models.responses.ResponseCreateParams;
+import com.openai.models.responses.ResponseFormatTextConfig;
+import com.openai.models.responses.ResponseFormatTextJsonSchemaConfig;
+import com.openai.models.responses.ResponseOutputItem;
+import com.openai.models.responses.ResponseOutputMessage;
+import com.openai.models.responses.ResponseOutputText;
+import com.openai.models.responses.ResponseTextConfig;
+import com.openai.models.responses.ResponseUsage;
+import com.openai.models.responses.Tool;
+import com.openai.models.responses.WebSearchTool;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Helpers for the OpenAI Responses API producer.
+ */
+final class OpenAIResponsesSupport {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private OpenAIResponsesSupport() {
+ }
+
+ static void applyBuiltinTools(
+ ResponseCreateParams.Builder paramsBuilder, String builtinTools,
+ String fileSearchVectorStoreIds) {
+ if (ObjectHelper.isEmpty(builtinTools)) {
+ return;
+ }
+ for (String token : builtinTools.split(",")) {
+ String name = token.trim().toLowerCase(Locale.ROOT);
+ if (name.isEmpty()) {
+ continue;
+ }
+ switch (name) {
+ case "web_search" ->
paramsBuilder.addTool(WebSearchTool.builder()
+ .type(WebSearchTool.Type.WEB_SEARCH)
+ .build());
+ case "file_search" -> {
+ FileSearchTool.Builder fsBuilder = FileSearchTool.builder()
+ .type(JsonValue.from("file_search"));
+ if (ObjectHelper.isEmpty(fileSearchVectorStoreIds)) {
+ throw new IllegalArgumentException(
+ "fileSearchVectorStoreIds must be set when
builtinTools includes file_search");
+ }
+ int ids = 0;
+ for (String id : fileSearchVectorStoreIds.split(",")) {
+ String trimmed = id.trim();
+ if (!trimmed.isEmpty()) {
+ fsBuilder.addVectorStoreId(trimmed);
+ ids++;
+ }
+ }
+ if (ids == 0) {
+ throw new IllegalArgumentException(
+ "fileSearchVectorStoreIds must contain at
least one vector store id for file_search");
+ }
+ paramsBuilder.addTool(fsBuilder.build());
+ }
+ case "code_interpreter" ->
paramsBuilder.addTool(Tool.CodeInterpreter.builder()
+ .type(JsonValue.from("code_interpreter"))
+ .container("auto")
+ .build());
+ default -> throw new IllegalArgumentException(
+ "Unknown builtin tool '" + token.trim()
+ + "'. Supported:
web_search, file_search, code_interpreter");
+ }
+ }
+ }
+
+ static void applyHostedMcpTools(ResponseCreateParams.Builder
paramsBuilder, String hostedMcpToolsJson)
+ throws Exception {
+ if (ObjectHelper.isEmpty(hostedMcpToolsJson)) {
+ return;
+ }
+ JsonNode root = OBJECT_MAPPER.readTree(hostedMcpToolsJson);
+ if (!root.isArray()) {
+ throw new IllegalArgumentException("hostedMcpTools must be a JSON
array of Tool.Mcp objects");
+ }
+ for (JsonNode node : root) {
+ Tool.Mcp.Builder builder = Tool.Mcp.builder();
+ if (node.hasNonNull("server_label")) {
+ builder.serverLabel(node.get("server_label").asText());
+ } else if (node.hasNonNull("serverLabel")) {
+ builder.serverLabel(node.get("serverLabel").asText());
+ }
+ if (node.hasNonNull("server_url")) {
+ builder.serverUrl(node.get("server_url").asText());
+ } else if (node.hasNonNull("serverUrl")) {
+ builder.serverUrl(node.get("serverUrl").asText());
+ }
+ if (node.hasNonNull("server_description")) {
+
builder.serverDescription(node.get("server_description").asText());
+ } else if (node.hasNonNull("serverDescription")) {
+
builder.serverDescription(node.get("serverDescription").asText());
+ }
+ paramsBuilder.addTool(builder.build());
+ }
+ }
+
+ static void applyJsonSchemaTextFormat(ResponseCreateParams.Builder
paramsBuilder, String jsonSchema)
+ throws Exception {
+ Map<String, Object> root = OBJECT_MAPPER.readValue(jsonSchema,
Map.class);
+ if (root == null) {
+ throw new IllegalArgumentException("JSON schema string parsed to
null");
+ }
+ ResponseFormatTextJsonSchemaConfig.Schema.Builder schemaBuilder
+ = ResponseFormatTextJsonSchemaConfig.Schema.builder();
+ for (Map.Entry<String, Object> e : root.entrySet()) {
+ schemaBuilder.putAdditionalProperty(e.getKey(),
JsonValue.from(e.getValue()));
+ }
+ ResponseFormatTextJsonSchemaConfig jsonSchemaConfig =
ResponseFormatTextJsonSchemaConfig.builder()
+ .name("camel_schema")
+ .schema(schemaBuilder.build())
+ .build();
+ paramsBuilder.text(
+ ResponseTextConfig.builder()
+
.format(ResponseFormatTextConfig.ofJsonSchema(jsonSchemaConfig))
+ .build());
+ }
+
+ static void applyAdditionalBodyProperties(ResponseCreateParams.Builder
paramsBuilder, Map<String, Object> additional) {
+ if (additional == null || additional.isEmpty()) {
+ return;
+ }
+ for (Map.Entry<String, Object> e : additional.entrySet()) {
+ Object valueToUse = e.getValue();
+ if (valueToUse instanceof String s) {
+ valueToUse = parseJsonOrString(s);
+ }
+ paramsBuilder.putAdditionalBodyProperty(e.getKey(),
JsonValue.from(valueToUse));
+ }
+ }
+
+ private static Object parseJsonOrString(String value) {
+ try {
+ return OBJECT_MAPPER.readValue(value, Object.class);
+ } catch (Exception e) {
+ return value;
+ }
+ }
+
+ static String extractAssistantText(Response response) {
+ StringBuilder text = new StringBuilder();
+ for (ResponseOutputItem item : response.output()) {
+ if (!item.isMessage()) {
+ continue;
+ }
+ ResponseOutputMessage message = item.asMessage();
+ for (ResponseOutputMessage.Content content : message.content()) {
+ if (content.isOutputText()) {
+ ResponseOutputText outputText = content.asOutputText();
+ if (text.length() > 0) {
+ text.append('\n');
+ }
+ text.append(outputText.text());
+ }
+ }
+ }
+ return text.toString();
+ }
+
+ static Optional<String> extractFinishStatus(Response response) {
+ for (ResponseOutputItem item : response.output()) {
+ if (item.isMessage()) {
+ return Optional.of(item.asMessage().status().toString());
+ }
+ }
+ return response.status().map(Object::toString);
+ }
+
+ static List<String> collectBuiltinToolTypesInRequest(String requestBody)
throws Exception {
Review Comment:
`collectBuiltinToolTypesInRequest()` is only called from
`OpenAIResponsesMockTest`. Test-only utilities should not live in production
code — consider moving this method into the test class or a test support
utility.
##########
components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesSupport.java:
##########
@@ -0,0 +1,217 @@
+/*
+ * 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.openai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.core.JsonValue;
+import com.openai.models.responses.FileSearchTool;
+import com.openai.models.responses.Response;
+import com.openai.models.responses.ResponseCreateParams;
+import com.openai.models.responses.ResponseFormatTextConfig;
+import com.openai.models.responses.ResponseFormatTextJsonSchemaConfig;
+import com.openai.models.responses.ResponseOutputItem;
+import com.openai.models.responses.ResponseOutputMessage;
+import com.openai.models.responses.ResponseOutputText;
+import com.openai.models.responses.ResponseTextConfig;
+import com.openai.models.responses.ResponseUsage;
+import com.openai.models.responses.Tool;
+import com.openai.models.responses.WebSearchTool;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Helpers for the OpenAI Responses API producer.
+ */
+final class OpenAIResponsesSupport {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private OpenAIResponsesSupport() {
+ }
+
+ static void applyBuiltinTools(
+ ResponseCreateParams.Builder paramsBuilder, String builtinTools,
+ String fileSearchVectorStoreIds) {
+ if (ObjectHelper.isEmpty(builtinTools)) {
+ return;
+ }
+ for (String token : builtinTools.split(",")) {
+ String name = token.trim().toLowerCase(Locale.ROOT);
+ if (name.isEmpty()) {
+ continue;
+ }
+ switch (name) {
+ case "web_search" ->
paramsBuilder.addTool(WebSearchTool.builder()
+ .type(WebSearchTool.Type.WEB_SEARCH)
+ .build());
+ case "file_search" -> {
+ FileSearchTool.Builder fsBuilder = FileSearchTool.builder()
+ .type(JsonValue.from("file_search"));
+ if (ObjectHelper.isEmpty(fileSearchVectorStoreIds)) {
+ throw new IllegalArgumentException(
+ "fileSearchVectorStoreIds must be set when
builtinTools includes file_search");
+ }
+ int ids = 0;
+ for (String id : fileSearchVectorStoreIds.split(",")) {
+ String trimmed = id.trim();
+ if (!trimmed.isEmpty()) {
+ fsBuilder.addVectorStoreId(trimmed);
+ ids++;
+ }
+ }
+ if (ids == 0) {
+ throw new IllegalArgumentException(
+ "fileSearchVectorStoreIds must contain at
least one vector store id for file_search");
+ }
+ paramsBuilder.addTool(fsBuilder.build());
+ }
+ case "code_interpreter" ->
paramsBuilder.addTool(Tool.CodeInterpreter.builder()
+ .type(JsonValue.from("code_interpreter"))
+ .container("auto")
+ .build());
+ default -> throw new IllegalArgumentException(
+ "Unknown builtin tool '" + token.trim()
+ + "'. Supported:
web_search, file_search, code_interpreter");
+ }
+ }
+ }
+
+ static void applyHostedMcpTools(ResponseCreateParams.Builder
paramsBuilder, String hostedMcpToolsJson)
+ throws Exception {
+ if (ObjectHelper.isEmpty(hostedMcpToolsJson)) {
+ return;
+ }
+ JsonNode root = OBJECT_MAPPER.readTree(hostedMcpToolsJson);
+ if (!root.isArray()) {
+ throw new IllegalArgumentException("hostedMcpTools must be a JSON
array of Tool.Mcp objects");
+ }
+ for (JsonNode node : root) {
+ Tool.Mcp.Builder builder = Tool.Mcp.builder();
+ if (node.hasNonNull("server_label")) {
+ builder.serverLabel(node.get("server_label").asText());
+ } else if (node.hasNonNull("serverLabel")) {
+ builder.serverLabel(node.get("serverLabel").asText());
+ }
+ if (node.hasNonNull("server_url")) {
+ builder.serverUrl(node.get("server_url").asText());
+ } else if (node.hasNonNull("serverUrl")) {
+ builder.serverUrl(node.get("serverUrl").asText());
+ }
+ if (node.hasNonNull("server_description")) {
+
builder.serverDescription(node.get("server_description").asText());
+ } else if (node.hasNonNull("serverDescription")) {
+
builder.serverDescription(node.get("serverDescription").asText());
+ }
+ paramsBuilder.addTool(builder.build());
+ }
+ }
+
+ static void applyJsonSchemaTextFormat(ResponseCreateParams.Builder
paramsBuilder, String jsonSchema)
+ throws Exception {
+ Map<String, Object> root = OBJECT_MAPPER.readValue(jsonSchema,
Map.class);
+ if (root == null) {
+ throw new IllegalArgumentException("JSON schema string parsed to
null");
+ }
+ ResponseFormatTextJsonSchemaConfig.Schema.Builder schemaBuilder
+ = ResponseFormatTextJsonSchemaConfig.Schema.builder();
+ for (Map.Entry<String, Object> e : root.entrySet()) {
+ schemaBuilder.putAdditionalProperty(e.getKey(),
JsonValue.from(e.getValue()));
+ }
+ ResponseFormatTextJsonSchemaConfig jsonSchemaConfig =
ResponseFormatTextJsonSchemaConfig.builder()
+ .name("camel_schema")
+ .schema(schemaBuilder.build())
+ .build();
+ paramsBuilder.text(
+ ResponseTextConfig.builder()
+
.format(ResponseFormatTextConfig.ofJsonSchema(jsonSchemaConfig))
+ .build());
+ }
+
+ static void applyAdditionalBodyProperties(ResponseCreateParams.Builder
paramsBuilder, Map<String, Object> additional) {
+ if (additional == null || additional.isEmpty()) {
+ return;
+ }
+ for (Map.Entry<String, Object> e : additional.entrySet()) {
+ Object valueToUse = e.getValue();
+ if (valueToUse instanceof String s) {
+ valueToUse = parseJsonOrString(s);
+ }
+ paramsBuilder.putAdditionalBodyProperty(e.getKey(),
JsonValue.from(valueToUse));
+ }
+ }
+
+ private static Object parseJsonOrString(String value) {
+ try {
+ return OBJECT_MAPPER.readValue(value, Object.class);
+ } catch (Exception e) {
+ return value;
+ }
+ }
+
+ static String extractAssistantText(Response response) {
+ StringBuilder text = new StringBuilder();
+ for (ResponseOutputItem item : response.output()) {
+ if (!item.isMessage()) {
+ continue;
+ }
+ ResponseOutputMessage message = item.asMessage();
+ for (ResponseOutputMessage.Content content : message.content()) {
+ if (content.isOutputText()) {
+ ResponseOutputText outputText = content.asOutputText();
+ if (text.length() > 0) {
+ text.append('\n');
+ }
+ text.append(outputText.text());
+ }
+ }
+ }
+ return text.toString();
+ }
+
+ static Optional<String> extractFinishStatus(Response response) {
+ for (ResponseOutputItem item : response.output()) {
+ if (item.isMessage()) {
+ return Optional.of(item.asMessage().status().toString());
+ }
+ }
+ return response.status().map(Object::toString);
+ }
+
+ static List<String> collectBuiltinToolTypesInRequest(String requestBody)
throws Exception {
+ List<String> types = new ArrayList<>();
+ JsonNode root = OBJECT_MAPPER.readTree(requestBody);
+ JsonNode tools = root.get("tools");
+ if (tools != null && tools.isArray()) {
+ for (JsonNode tool : tools) {
+ if (tool.has("type")) {
+ types.add(tool.get("type").asText());
+ }
+ }
+ }
+ return types;
+ }
+
+ static ResponseUsage usageOrNull(Response response) {
Review Comment:
`usageOrNull()` is defined but never called anywhere in the codebase. Please
remove dead code, or if it's intended for future use, add it in that future PR.
##########
components/camel-ai/camel-openai/src/main/docs/openai-responses.adoc:
##########
@@ -0,0 +1,35 @@
+= OpenAI - Responses API Operation
+:tabs-sync-option:
+
+xref:ROOT:openai-component.adoc[Back to OpenAI Component]
+
+The `responses` operation calls the OpenAI Responses API (non-streaming). It
supports the same text and image
+input ergonomics as `chat-completion`, maps `systemMessage` to API
`instructions`, and exposes
+`CamelOpenAIResponseId` plus token usage headers. Use `previousResponseId` /
`CamelOpenAIPreviousResponseId` for
+server-side conversation state.
+
+Hosted tools: set `builtinTools` to a comma-separated list (`web_search`,
`file_search`, `code_interpreter`).
+`file_search` requires `fileSearchVectorStoreIds`. Pass hosted MCP tool
definitions as JSON via `hostedMcpTools`.
+
+When `storeFullResponse=true`, the SDK `Response` object is stored on exchange
property `CamelOpenAIResponsesResponse`.
+
+[tabs]
+====
Review Comment:
The code example only has a `Java::` tab. All sibling sub-pages
(`openai-mcp.adoc`, `openai-operations.adoc`, `openai-providers.adoc`) provide
both Java and YAML tabs. Please add a `YAML::` tab with the equivalent YAML DSL
example for consistency.
##########
components/camel-ai/camel-openai/src/main/docs/openai-responses.adoc:
##########
@@ -0,0 +1,35 @@
+= OpenAI - Responses API Operation
+:tabs-sync-option:
+
+xref:ROOT:openai-component.adoc[Back to OpenAI Component]
+
+The `responses` operation calls the OpenAI Responses API (non-streaming). It
supports the same text and image
+input ergonomics as `chat-completion`, maps `systemMessage` to API
`instructions`, and exposes
+`CamelOpenAIResponseId` plus token usage headers. Use `previousResponseId` /
`CamelOpenAIPreviousResponseId` for
+server-side conversation state.
+
+Hosted tools: set `builtinTools` to a comma-separated list (`web_search`,
`file_search`, `code_interpreter`).
+`file_search` requires `fileSearchVectorStoreIds`. Pass hosted MCP tool
definitions as JSON via `hostedMcpTools`.
+
+When `storeFullResponse=true`, the SDK `Response` object is stored on exchange
property `CamelOpenAIResponsesResponse`.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:ask")
+ .to("openai:responses?model=gpt-4o&systemMessage=You are a support
assistant")
+ .log("Answer: ${body} id: ${header.CamelOpenAIResponseId}");
+
+from("direct:continue")
+ .setHeader("CamelOpenAIPreviousResponseId", variable("lastId"))
+ .to("openai:responses?model=gpt-4o")
+ .setVariable("lastId", header("CamelOpenAIResponseId"));
+----
+====
+
+Streaming is not supported on this operation; use `chat-completion` for
streaming.
+
+Function tools wired to Camel routes are intentionally deferred (see
CAMEL-23382 / CAMEL-23969).
Review Comment:
JIRA ticket IDs (`CAMEL-23382 / CAMEL-23969`) are not meaningful to end
users reading the docs. Consider replacing with descriptive prose, e.g.:
```suggestion
Function tools wired to Camel routes are not yet supported on this operation.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]