This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 915c68f1f74aad8316f6db33607146b0aaad98b5 Author: Croway <[email protected]> AuthorDate: Wed May 6 15:56:31 2026 +0200 CAMEL-23425: address review feedback - docs, non-string fallback, agentic test --- .../src/main/docs/openai-component.adoc | 39 ++++++++++++++++++++++ .../camel/component/openai/OpenAIProducer.java | 4 +-- .../openai/OpenAIProducerMcpMockTest.java | 25 ++++++++++++++ .../test/infra/openai/mock/RequestHandler.java | 2 +- .../test/infra/openai/mock/ResponseBuilder.java | 9 +++-- 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc index 65b01fca6841..5da5f286a4c0 100644 --- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc +++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc @@ -451,6 +451,45 @@ WARNING: Disabling hostname verification is insecure and should only be used in | `sslEndpointAlgorithm` | String | `https` | Hostname verification algorithm; set to empty or `none` to disable |=== +== Reasoning Models + +Some OpenAI-compatible models (e.g., Qwen3, DeepSeek-R1) return chain-of-thought reasoning in a separate `reasoning_content` field alongside the regular `content` in the API response. The component automatically extracts this field and sets it as the `CamelOpenAIReasoningContent` message header. + +This is independent from the inline `<think>...</think>` tag stripping controlled by `stripThinking`. A response can populate both headers simultaneously: + +* `CamelOpenAIReasoningContent` — from the API-level `reasoning_content` field +* `CamelOpenAIThinkingContent` — from inline `<think>` tags in the `content` field (requires `stripThinking=true`) + +[source,java] +---- +from("direct:chat") + .to("openai:chat-completion?model=qwen3&stripThinking=true") + .log("Answer: ${body}") + .log("Reasoning: ${header.CamelOpenAIReasoningContent}") + .log("Thinking: ${header.CamelOpenAIThinkingContent}"); +---- + +NOTE: Reasoning content extraction is supported in non-streaming mode only (both simple and agentic/MCP tool loop paths). Streaming responses do not extract reasoning content. + +=== Mapping Additional Response Fields to Headers + +The `additionalResponseHeader` option allows mapping any extra field from the API response message into a named Camel header. This is useful for provider-specific fields that are not part of the standard OpenAI response schema. + +The key is the field name in the API response, and the value is the Camel header name to set: + +[source,java] +---- +from("direct:chat") + .to("openai:chat-completion?model=qwen3" + + "&additionalResponseHeader.reasoning_content=CamelMyReasoning" + + "&additionalResponseHeader.custom_field=CamelMyCustomField") + .log("Custom reasoning: ${header.CamelMyReasoning}"); +---- + +String-valued fields are set directly. Non-string fields (numbers, booleans, objects) are converted using `toString()`. + +NOTE: This maps fields from the response message's additional properties (fields not part of the standard schema). Standard response fields like `content`, `role`, and `tool_calls` are not accessible through this option. + == Compatibility This component works with any OpenAI API-compatible endpoint by setting the `baseUrl` parameter. This includes: diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java index f91c8639c492..e87a75f2d197 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java @@ -739,7 +739,6 @@ public class OpenAIProducer extends DefaultAsyncProducer { } } - @SuppressWarnings("unchecked") private void extractReasoningContent(Exchange exchange, ChatCompletionMessage message) { Map<String, JsonValue> additional = message._additionalProperties(); JsonValue reasoningValue = additional.get("reasoning_content"); @@ -751,7 +750,6 @@ public class OpenAIProducer extends DefaultAsyncProducer { } } - @SuppressWarnings("unchecked") private void extractAdditionalResponseHeaders(Exchange exchange, ChatCompletionMessage message) { OpenAIConfiguration config = getEndpoint().getConfiguration(); Map<String, Object> mapping = config.getAdditionalResponseHeader(); @@ -768,6 +766,8 @@ public class OpenAIProducer extends DefaultAsyncProducer { String strValue = (String) value.asString().orElse(null); if (strValue != null) { exchange.getMessage().setHeader(headerName, strValue); + } else { + exchange.getMessage().setHeader(headerName, value.toString()); } } } diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIProducerMcpMockTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIProducerMcpMockTest.java index f5bd6166bcf2..5f24e1327e5e 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIProducerMcpMockTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIProducerMcpMockTest.java @@ -63,6 +63,13 @@ public class OpenAIProducerMcpMockTest extends CamelTestSupport { .when("no tools needed") .replyWith("Just a text response") .end() + // Tool call with reasoning content in final response + .when("call tool with reasoning") + .invokeTool("get_weather") + .withParam("city", "Tokyo") + .replyWith("The weather in Tokyo is rainy.") + .replyWithReasoningContent("I need to check the weather API for Tokyo") + .end() .build(); @Override @@ -298,4 +305,22 @@ public class OpenAIProducerMcpMockTest extends CamelTestSupport { assertFalse(result.getMessage().getHeader(OpenAIConstants.MCP_RETURN_DIRECT, Boolean.class)); assertEquals("The weather in London is sunny.", result.getMessage().getBody(String.class)); } + + @Test + void agenticLoopExtractsReasoningContent() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true&baseUrl=" + + openAIMock.getBaseUrl() + "/v1"; + + Map<String, McpSyncClient> toolClients = new HashMap<>(); + toolClients.put("get_weather", createMockMcpClient("get_weather", "Rainy, 18°C")); + injectMcpTools(endpointUri, toolClients); + + Exchange result = template.request("direct:mcp-chat", + e -> e.getIn().setBody("call tool with reasoning")); + + assertEquals("The weather in Tokyo is rainy.", result.getMessage().getBody(String.class)); + assertEquals("I need to check the weather API for Tokyo", + result.getMessage().getHeader(OpenAIConstants.REASONING_CONTENT, String.class)); + assertEquals(1, result.getMessage().getHeader(OpenAIConstants.TOOL_ITERATIONS, Integer.class)); + } } diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java index bd4728d1d6e0..8c8aed7afb90 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java @@ -86,7 +86,7 @@ public class RequestHandler { } else { LOG.debug("Tool sequence completed for expectation: {}", originalInput); return responseBuilder.createFinalToolResponse(context.getMessagesNode(), expectation.getExpectedResponse(), - expectation.getToolContentResponse()); + expectation.getToolContentResponse(), expectation.getReasoningContent()); } } diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java index 9475397ae6dd..8fa7bb7f3658 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java @@ -68,7 +68,9 @@ public class ResponseBuilder { return objectMapper.writeValueAsString(chatCompletion); } - public String createFinalToolResponse(JsonNode messagesNode, String fallbackContent, String toolContentResponse) + public String createFinalToolResponse( + JsonNode messagesNode, String fallbackContent, String toolContentResponse, + String reasoningContent) throws Exception { Map<String, Object> responseMessage = createBaseMessage(); @@ -82,6 +84,9 @@ public class ResponseBuilder { content = extractLastToolContent(messagesNode).orElse("All tools processed"); } responseMessage.put("content", content); + if (reasoningContent != null) { + responseMessage.put("reasoning_content", reasoningContent); + } Map<String, Object> choice = createBaseChoice("stop", responseMessage); Map<String, Object> chatCompletion = createBaseChatCompletion(choice); @@ -91,7 +96,7 @@ public class ResponseBuilder { } public String createFinalToolResponse(JsonNode messagesNode, String fallbackContent) throws Exception { - return createFinalToolResponse(messagesNode, fallbackContent, null); + return createFinalToolResponse(messagesNode, fallbackContent, null, null); } public String createErrorResponse(int statusCode, String errorMessage, HttpExchange exchange) {
