From 11a979cbd31c4872a66e8006ada3df4380f4b560 Mon Sep 17 00:00:00 2001 From: "chi.cat" Date: Sun, 3 May 2026 01:31:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dclaudecode=E4=BD=BF?= =?UTF-8?q?=E7=94=A8deepseek-v4=E7=B3=BB=E5=88=97,=20=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E5=93=8D=E5=BA=94tool=5Fuse=E5=89=8D=E6=B2=A1=E6=9C=89thinking?= =?UTF-8?q?=E5=86=85=E5=AE=B9,=E5=AF=BC=E8=87=B4=E5=90=8E=E7=BB=AD?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=8A=A5=E9=94=99(The=20`content[].thinking`?= =?UTF-8?q?=20in=20the=20thinking=20mode=20must=20be=20passed=20back=20to?= =?UTF-8?q?=20the=20API.),=20=E4=BF=AE=E5=A4=8D=E4=B8=BA=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=88=B0=E5=BC=80=E5=90=AFdeepseek-v4=E7=B3=BB=E5=88=97?= =?UTF-8?q?=E6=97=B6,=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=85=E7=A9=BAthiking?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E3=80=82=E7=9B=AE=E5=89=8D=E5=B7=B2=E7=9F=A5?= =?UTF-8?q?deepseek-v4=E7=B3=BB=E5=88=97=E9=BB=98=E8=AE=A4=E5=BC=80?= =?UTF-8?q?=E5=90=AFthinking=E6=A8=A1=E5=BC=8F(https://api-docs.deepseek.c?= =?UTF-8?q?om/zh-cn/quick=5Fstart/pricing),=20=E5=8F=88=E5=B7=B2=E7=9F=A5?= =?UTF-8?q?=E5=9C=A8=E4=B8=A4=E4=B8=AA=20user=20=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E4=B9=8B=E9=97=B4=EF=BC=8C=E5=A6=82=E6=9E=9C=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=9C=AA=E8=BF=9B=E8=A1=8C=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E5=88=99=E4=B8=AD=E9=97=B4=20assistant=20=E7=9A=84=20?= =?UTF-8?q?reasoning=5Fcontent=20=E6=97=A0=E9=9C=80=E5=8F=82=E4=B8=8E?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=8B=BC=E6=8E=A5=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E8=BD=AE=E6=AC=A1=E4=B8=AD=E5=B0=86=E5=85=B6?= =?UTF-8?q?=E4=BC=A0=E5=85=A5=20API=20=E4=BC=9A=E8=A2=AB=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E3=80=82=E5=9C=A8=E4=B8=A4=E4=B8=AA=20user=20=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E4=B9=8B=E9=97=B4=EF=BC=8C=E5=A6=82=E6=9E=9C=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=BF=9B=E8=A1=8C=E4=BA=86=E5=B7=A5=E5=85=B7=E8=B0=83?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=88=99=E4=B8=AD=E9=97=B4=20assistant=20?= =?UTF-8?q?=E7=9A=84=20reasoning=5Fcontent=20=E9=9C=80=E5=8F=82=E4=B8=8E?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=8B=BC=E6=8E=A5=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E6=89=80=E6=9C=89=20user=20=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E8=BD=AE=E6=AC=A1=E4=B8=AD=E5=BF=85=E9=A1=BB=E5=9B=9E?= =?UTF-8?q?=E4=BC=A0=E7=BB=99=20API=E3=80=82=20(https://api-docs.deepseek.?= =?UTF-8?q?com/zh-cn/guides/thinking=5Fmode)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/ali/adaptor.go | 2 + relay/channel/deepseek/adaptor.go | 2 + relay/channel/deepseek/thinking.go | 77 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 relay/channel/deepseek/thinking.go 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 +}