Repository: agentscope-ai/agentscope-java (2026-07-14T05:07:52Z)

Issue:
Why does `DeepSeekFormatter` convert `system` messages to `user` messages?
**<u>AgentScope-Java is an open-source project. To involve a broader community, we recommend asking your questions in English.</u>**

`DeepSeekFormatter` currently converts every message with the `system` role into a `user` message:

https://github.com/agentscope-ai/agentscope-java/blob/e3a412ed2cc944e401da861c8d5e464b967724e9/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatter.java#L183-L185

### Problem

This conversion removes the semantic boundary between the system prompt and the actual user input.

For example, the following messages:

```json
[
  {
    "role": "system",
    "content": "You are an enterprise assistant. Follow these instructions..."
  },
  {
    "role": "user",
    "content": "Some question...."
  }
]
```

are formatted as:

```json
[
  {
    "role": "user",
    "content": "You are an enterprise assistant. Follow these instructions..."
  },
  {
    "role": "user",
    "content": "Some question...."
  }
]
```


Discussion:
It should be a history compatible legacy behavior. I read the latest Deepseek document, which clearly supports the system role. 
Hi @jujn, I'm interested in this issue and would like to work on a fix. Could you assign this issue to me?
🤖 **AI Assessment: Question**

**Classification:** Question (area/model/openai)

**Analysis:** Good question. DeepSeek API actually supports system role messages. The previous conversion to user was based on outdated documentation.

**Related PR:** #2189 (fix(openai): preserve system role in DeepSeek formatter) fixes this by preserving the system role.

**Next Steps:** Review and merge #2189.

---
*Automated assessment by github-manager*



Patch:
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatter.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatter.java
index 33a210c827..a981fb05e0 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatter.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatter.java
@@ -27,7 +27,6 @@
  * <p>DeepSeek API has the following specific requirements:
  * <ul>
  *   <li>No name field in messages (returns HTTP 400 if present)</li>
- *   <li>System messages should be converted to user messages</li>
  *   <li>Does NOT support strict parameter in tool definitions</li>
  *   <li>In thinking mode, reasoning_content is preserved for segments with tool calls</li>
  * </ul>
