package claude import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/QuantumNous/new-api/common" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/model_setting" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestConvertClaudeRequestTreatsZeroMaxTokensAsUnset(t *testing.T) { zero := uint(0) req := &dto.ClaudeRequest{ Model: "claude-sonnet-4-5", MaxTokens: &zero, Messages: []dto.ClaudeMessage{ {Role: "user", Content: "hello"}, }, } info := &relaycommon.RelayInfo{ ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: "claude-sonnet-4-5", }, } out, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, req) require.NoError(t, err) converted, ok := out.(*dto.ClaudeRequest) require.True(t, ok) require.NotNil(t, converted.MaxTokens) assert.Equal(t, uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(req.Model)), *converted.MaxTokens) } func TestConvertClaudeRequestPreservesNativeClaudeCodeThinking(t *testing.T) { budget := 10000 maxTokens := uint(20000) temperature := 0.7 topP := 0.9 req := &dto.ClaudeRequest{ Model: "claude-opus-4-8", MaxTokens: &maxTokens, Temperature: &temperature, TopP: &topP, Thinking: &dto.Thinking{Type: "enabled", BudgetTokens: &budget}, OutputConfig: []byte(`{"effort":"high"}`), Messages: []dto.ClaudeMessage{ {Role: "user", Content: "hello"}, }, } info := &relaycommon.RelayInfo{ OriginModelName: req.Model, ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: req.Model, }, } out, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, req) require.NoError(t, err) converted, ok := out.(*dto.ClaudeRequest) require.True(t, ok) require.NotNil(t, converted.Thinking) assert.Equal(t, "enabled", converted.Thinking.Type) require.NotNil(t, converted.Thinking.BudgetTokens) assert.Equal(t, budget, *converted.Thinking.BudgetTokens) assert.Equal(t, temperature, *converted.Temperature) assert.Equal(t, topP, *converted.TopP) assert.JSONEq(t, `{"effort":"high"}`, string(converted.OutputConfig)) assert.Empty(t, info.ConversionDiagnostics()) } func TestConvertClaudeRequestPreservesMessageOutputConfig(t *testing.T) { body := `{"model":"claude-opus-5-5","max_tokens":64,"output_config":{"effort":"medium"},"messages":[` + `{"role":"user","content":"summary"},` + `{"role":"system","content":[],"output_config":{"effort":"high"}},` + `{"role":"assistant","content":"done"}]}` var req dto.ClaudeRequest require.NoError(t, common.UnmarshalJsonStr(body, &req)) info := &relaycommon.RelayInfo{ OriginModelName: req.Model, ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: req.Model, }, } out, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, &req) require.NoError(t, err) encoded, err := common.Marshal(out) require.NoError(t, err) var upstream struct { Messages []map[string]any `json:"messages"` } require.NoError(t, common.Unmarshal(encoded, &upstream)) require.Len(t, upstream.Messages, 3) assert.Equal(t, map[string]any{"effort": "high"}, upstream.Messages[1]["output_config"]) assert.NotContains(t, upstream.Messages[0], "output_config") assert.NotContains(t, upstream.Messages[2], "output_config") } func TestConvertClaudeRequestZeroMaxTokensStillRaisesThinkingBudget(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) zero := uint(0) original := &dto.ClaudeRequest{ Model: "claude-3-7-sonnet-thinking", MaxTokens: &zero, Messages: []dto.ClaudeMessage{ {Role: "user", Content: "hello"}, }, } info := &relaycommon.RelayInfo{ OriginModelName: "claude-3-7-sonnet-thinking", Request: original, ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: "claude-3-7-sonnet-thinking", }, } outbound, err := common.DeepCopy(original) require.NoError(t, err) require.NoError(t, helper.ModelMappedHelper(c, info, outbound)) err = helper.ApplyReasoningModelSuffix(nil, info, outbound) require.NoError(t, err) out, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, outbound) require.NoError(t, err) converted, ok := out.(*dto.ClaudeRequest) require.True(t, ok) assert.Equal(t, "claude-3-7-sonnet", converted.Model) require.NotNil(t, converted.Thinking) require.NotNil(t, converted.MaxTokens) assert.Greater(t, *converted.MaxTokens, uint(1024)) } func TestConvertClaudeRequestDoesNotOverwriteTrimmedUpstreamModelName(t *testing.T) { req := &dto.ClaudeRequest{ Model: "claude-3-7-sonnet-thinking", Messages: []dto.ClaudeMessage{ {Role: "user", Content: "hello"}, }, } info := &relaycommon.RelayInfo{ ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: "claude-3-7-sonnet", }, } _, err := (&Adaptor{}).ConvertClaudeRequest(nil, info, req) require.NoError(t, err) assert.Equal(t, "claude-3-7-sonnet", info.UpstreamModelName) } func TestConvertClaudeRequestForwardsThinkingBlockBinding(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string model string thinking string wantThinking string }{ { name: "native request keeps every thinking field", model: "claude-opus-5-5", thinking: `{"type":"adaptive","display":"summarized","block_binding":{"prefix_mismatch_behavior":"drop_block"},"future_field":{"a":1}}`, wantThinking: `{"type":"adaptive","display":"summarized","block_binding":{"prefix_mismatch_behavior":"drop_block"},"future_field":{"a":1}}`, }, { name: "effort suffix re-renders adaptive and keeps block_binding", model: "claude-opus-5-5-high", thinking: `{"type":"adaptive","block_binding":{"prefix_mismatch_behavior":"drop_block"}}`, wantThinking: `{"type":"adaptive","display":"summarized","block_binding":{"prefix_mismatch_behavior":"drop_block"}}`, }, { name: "suffix rendering between_tools does not carry block_binding", model: "claude-sonnet-5-5-none", thinking: `{"type":"adaptive","block_binding":{"prefix_mismatch_behavior":"drop_block"}}`, wantThinking: `{"type":"between_tools"}`, }, { name: "request without block_binding gets none", model: "claude-opus-5-5-high", thinking: `{"type":"adaptive"}`, wantThinking: `{"type":"adaptive","display":"summarized"}`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) body := `{"model":"` + tt.model + `","max_tokens":1024,"thinking":` + tt.thinking + `,"messages":[{"role":"user","content":"hi"}]}` var original dto.ClaudeRequest require.NoError(t, common.UnmarshalJsonStr(body, &original)) info := &relaycommon.RelayInfo{ OriginModelName: tt.model, Request: &original, ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: tt.model}, } // Same steps as the Claude handler before the adaptor converts. outbound, err := common.DeepCopy(&original) require.NoError(t, err) require.NoError(t, helper.ModelMappedHelper(c, info, outbound)) require.NoError(t, helper.ApplyReasoningModelSuffix(nil, info, outbound)) out, err := (&Adaptor{}).ConvertClaudeRequest(c, info, outbound) require.NoError(t, err) encoded, err := common.Marshal(out) require.NoError(t, err) var upstream struct { Thinking json.RawMessage `json:"thinking"` } require.NoError(t, common.Unmarshal(encoded, &upstream)) assert.JSONEq(t, tt.wantThinking, string(upstream.Thinking)) }) } } func TestSetupRequestHeaderForwardsEveryAnthropicBetaValue(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) c.Request.Header.Add("anthropic-beta", "fast-mode-2026-02-01") c.Request.Header.Add("anthropic-beta", "thinking-binding-controls-2026-08-01") info := &relaycommon.RelayInfo{ OriginModelName: "claude-opus-5-5", ChannelMeta: &relaycommon.ChannelMeta{ApiKey: "sk-test"}, } header := http.Header{} require.NoError(t, (&Adaptor{}).SetupRequestHeader(c, &header, info)) assert.Equal(t, []string{"fast-mode-2026-02-01,thinking-binding-controls-2026-08-01"}, header.Values("anthropic-beta")) } func geminiToClaudeInfo() *relaycommon.RelayInfo { return &relaycommon.RelayInfo{ OriginModelName: "claude-3-7-sonnet", ChannelMeta: &relaycommon.ChannelMeta{ UpstreamModelName: "claude-3-7-sonnet", }, } } func TestConvertGeminiRequestMapsSystemInstructionToolsAndMultimodal(t *testing.T) { req := &dto.GeminiChatRequest{ Contents: []dto.GeminiChatContent{ { Role: "user", Parts: []dto.GeminiPart{ {Text: "What is in this image?"}, {InlineData: &dto.GeminiInlineData{MimeType: "image/png", Data: "aGVsbG8="}}, }, }, }, SystemInstructions: &dto.GeminiChatContent{ Parts: []dto.GeminiPart{{Text: "You are a helpful assistant."}}, }, } req.SetTools([]dto.GeminiChatTool{ { FunctionDeclarations: []dto.FunctionRequest{ { Name: "lookup", Description: "Lookup data", Parameters: map[string]any{ "type": "object", "properties": map[string]any{"q": map[string]any{"type": "string"}}, }, }, }, }, }) out, err := (&Adaptor{}).ConvertGeminiRequest(nil, geminiToClaudeInfo(), req) require.NoError(t, err) converted, ok := out.(*dto.ClaudeRequest) require.True(t, ok) system := converted.ParseSystem() require.NotEmpty(t, system) assert.Contains(t, system[0].GetText(), "You are a helpful assistant.") require.NotEmpty(t, converted.Messages) assert.Equal(t, "user", converted.Messages[0].Role) blocks, parseErr := converted.Messages[0].ParseContent() require.NoError(t, parseErr) var foundImage bool for _, block := range blocks { if block.Type == "image" || (block.Source != nil && block.Source.Type == "base64") { foundImage = true break } } assert.True(t, foundImage) require.NotNil(t, converted.Tools) tools, err := common.Marshal(converted.Tools) require.NoError(t, err) assert.Contains(t, string(tools), `"lookup"`) require.NotNil(t, converted.MaxTokens) assert.Greater(t, *converted.MaxTokens, uint(0)) } func TestConvertGeminiRequestThinkingConfigUsesReasoningIntent(t *testing.T) { budget := 1024 maxTokens := uint(4096) req := &dto.GeminiChatRequest{ Contents: []dto.GeminiChatContent{ {Role: "user", Parts: []dto.GeminiPart{{Text: "think"}}}, }, GenerationConfig: dto.GeminiChatGenerationConfig{ MaxOutputTokens: &maxTokens, ThinkingConfig: &dto.GeminiThinkingConfig{ThinkingBudget: &budget}, }, } out, err := (&Adaptor{}).ConvertGeminiRequest(nil, geminiToClaudeInfo(), req) require.NoError(t, err) converted, ok := out.(*dto.ClaudeRequest) require.True(t, ok) require.NotNil(t, converted.Thinking) assert.Equal(t, "enabled", converted.Thinking.Type) require.NotNil(t, converted.Thinking.BudgetTokens) assert.Equal(t, 1024, *converted.Thinking.BudgetTokens) } func TestConvertGeminiRequestNilRequest(t *testing.T) { _, err := (&Adaptor{}).ConvertGeminiRequest(nil, geminiToClaudeInfo(), nil) require.Error(t, err) }