diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 527363205a1f..b61601ae15fd 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -108,28 +107,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ } if info.ChannelSetting.SystemPrompt != "" { - if request.System == nil { - request.SetStringSystem(info.ChannelSetting.SystemPrompt) - } else if info.ChannelSetting.SystemPromptOverride { - common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) - if request.IsStringSystem() { - existing := strings.TrimSpace(request.GetStringSystem()) - if existing == "" { - request.SetStringSystem(info.ChannelSetting.SystemPrompt) - } else { - request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing) - } - } else { - systemContents := request.ParseSystem() - newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText} - newSystem.SetText(info.ChannelSetting.SystemPrompt) - if len(systemContents) == 0 { - request.System = []dto.ClaudeMediaMessage{newSystem} - } else { - request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...) - } - } - } + applySystemPromptToClaudeRequest(c, info, request) } if !model_setting.GetGlobalSettings().PassThroughRequestEnabled && diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index a68cfe730f60..13232b5b82a9 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -113,44 +113,18 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types relaycommon.AppendRequestConversionFromRequest(info, convertedRequest) if info.ChannelSetting.SystemPrompt != "" { - // 如果有系统提示,则将其添加到请求中 - request, ok := convertedRequest.(*dto.GeneralOpenAIRequest) - if ok { - containSystemPrompt := false - for _, message := range request.Messages { - if message.Role == request.GetSystemRoleName() { - containSystemPrompt = true - break - } - } - if !containSystemPrompt { - // 如果没有系统提示,则添加系统提示 - systemMessage := dto.Message{ - Role: request.GetSystemRoleName(), - Content: info.ChannelSetting.SystemPrompt, - } - request.Messages = append([]dto.Message{systemMessage}, request.Messages...) - } else if info.ChannelSetting.SystemPromptOverride { - common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) - // 如果有系统提示,且允许覆盖,则拼接到前面 - for i, message := range request.Messages { - if message.Role == request.GetSystemRoleName() { - if message.IsStringContent() { - request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent()) - } else { - contents := message.ParseContent() - contents = append([]dto.MediaContent{ - { - Type: dto.ContentTypeText, - Text: info.ChannelSetting.SystemPrompt, - }, - }, contents...) - request.Messages[i].Content = contents - } - break - } - } - } + // Channel system prompt injection must cover every relay format the + // adaptor may return (OpenAI / Claude / Gemini). Adaptors such as the + // Anthropic one (channel type=14) convert the request into a + // *dto.ClaudeRequest; only matching *dto.GeneralOpenAIRequest would + // silently skip the configured system prompt for those channels. + switch r := convertedRequest.(type) { + case *dto.GeneralOpenAIRequest: + applySystemPromptToOpenAIRequest(c, info, r) + case *dto.ClaudeRequest: + applySystemPromptToClaudeRequest(c, info, r) + case *dto.GeminiChatRequest: + applySystemPromptToGeminiRequest(c, info, r) } } diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 8f64552a696d..b4384dbead16 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/relay/channel/gemini" @@ -96,29 +95,7 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ adaptor.Init(info) if info.ChannelSetting.SystemPrompt != "" { - if request.SystemInstructions == nil { - request.SystemInstructions = &dto.GeminiChatContent{ - Parts: []dto.GeminiPart{ - {Text: info.ChannelSetting.SystemPrompt}, - }, - } - } else if len(request.SystemInstructions.Parts) == 0 { - request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}} - } else if info.ChannelSetting.SystemPromptOverride { - common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) - merged := false - for i := range request.SystemInstructions.Parts { - if request.SystemInstructions.Parts[i].Text == "" { - continue - } - request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text - merged = true - break - } - if !merged { - request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...) - } - } + applySystemPromptToGeminiRequest(c, info, request) } // Clean up empty system instruction diff --git a/relay/system_prompt_inject.go b/relay/system_prompt_inject.go new file mode 100644 index 000000000000..a1029ad73554 --- /dev/null +++ b/relay/system_prompt_inject.go @@ -0,0 +1,128 @@ +package relay + +import ( + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + + "github.com/gin-gonic/gin" +) + +// applySystemPromptToOpenAIRequest injects the channel-level system prompt into an +// OpenAI-format request. When the request already contains a system message and +// SystemPromptOverride is disabled, the existing system message is left untouched. +func applySystemPromptToOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) { + if request == nil { + return + } + systemRole := request.GetSystemRoleName() + containSystemPrompt := false + for _, message := range request.Messages { + if message.Role == systemRole { + containSystemPrompt = true + break + } + } + if !containSystemPrompt { + systemMessage := dto.Message{ + Role: systemRole, + Content: info.ChannelSetting.SystemPrompt, + } + request.Messages = append([]dto.Message{systemMessage}, request.Messages...) + return + } + if !info.ChannelSetting.SystemPromptOverride { + return + } + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) + for i, message := range request.Messages { + if message.Role != systemRole { + continue + } + if message.IsStringContent() { + request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent()) + } else { + contents := message.ParseContent() + contents = append([]dto.MediaContent{ + { + Type: dto.ContentTypeText, + Text: info.ChannelSetting.SystemPrompt, + }, + }, contents...) + request.Messages[i].Content = contents + } + break + } +} + +// applySystemPromptToClaudeRequest injects the channel-level system prompt into a +// Claude-format request. Mirrors the behavior previously inlined in ClaudeHelper. +func applySystemPromptToClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) { + if request == nil { + return + } + if request.System == nil { + request.SetStringSystem(info.ChannelSetting.SystemPrompt) + return + } + if !info.ChannelSetting.SystemPromptOverride { + return + } + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) + if request.IsStringSystem() { + existing := strings.TrimSpace(request.GetStringSystem()) + if existing == "" { + request.SetStringSystem(info.ChannelSetting.SystemPrompt) + } else { + request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing) + } + return + } + systemContents := request.ParseSystem() + newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText} + newSystem.SetText(info.ChannelSetting.SystemPrompt) + if len(systemContents) == 0 { + request.System = []dto.ClaudeMediaMessage{newSystem} + } else { + request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...) + } +} + +// applySystemPromptToGeminiRequest injects the channel-level system prompt into a +// Gemini-format request. Mirrors the behavior previously inlined in GeminiHelper. +func applySystemPromptToGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) { + if request == nil { + return + } + if request.SystemInstructions == nil { + request.SystemInstructions = &dto.GeminiChatContent{ + Parts: []dto.GeminiPart{ + {Text: info.ChannelSetting.SystemPrompt}, + }, + } + return + } + if len(request.SystemInstructions.Parts) == 0 { + request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}} + return + } + if !info.ChannelSetting.SystemPromptOverride { + return + } + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) + merged := false + for i := range request.SystemInstructions.Parts { + if request.SystemInstructions.Parts[i].Text == "" { + continue + } + request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text + merged = true + break + } + if !merged { + request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...) + } +} diff --git a/relay/system_prompt_inject_test.go b/relay/system_prompt_inject_test.go new file mode 100644 index 000000000000..fe6c7888d911 --- /dev/null +++ b/relay/system_prompt_inject_test.go @@ -0,0 +1,76 @@ +package relay + +import ( + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newRelayInfoWithSystemPrompt(systemPrompt string, override bool) *relaycommon.RelayInfo { + info := &relaycommon.RelayInfo{} + info.ChannelSetting.SystemPrompt = systemPrompt + info.ChannelSetting.SystemPromptOverride = override + return info +} + +func newTestGinContext() *gin.Context { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + return c +} + +func TestApplySystemPromptToClaudeRequest_InjectsWhenAbsent(t *testing.T) { + info := newRelayInfoWithSystemPrompt("hide model info", false) + request := &dto.ClaudeRequest{System: nil} + applySystemPromptToClaudeRequest(newTestGinContext(), info, request) + require.True(t, request.IsStringSystem()) + assert.Equal(t, "hide model info", request.GetStringSystem()) +} + +func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) { + info := newRelayInfoWithSystemPrompt("hide model info", false) + request := &dto.ClaudeRequest{System: "keep me"} + applySystemPromptToClaudeRequest(newTestGinContext(), info, request) + require.True(t, request.IsStringSystem()) + assert.Equal(t, "keep me", request.GetStringSystem()) + assert.False(t, common.GetContextKeyBool(newTestGinContext(), constant.ContextKeySystemPromptOverride)) +} + +func TestApplySystemPromptToClaudeRequest_OverrideStringPrepends(t *testing.T) { + c := newTestGinContext() + info := newRelayInfoWithSystemPrompt("hide model info", true) + request := &dto.ClaudeRequest{System: "keep me"} + applySystemPromptToClaudeRequest(c, info, request) + require.True(t, request.IsStringSystem()) + assert.Equal(t, "hide model info\nkeep me", request.GetStringSystem()) + assert.True(t, common.GetContextKeyBool(c, constant.ContextKeySystemPromptOverride)) +} + +func TestApplySystemPromptToOpenAIRequest_InjectsWhenAbsent(t *testing.T) { + info := newRelayInfoWithSystemPrompt("hide model info", false) + request := &dto.GeneralOpenAIRequest{ + Model: "gpt-test", + Messages: []dto.Message{ + {Role: "user", Content: "hi"}, + }, + } + applySystemPromptToOpenAIRequest(newTestGinContext(), info, request) + require.Len(t, request.Messages, 2) + assert.Equal(t, "system", request.Messages[0].Role) + assert.Equal(t, "hide model info", request.Messages[0].StringContent()) +} + +func TestApplySystemPromptToGeminiRequest_InjectsWhenAbsent(t *testing.T) { + info := newRelayInfoWithSystemPrompt("hide model info", false) + request := &dto.GeminiChatRequest{} + applySystemPromptToGeminiRequest(newTestGinContext(), info, request) + require.NotNil(t, request.SystemInstructions) + require.Len(t, request.SystemInstructions.Parts, 1) + assert.Equal(t, "hide model info", request.SystemInstructions.Parts[0].Text) +}