@@ -84,7 +83,6 @@ protected boolean supportsStrict() {
      * <p>DeepSeek API requires:
      * <ul>
      *   <li>No name field in messages</li>
-     *   <li>System messages converted to user</li>
      *   <li>In thinking mode, reasoning_content preserved for segments with tool calls</li>
      *   <li>reasoning_content removed for segments without tool calls in thinking mode</li>
      * </ul>
@@ -169,20 +167,18 @@ private static int findLastUserIndex(List<OpenAIMessage> messages) {
 
     @SuppressWarnings("unchecked")
     private static OpenAIMessage fixMessage(OpenAIMessage msg, boolean needReasoning) {
-        boolean isSystem = "system".equals(msg.getRole());
         boolean hasName = msg.getName() != null;
         boolean hasReasoning = msg.getReasoningContent() != null;
         // needReasoning is determined by applyDeepSeekFixes:
         // true = current turn, or segment had tool calls in thinking mode
         boolean shouldRemoveReasoning = hasReasoning && !needReasoning;
 
-        if (!isSystem && !hasName && !shouldRemoveReasoning) {
+        if (!hasName && !shouldRemoveReasoning) {
             return msg;
         }
 
-        // Build new message: convert system to user, remove name field
-        OpenAIMessage.Builder builder =
-                OpenAIMessage.builder().role(isSystem ? "user" : msg.getRole());
+        // Build new message without the name field or stale reasoning content
+        OpenAIMessage.Builder builder = OpenAIMessage.builder().role(msg.getRole());
 
         Object content = msg.getContent();
         if (content instanceof String s) {
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatter.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatter.java
index b1c21a2a76..80434366c8 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatter.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/main/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatter.java
@@ -25,7 +25,6 @@
  * <p>This formatter extends {@link OpenAIMultiAgentFormatter} with DeepSeek-specific handling:
  * <ul>
  *   <li>No name field in messages (returns HTTP 400 if present)</li>
- *   <li>System messages converted to user messages</li>
  *   <li>Does NOT support strict parameter in tool definitions</li>
  *   <li>reasoning_content handling for thinking mode</li>
  * </ul>


Test patch:
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatterTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatterTest.java
index e45f7a735f..d597bdd138 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatterTest.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekFormatterTest.java
@@ -44,7 +44,7 @@
  * <p>Tests verify DeepSeek-specific requirements:
  * <ul>
  *   <li>No name field in messages</li>
- *   <li>System messages converted to user</li>
+ *   <li>System message roles are preserved</li>
  *   <li>Does NOT support strict parameter in tool definitions</li>
  *   <li>reasoning_content handling for current vs previous turns</li>
  *   <li>Optional empty user message appending</li>
@@ -187,8 +187,8 @@ void testRemoveNameField() {
         }
 
         @Test
-        @DisplayName("Should convert system message to user message")
-        void testConvertSystemToUser() {
+        @DisplayName("Should preserve system message role")
+        void testPreserveSystemRole() {
             List<OpenAIMessage> messages =
                     List.of(
                             OpenAIMessage.builder()
@@ -199,7 +199,7 @@ void testConvertSystemToUser() {
             List<OpenAIMessage> result = DeepSeekFormatter.applyDeepSeekFixes(messages);
 
             assertEquals(1, result.size());
-            assertEquals("user", result.get(0).getRole());
+            assertEquals("system", result.get(0).getRole());
             assertEquals("You are helpful", result.get(0).getContentAsString());
         }
 
@@ -294,7 +294,7 @@ void testPreserveToolCallId() {
         }
 
         @Test
-        @DisplayName("Should handle content as list")
+        @DisplayName("Should preserve system role and list content when removing name")
         void testHandleContentAsList() {
             List<OpenAIContentPart> contentParts =
                     List.of(OpenAIContentPart.text("Hello"), OpenAIContentPart.text("World"));
@@ -310,7 +310,7 @@ void testHandleContentAsList() {
             List<OpenAIMessage> result = DeepSeekFormatter.applyDeepSeekFixes(messages);
 
             assertEquals(1, result.size());
-            assertEquals("user", result.get(0).getRole());
+            assertEquals("system", result.get(0).getRole());
             assertNull(result.get(0).getName());
             assertTrue(result.get(0).getContent() instanceof List);
         }
@@ -701,8 +701,8 @@ void testFormatAppliesFixes() {
             List<OpenAIMessage> result = formatter.format(messages);
 
             assertEquals(2, result.size());
-            // System converted to user
-            assertEquals("user", result.get(0).getRole());
+            // System role preserved
+            assertEquals("system", result.get(0).getRole());
             // Name removed
             assertNull(result.get(1).getName());
         }
@@ -731,9 +731,9 @@ void testMultipleSystemMessages() {
             List<OpenAIMessage> result = formatter.format(messages);
 
             assertEquals(2, result.size());
-            // Both should be converted to user
-            assertEquals("user", result.get(0).getRole());
-            assertEquals("user", result.get(1).getRole());
+            // Both system roles should be preserved
+            assertEquals("system", result.get(0).getRole());
+            assertEquals("system", result.get(1).getRole());
         }
     }
 }
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatterTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatterTest.java
index 536dbfb386..24b107059b 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatterTest.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-openai/src/test/java/io/agentscope/extensions/model/openai/formatter/DeepSeekMultiAgentFormatterTest.java
@@ -44,7 +44,7 @@
  * <p>Tests verify DeepSeek multi-agent specific requirements:
  * <ul>
  *   <li>Inherits multi-agent conversation merging from OpenAIMultiAgentFormatter</li>
- *   <li>Applies DeepSeek-specific fixes (no name, system to user, reasoning_content)</li>
+ *   <li>Applies DeepSeek-specific fixes (no name and reasoning_content handling)</li>
  *   <li>Does NOT support strict parameter in tool definitions</li>
  *   <li>Optional empty user message appending</li>
  * </ul>
@@ -261,8 +261,8 @@ void testRemoveNameField() {
         }
 
         @Test
-        @DisplayName("Should convert system to user in merged output")
-        void testConvertSystemToUser() {
+        @DisplayName("Should preserve system role in merged output")
+        void testPreserveSystemRole() {
             List<Msg> messages =
                     List.of(
                             Msg.builder()
@@ -280,8 +280,8 @@ void testConvertSystemToUser() {
 
             List<OpenAIMessage> result = formatter.format(messages);
 
-            // First message (system) should be converted to user
-            assertEquals("user", result.get(0).getRole());
+            // First message should preserve the system role
+            assertEquals("system", result.get(0).getRole());
         }
 
         @Test
@@ -433,10 +433,10 @@ void testFormatMixedMessages() {
             List<OpenAIMessage> result = formatter.format(messages);
 
             assertNotNull(result);
-            // Should have: system (converted to user) + merged conversation + tool sequence
+            // Should have: system + merged conversation + tool sequence
             assertTrue(result.size() >= 3);
-            // First message should be user (converted from system)
-            assertEquals("user", result.get(0).getRole());
+            // First message should preserve the system role
+            assertEquals("system", result.get(0).getRole());
         }
 
         @Test