feat(translator): consolidate mid-conversation system messages into initial system content

- Updated `ConvertClaudeRequestToOpenAI` to move mid-conversation `system` messages to the initial system content.
- Added a new test case `TestConvertClaudeRequestToOpenAI_MidConversationSystemMessagesMoveToInitialSystem` to validate functionality.
- Refactored logic for appending system message content for improved readability and maintainability.

Closes: #3815
This commit is contained in:
Luis Pater
2026-06-13 16:17:36 +08:00
parent b94178bfd9
commit d6c4fc2d82
2 changed files with 79 additions and 18 deletions
@@ -103,25 +103,41 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
// Handle system message first
systemMsgJSON := []byte(`{"role":"system","content":[]}`)
hasSystemContent := false
if system := root.Get("system"); system.Exists() {
if system.Type == gjson.String {
if system.String() != "" && !util.IsClaudeCodeAttributionSystemText(system.String()) {
oldSystem := []byte(`{"type":"text","text":""}`)
oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String())
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
hasSystemContent = true
}
} else if system.Type == gjson.JSON {
if system.IsArray() {
systemResults := system.Array()
for i := 0; i < len(systemResults); i++ {
if contentItem, ok := convertClaudeContentPart(systemResults[i]); ok {
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", []byte(contentItem))
hasSystemContent = true
}
}
}
appendSystemContent := func(content gjson.Result) {
if !content.Exists() {
return
}
if content.Type == gjson.String {
if content.String() == "" || util.IsClaudeCodeAttributionSystemText(content.String()) {
return
}
oldSystem := []byte(`{"type":"text","text":""}`)
oldSystem, _ = sjson.SetBytes(oldSystem, "text", content.String())
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
hasSystemContent = true
return
}
if content.IsArray() {
content.ForEach(func(_, item gjson.Result) bool {
if contentItem, ok := convertClaudeContentPart(item); ok {
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", []byte(contentItem))
hasSystemContent = true
}
return true
})
}
}
if system := root.Get("system"); system.Exists() {
appendSystemContent(system)
}
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
messages.ForEach(func(_, message gjson.Result) bool {
if message.Get("role").String() == "system" {
appendSystemContent(message.Get("content"))
}
return true
})
}
// Only add system message if it has content
if hasSystemContent {
@@ -132,6 +148,9 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
messages.ForEach(func(_, message gjson.Result) bool {
role := message.Get("role").String()
if role == "system" {
return true
}
contentResult := message.Get("content")
// Handle content
@@ -2,6 +2,7 @@ package claude
import (
"encoding/base64"
"fmt"
"testing"
"github.com/tidwall/gjson"
@@ -356,6 +357,47 @@ func validGPTChatReasoningSignature() string {
return base64.URLEncoding.EncodeToString(raw)
}
func TestConvertClaudeRequestToOpenAI_MidConversationSystemMessagesMoveToInitialSystem(t *testing.T) {
inputJSON := `{
"model": "claude-sonnet-4-5",
"system": [{"type": "text", "text": "Top-level rules"}],
"messages": [
{"role": "user", "content": [{"type": "text", "text": "Hello"}]},
{"role": "system", "content": "String mid-conversation rule"},
{"role": "assistant", "content": [{"type": "text", "text": "Hi there"}]},
{"role": "system", "content": [{"type": "text", "text": "Array mid-conversation rule"}]},
{"role": "user", "content": [{"type": "text", "text": "Follow up"}]}
]
}`
result := ConvertClaudeRequestToOpenAI("gpt-5", []byte(inputJSON), false)
resultJSON := gjson.ParseBytes(result)
messages := resultJSON.Get("messages").Array()
if len(messages) != 4 {
t.Fatalf("Expected 4 messages, got %d: %s", len(messages), resultJSON.Get("messages").Raw)
}
roles := make([]string, 0, len(messages))
for _, message := range messages {
roles = append(roles, message.Get("role").String())
}
if got, want := roles, []string{"system", "user", "assistant", "user"}; fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) {
t.Fatalf("Unexpected message roles: got %v, want %v", got, want)
}
systemContent := messages[0].Get("content").Array()
if len(systemContent) != 3 {
t.Fatalf("Expected 3 system content items, got %d: %s", len(systemContent), messages[0].Get("content").Raw)
}
wantTexts := []string{"Top-level rules", "String mid-conversation rule", "Array mid-conversation rule"}
for i, want := range wantTexts {
if got := systemContent[i].Get("text").String(); got != want {
t.Fatalf("system content[%d] = %q, want %q", i, got, want)
}
}
}
func TestConvertClaudeRequestToOpenAI_SystemMessageScenarios(t *testing.T) {
tests := []struct {
name string