From 4442bb302898fef9763c91dab8c638ae2b27fbe7 Mon Sep 17 00:00:00 2001 From: zcxads666 <1316510990@qq.com> Date: Sat, 15 Aug 2026 13:55:18 +0800 Subject: [PATCH] fix(relay): stop injecting empty tools into Claude requests --- .../claude_default_max_tokens_test.go | 77 +++++++++++++++++++ .../oai_chat/to_claude_messages_req.go | 4 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/relaykit/relayconvert/claude_default_max_tokens_test.go b/relaykit/relayconvert/claude_default_max_tokens_test.go index d6a3f9f3..d97c17b0 100644 --- a/relaykit/relayconvert/claude_default_max_tokens_test.go +++ b/relaykit/relayconvert/claude_default_max_tokens_test.go @@ -7,6 +7,7 @@ import ( "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" + kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -104,6 +105,82 @@ func TestClaudeThinkingAdapterSatisfiesMaxTokensWithoutCallback(t *testing.T) { assert.Equal(t, uint(1280), *got.MaxTokens) } +func TestOpenAIChatRequestToClaudeMessagesOmitsEmptyTools(t *testing.T) { + maxTokens := uint(16) + tests := []struct { + name string + request dto.GeneralOpenAIRequest + wantTools bool + }{ + { + name: "omitted tools", + request: dto.GeneralOpenAIRequest{ + Model: "claude-test", + MaxTokens: &maxTokens, + Messages: []dto.Message{{Role: "user", Content: "hi"}}, + }, + }, + { + name: "explicit empty tools", + request: dto.GeneralOpenAIRequest{ + Model: "claude-test", + MaxTokens: &maxTokens, + Messages: []dto.Message{{Role: "user", Content: "hi"}}, + Tools: []dto.ToolCallRequest{}, + }, + }, + { + name: "function tool", + request: dto.GeneralOpenAIRequest{ + Model: "claude-test", + MaxTokens: &maxTokens, + Messages: []dto.Message{{Role: "user", Content: "hi"}}, + Tools: []dto.ToolCallRequest{{ + Type: "function", + Function: dto.FunctionRequest{ + Name: "get_weather", + Description: "Get weather by city", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{"city": map[string]any{"type": "string"}}, + "required": []any{"city"}, + }, + }, + }}, + }, + wantTools: true, + }, + { + name: "web search only", + request: dto.GeneralOpenAIRequest{ + Model: "claude-test", + MaxTokens: &maxTokens, + Messages: []dto.Message{{Role: "user", Content: "hi"}}, + WebSearchOptions: &dto.WebSearchOptions{SearchContextSize: "low"}, + }, + wantTools: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := OpenAIChatRequestToClaudeMessages(context.Background(), &convmeta.Values{}, test.request) + require.NoError(t, err) + + body, err := kitutil.Marshal(got) + require.NoError(t, err) + + if test.wantTools { + assert.NotNil(t, got.Tools) + assert.Contains(t, string(body), `"tools":`) + return + } + assert.Nil(t, got.Tools) + assert.NotContains(t, string(body), `"tools":`) + }) + } +} + func claudeDefaultsMeta(defaultMaxTokens func(string) int) convmeta.Meta { return &convmeta.Values{Options: &convmeta.Options{ Claude: convmeta.ClaudeOptions{DefaultMaxTokens: defaultMaxTokens}, diff --git a/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go index 54cb11d5..fec9d95e 100644 --- a/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go +++ b/relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go @@ -101,7 +101,9 @@ func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, te Model: textRequest.Model, StopSequences: nil, Temperature: textRequest.Temperature, - Tools: claudeTools, + } + if len(claudeTools) > 0 { + claudeRequest.Tools = claudeTools } if maxTokens := textRequest.GetMaxTokens(); maxTokens > 0 { claudeRequest.MaxTokens = kitutil.GetPointer(maxTokens)