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) {

Reply via email to