diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 7ff7a582..5a769ca4 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -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 diff --git a/internal/translator/openai/claude/openai_claude_request_test.go b/internal/translator/openai/claude/openai_claude_request_test.go index 9e2d771a..34de754d 100644 --- a/internal/translator/openai/claude/openai_claude_request_test.go +++ b/internal/translator/openai/claude/openai_claude_request_test.go @@ -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