diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index cb3070ff367..96f91f48ed8 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -11,6 +11,7 @@ import ( "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/deepseek" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/constant" @@ -72,6 +73,7 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { if supportsAliAnthropicMessages(info.UpstreamModelName) { + deepseek.EnsureThinkingBeforeToolUse(req) return req, nil } diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index 60eaf22be56..1bccf7d4dee 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -40,6 +40,8 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn if err := applyDeepSeekV4ClaudeThinkingSuffix(info, claudeRequest); err != nil { return nil, err } + // DeepSeek V4 requires thinking blocks before tool_use when thinking is enabled + EnsureThinkingBeforeToolUse(claudeRequest) return claudeRequest, nil } diff --git a/relay/channel/deepseek/thinking.go b/relay/channel/deepseek/thinking.go new file mode 100644 index 00000000000..c981411479d --- /dev/null +++ b/relay/channel/deepseek/thinking.go @@ -0,0 +1,77 @@ +package deepseek + +import ( + "strings" + + "github.com/QuantumNous/new-api/dto" +) + +// EnsureThinkingBeforeToolUse ensures that when thinking mode is enabled, +// every tool_use block in assistant messages has a preceding thinking block. +// DeepSeek V4 requires thinking blocks before tool_use when thinking is enabled. +func EnsureThinkingBeforeToolUse(req *dto.ClaudeRequest) { + if req == nil || len(req.Messages) == 0 { + return + } + // Only process deepseek-v4-* models + if !isDeepSeekV4(req.Model) { + return + } + // Thinking disabled? skip + if req.Thinking != nil && req.Thinking.Type == "disabled" { + return + } + for i := range req.Messages { + msg := &req.Messages[i] + if msg.Role != "assistant" { + continue + } + contentList, ok := msg.Content.([]any) + if !ok || len(contentList) == 0 { + continue + } + contentList, fixed := ensureThinkingInContentArray(contentList) + if fixed { + msg.Content = contentList + } + } +} + +// isDeepSeekV4 checks if the model name belongs to DeepSeek V4 series. +func isDeepSeekV4(modelName string) bool { + return strings.HasPrefix(modelName, "deepseek-v4-") +} + +// ensureThinkingInContentArray inserts an empty thinking block at position 0 +// if the content array contains any tool_use without a thinking block as the first element. +// Returns the modified slice and whether modifications were made. +func ensureThinkingInContentArray(contentList []any) ([]any, bool) { + if len(contentList) == 0 { + return contentList, false + } + // Check if the first element is already a thinking block + if first, ok := contentList[0].(map[string]any); ok && first["type"] == "thinking" { + return contentList, false + } + // Check if there are any tool_use blocks + hasToolUse := false + for _, item := range contentList { + if m, ok := item.(map[string]any); ok && m["type"] == "tool_use" { + hasToolUse = true + break + } + } + if !hasToolUse { + return contentList, false + } + // Insert empty thinking block at the beginning + emptyStr := "" + thinkingBlock := map[string]any{ + "type": "thinking", + "thinking": emptyStr, + } + contentList = append(contentList, nil) + copy(contentList[1:], contentList[0:]) + contentList[0] = thinkingBlock + return contentList, true +}