Skip to content

Commit 0f48e3f

Browse files
authored
feat(formatter): support both reasoning_content and reasoning fields for deserialization (#1086)
1 parent 8caae64 commit 0f48e3f

3 files changed

Lines changed: 178 additions & 2 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/formatter/dashscope/dto/DashScopeMessage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.agentscope.core.formatter.dashscope.dto;
1717

18+
import com.fasterxml.jackson.annotation.JsonAlias;
1819
import com.fasterxml.jackson.annotation.JsonIgnore;
1920
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2021
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -79,6 +80,7 @@ public class DashScopeMessage {
7980

8081
/** Reasoning/thinking content (for assistant messages with thinking enabled). */
8182
@JsonProperty("reasoning_content")
83+
@JsonAlias("reasoning")
8284
private String reasoningContent;
8385

8486
/** Cache control configuration for prompt caching. */

agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIMessage.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.agentscope.core.formatter.openai.dto;
1717

18+
import com.fasterxml.jackson.annotation.JsonAlias;
1819
import com.fasterxml.jackson.annotation.JsonIgnore;
1920
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2021
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -76,10 +77,12 @@ public class OpenAIMessage {
7677
private List<OpenAIToolCall> toolCalls;
7778

7879
/**
79-
* Reasoning/thinking content (for o1 and similar reasoning models).
80-
* This field contains the model's internal reasoning process.
80+
* Reasoning/thinking content from the model.
81+
* Accepts both {@code reasoning_content} (commercial APIs like DeepSeek, DashScope) and
82+
* {@code reasoning} (vLLM deployments) during deserialization.
8183
*/
8284
@JsonProperty("reasoning_content")
85+
@JsonAlias("reasoning")
8386
private String reasoningContent;
8487

8588
/**
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.formatter.openai.dto;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
22+
import io.agentscope.core.util.JsonCodec;
23+
import io.agentscope.core.util.JsonUtils;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.DisplayName;
26+
import org.junit.jupiter.api.Tag;
27+
import org.junit.jupiter.api.Test;
28+
29+
/**
30+
* Tests for OpenAIMessage reasoning field compatibility.
31+
*
32+
* <p>Verifies that both {@code reasoning_content} (commercial APIs) and {@code reasoning}
33+
* (vLLM) are correctly deserialized into {@code reasoningContent} via {@code @JsonAlias}.
34+
*/
35+
@Tag("unit")
36+
@DisplayName("OpenAIMessage Reasoning Field Compatibility")
37+
class OpenAIMessageReasoningFieldTest {
38+
39+
private JsonCodec jsonCodec;
40+
41+
@BeforeEach
42+
void setUp() {
43+
jsonCodec = JsonUtils.getJsonCodec();
44+
}
45+
46+
@Test
47+
@DisplayName("Should deserialize reasoning_content from commercial APIs")
48+
void testDeserializeReasoningContent() {
49+
String json =
50+
"""
51+
{
52+
"role": "assistant",
53+
"reasoning_content": "Let me analyze this step by step...",
54+
"content": "The answer is 42."
55+
}
56+
""";
57+
58+
OpenAIMessage message = jsonCodec.fromJson(json, OpenAIMessage.class);
59+
60+
assertEquals("Let me analyze this step by step...", message.getReasoningContent());
61+
assertEquals("The answer is 42.", message.getContentAsString());
62+
}
63+
64+
@Test
65+
@DisplayName("Should deserialize reasoning from vLLM deployments via @JsonAlias")
66+
void testDeserializeReasoning() {
67+
String json =
68+
"""
69+
{
70+
"role": "assistant",
71+
"reasoning": "First, I need to consider the problem...",
72+
"content": "The result is 7."
73+
}
74+
""";
75+
76+
OpenAIMessage message = jsonCodec.fromJson(json, OpenAIMessage.class);
77+
78+
assertEquals("First, I need to consider the problem...", message.getReasoningContent());
79+
assertEquals("The result is 7.", message.getContentAsString());
80+
}
81+
82+
@Test
83+
@DisplayName("Should handle null reasoning fields gracefully")
84+
void testDeserializeWithoutReasoning() {
85+
String json =
86+
"""
87+
{
88+
"role": "assistant",
89+
"content": "Simple response without thinking."
90+
}
91+
""";
92+
93+
OpenAIMessage message = jsonCodec.fromJson(json, OpenAIMessage.class);
94+
95+
assertNull(message.getReasoningContent());
96+
}
97+
98+
@Test
99+
@DisplayName("Should deserialize reasoning from vLLM streaming delta")
100+
void testDeserializeVllmStreamingDelta() {
101+
String json =
102+
"""
103+
{
104+
"role": "assistant",
105+
"reasoning": "Thinking chunk...",
106+
"content": null
107+
}
108+
""";
109+
110+
OpenAIMessage delta = jsonCodec.fromJson(json, OpenAIMessage.class);
111+
112+
assertEquals("Thinking chunk...", delta.getReasoningContent());
113+
assertNull(delta.getContentAsString());
114+
}
115+
116+
@Test
117+
@DisplayName("Should deserialize full vLLM-style response")
118+
void testDeserializeFullVllmResponse() {
119+
String json =
120+
"""
121+
{
122+
"id": "chatcmpl-vllm-123",
123+
"object": "chat.completion",
124+
"choices": [
125+
{
126+
"index": 0,
127+
"message": {
128+
"role": "assistant",
129+
"reasoning": "Comparing decimals...",
130+
"content": "9.8 is greater as a decimal number."
131+
},
132+
"finish_reason": "stop"
133+
}
134+
]
135+
}
136+
""";
137+
138+
OpenAIResponse response = jsonCodec.fromJson(json, OpenAIResponse.class);
139+
OpenAIMessage message = response.getFirstChoice().getMessage();
140+
141+
assertEquals("Comparing decimals...", message.getReasoningContent());
142+
assertTrue(message.getContentAsString().contains("9.8 is greater"));
143+
}
144+
145+
@Test
146+
@DisplayName("Should deserialize full vLLM streaming chunk")
147+
void testDeserializeVllmStreamingChunk() {
148+
String json =
149+
"""
150+
{
151+
"id": "chatcmpl-vllm-456",
152+
"object": "chat.completion.chunk",
153+
"choices": [
154+
{
155+
"index": 0,
156+
"delta": {
157+
"role": "assistant",
158+
"reasoning": "Step 1: parse the input..."
159+
},
160+
"finish_reason": null
161+
}
162+
]
163+
}
164+
""";
165+
166+
OpenAIResponse response = jsonCodec.fromJson(json, OpenAIResponse.class);
167+
OpenAIMessage delta = response.getFirstChoice().getDelta();
168+
169+
assertEquals("Step 1: parse the input...", delta.getReasoningContent());
170+
}
171+
}

0 commit comments

Comments
 (0)