Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions relay/channel/deepseek/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
71 changes: 71 additions & 0 deletions relay/channel/deepseek/adaptor_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
112 changes: 112 additions & 0 deletions relay/channel/deepseek/claude_system_normalize.go
Original file line number Diff line number Diff line change
@@ -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
}