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
24 changes: 1 addition & 23 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 &&
Expand Down
50 changes: 12 additions & 38 deletions relay/compatible_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
25 changes: 1 addition & 24 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
128 changes: 128 additions & 0 deletions relay/system_prompt_inject.go
Original file line number Diff line number Diff line change
@@ -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...)
}
}
76 changes: 76 additions & 0 deletions relay/system_prompt_inject_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Comment on lines +36 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assertion checks the wrong context instance.

Line 42 creates a brand-new newTestGinContext() instead of reusing the c passed to applySystemPromptToClaudeRequest at line 39. Since each call to gin.CreateTestContext() returns an independent context, this assertion always evaluates against an untouched context and will pass regardless of whether the override key was incorrectly set on the real context — it doesn't actually verify the "no override" invariant.

As per path instructions, "Backend tests must protect real behavior, API contracts, billing/accounting invariants, data compatibility, or regression paths."

🐛 Proposed fix
 func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) {
+	c := newTestGinContext()
 	info := newRelayInfoWithSystemPrompt("hide model info", false)
 	request := &dto.ClaudeRequest{System: "keep me"}
-	applySystemPromptToClaudeRequest(newTestGinContext(), info, request)
+	applySystemPromptToClaudeRequest(c, info, request)
 	require.True(t, request.IsStringSystem())
 	assert.Equal(t, "keep me", request.GetStringSystem())
-	assert.False(t, common.GetContextKeyBool(newTestGinContext(), constant.ContextKeySystemPromptOverride))
+	assert.False(t, common.GetContextKeyBool(c, constant.ContextKeySystemPromptOverride))
 }
📝 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
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_NoOverrideKeepsExisting(t *testing.T) {
c := newTestGinContext()
info := newRelayInfoWithSystemPrompt("hide model info", false)
request := &dto.ClaudeRequest{System: "keep me"}
applySystemPromptToClaudeRequest(c, info, request)
require.True(t, request.IsStringSystem())
assert.Equal(t, "keep me", request.GetStringSystem())
assert.False(t, common.GetContextKeyBool(c, constant.ContextKeySystemPromptOverride))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/system_prompt_inject_test.go` around lines 36 - 43, The no-override
test is checking a fresh Gin context instead of the one passed into
applySystemPromptToClaudeRequest, so it never verifies the real
ContextKeySystemPromptOverride state. Reuse the same context variable created
for the call (the one passed to applySystemPromptToClaudeRequest) and assert the
override key on that instance, using the existing helper methods like
common.GetContextKeyBool to confirm the key was not set.

Source: Path instructions


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)
}