diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index 60eaf22be568..0c4d2937a5ac 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -37,6 +37,7 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn if !ok { return convertedRequest, nil } + normalizeClaudeSystemMessagesForNonNativeUpstream(claudeRequest) if err := applyDeepSeekV4ClaudeThinkingSuffix(info, claudeRequest); err != nil { return nil, err } diff --git a/relay/channel/deepseek/adaptor_test.go b/relay/channel/deepseek/adaptor_test.go new file mode 100644 index 000000000000..58c1752cae92 --- /dev/null +++ b/relay/channel/deepseek/adaptor_test.go @@ -0,0 +1,71 @@ +package deepseek + +import ( + "strings" + "testing" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" +) + +func TestConvertClaudeRequestMergesTrailingSystemIntoPreviousUser(t *testing.T) { + req := &dto.ClaudeRequest{ + Model: "deepseek-v4-pro", + Messages: []dto.ClaudeMessage{ + {Role: "user", Content: "Why did the skill stop?"}, + {Role: "system", Content: "Ultracode is on: keep working."}, + }, + } + + converted, err := (&Adaptor{}).ConvertClaudeRequest(nil, &relaycommon.RelayInfo{}, req) + if err != nil { + t.Fatalf("ConvertClaudeRequest returned error: %v", err) + } + claudeReq := converted.(*dto.ClaudeRequest) + + if len(claudeReq.Messages) != 1 { + t.Fatalf("expected 1 message after normalization, got %d", len(claudeReq.Messages)) + } + if claudeReq.Messages[0].Role != "user" { + t.Fatalf("expected normalized message to remain user, got %q", claudeReq.Messages[0].Role) + } + content := claudeReq.Messages[0].GetStringContent() + if !strings.Contains(content, "Why did the skill stop?") { + t.Fatalf("expected user content to be preserved, got %q", content) + } + if !strings.Contains(content, "Ultracode is on: keep working.") { + t.Fatalf("expected trailing system content to be merged into user content, got %q", content) + } +} + +func TestConvertClaudeRequestMergesInterleavedSystemIntoNextUser(t *testing.T) { + req := &dto.ClaudeRequest{ + Model: "deepseek-v4-pro", + Messages: []dto.ClaudeMessage{ + {Role: "user", Content: "First request"}, + {Role: "assistant", Content: "First response"}, + {Role: "system", Content: "Apply this reminder before the next answer."}, + {Role: "user", Content: "Second request"}, + }, + } + + converted, err := (&Adaptor{}).ConvertClaudeRequest(nil, &relaycommon.RelayInfo{}, req) + if err != nil { + t.Fatalf("ConvertClaudeRequest returned error: %v", err) + } + claudeReq := converted.(*dto.ClaudeRequest) + + if len(claudeReq.Messages) != 3 { + t.Fatalf("expected 3 messages after normalization, got %d", len(claudeReq.Messages)) + } + if claudeReq.Messages[2].Role != "user" { + t.Fatalf("expected interleaved system to be merged into next user, got role %q", claudeReq.Messages[2].Role) + } + content := claudeReq.Messages[2].GetStringContent() + if !strings.Contains(content, "Apply this reminder before the next answer.") { + t.Fatalf("expected interleaved system content in next user message, got %q", content) + } + if !strings.Contains(content, "Second request") { + t.Fatalf("expected next user content to be preserved, got %q", content) + } +} diff --git a/relay/channel/deepseek/claude_system_normalize.go b/relay/channel/deepseek/claude_system_normalize.go new file mode 100644 index 000000000000..85778174d73a --- /dev/null +++ b/relay/channel/deepseek/claude_system_normalize.go @@ -0,0 +1,112 @@ +package deepseek + +import ( + "strings" + + "github.com/QuantumNous/new-api/dto" +) + +func normalizeClaudeSystemMessagesForNonNativeUpstream(request *dto.ClaudeRequest) { + if request == nil || len(request.Messages) == 0 { + return + } + + normalizedMessages := make([]dto.ClaudeMessage, 0, len(request.Messages)) + var pendingSystems []string + + for _, message := range request.Messages { + if message.Role != "system" { + if message.Role == "user" && len(pendingSystems) > 0 { + mergeSystemTextIntoUserMessage(&message, strings.Join(pendingSystems, "\n\n"), true) + pendingSystems = nil + } + normalizedMessages = append(normalizedMessages, message) + continue + } + + systemText := claudeSystemMessageText(message) + if systemText == "" { + continue + } + if len(normalizedMessages) > 0 && normalizedMessages[len(normalizedMessages)-1].Role == "user" { + mergeSystemTextIntoUserMessage(&normalizedMessages[len(normalizedMessages)-1], systemText, false) + continue + } + pendingSystems = append(pendingSystems, systemText) + } + + if len(pendingSystems) > 0 { + mergePendingSystemTextIntoLastUser(normalizedMessages, strings.Join(pendingSystems, "\n\n")) + } + request.Messages = normalizedMessages +} + +func mergePendingSystemTextIntoLastUser(messages []dto.ClaudeMessage, systemText string) { + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Role == "user" { + mergeSystemTextIntoUserMessage(&messages[i], systemText, false) + return + } + } +} + +func claudeSystemMessageText(message dto.ClaudeMessage) string { + if message.IsStringContent() { + return strings.TrimSpace(message.GetStringContent()) + } + contents, err := message.ParseContent() + if err != nil { + return strings.TrimSpace(message.GetStringContent()) + } + var builder strings.Builder + for _, content := range contents { + if content.Type == dto.ContentTypeText { + builder.WriteString(content.GetText()) + } + } + return strings.TrimSpace(builder.String()) +} + +func mergeSystemTextIntoUserMessage(message *dto.ClaudeMessage, systemText string, prepend bool) { + if systemText == "" { + return + } + if message.IsStringContent() { + userText := message.GetStringContent() + if prepend { + message.SetStringContent(joinClaudeText(systemText, userText)) + } else { + message.SetStringContent(joinClaudeText(userText, systemText)) + } + return + } + + systemTextBlock := dto.ClaudeMediaMessage{Type: dto.ContentTypeText} + systemTextBlock.SetText(systemText) + contents, err := message.ParseContent() + if err != nil || len(contents) == 0 { + if prepend { + message.SetStringContent(joinClaudeText(systemText, message.GetStringContent())) + } else { + message.SetStringContent(joinClaudeText(message.GetStringContent(), systemText)) + } + return + } + if prepend { + message.SetContent(append([]dto.ClaudeMediaMessage{systemTextBlock}, contents...)) + } else { + message.SetContent(append(contents, systemTextBlock)) + } +} + +func joinClaudeText(first string, second string) string { + first = strings.TrimSpace(first) + second = strings.TrimSpace(second) + if first == "" { + return second + } + if second == "" { + return first + } + return first + "\n\n" + second +}