Skip to content
Open
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
2 changes: 2 additions & 0 deletions relay/channel/ali/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions relay/channel/deepseek/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
77 changes: 77 additions & 0 deletions relay/channel/deepseek/thinking.go
Original file line number Diff line number Diff line change
@@ -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-")
}
Comment on lines +40 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

isDeepSeekV4 and Ali's supportsAliAnthropicMessages use inconsistent matching strategies.

isDeepSeekV4 requires HasPrefix("deepseek-v4-") (trailing dash), but supportsAliAnthropicMessages uses Contains("deepseek-v4") (no dash). For all current model names (deepseek-v4-flash, deepseek-v4-pro) this is harmless — both checks pass. However, a model configured with the bare alias "deepseek-v4" would be routed through Ali's Anthropic messages path but EnsureThinkingBeforeToolUse would silently no-op, leaving the thinking-before-tool-use gap unfixed.

Consider aligning the two checks:

🛠 Proposed fix
 func isDeepSeekV4(modelName string) bool {
-	return strings.HasPrefix(modelName, "deepseek-v4-")
+	return strings.HasPrefix(modelName, "deepseek-v4-") || modelName == "deepseek-v4"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// isDeepSeekV4 checks if the model name belongs to DeepSeek V4 series.
func isDeepSeekV4(modelName string) bool {
return strings.HasPrefix(modelName, "deepseek-v4-")
}
// isDeepSeekV4 checks if the model name belongs to DeepSeek V4 series.
func isDeepSeekV4(modelName string) bool {
return strings.HasPrefix(modelName, "deepseek-v4-") || modelName == "deepseek-v4"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/deepseek/thinking.go` around lines 40 - 43, The prefix check in
isDeepSeekV4 is too strict (HasPrefix("deepseek-v4-")) and mismatches
supportsAliAnthropicMessages which uses Contains("deepseek-v4"); update
isDeepSeekV4 to use strings.HasPrefix(modelName, "deepseek-v4") (or otherwise
match the same "deepseek-v4" substring logic used by
supportsAliAnthropicMessages) so models named "deepseek-v4" and variants like
"deepseek-v4-pro" route consistently and EnsureThinkingBeforeToolUse no longer
silently no-ops for the bare alias; adjust the implementation in isDeepSeekV4
accordingly.


// 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
}
Loading