Skip to content
Closed
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
4 changes: 4 additions & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const (

ContextKeySystemPromptOverride ContextKey = "system_prompt_override"

// ContextKeyModelSystemPrompt stores model-level system prompt (JSON: {"en":"...", "zh":"..."})
ContextKeyModelSystemPrompt ContextKey = "model_system_prompt"
ContextKeyModelSystemPromptMode ContextKey = "model_system_prompt_mode"

// ContextKeyFileSourcesToCleanup stores file sources that need cleanup when request ends
ContextKeyFileSourcesToCleanup ContextKey = "file_sources_to_cleanup"

Expand Down
3 changes: 3 additions & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/model"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
relayhelper "github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
Expand Down Expand Up @@ -501,6 +502,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
case constant.ChannelTypeCoze:
c.Set("bot_id", channel.Other)
}
// 设置模型级系统提示词(优先级低于渠道级)
relayhelper.SetupModelSystemPrompt(c, modelName)
return nil
}

Expand Down
39 changes: 26 additions & 13 deletions model/model_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,20 @@ type BoundChannel struct {
}

type Model struct {
Id int `json:"id"`
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name_delete_at,priority:1"`
Description string `json:"description,omitempty" gorm:"type:text"`
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
Status int `json:"status" gorm:"default:1"`
SyncOfficial int `json:"sync_official" gorm:"default:1"`
CreatedTime int64 `json:"created_time" gorm:"bigint"`
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_model_name_delete_at,priority:2"`
Id int `json:"id"`
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name_delete_at,priority:1"`
Description string `json:"description,omitempty" gorm:"type:text"`
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
Status int `json:"status" gorm:"default:1"`
SyncOfficial int `json:"sync_official" gorm:"default:1"`
SystemPrompt string `json:"system_prompt,omitempty" gorm:"type:text"`
SystemPromptMode int `json:"system_prompt_mode" gorm:"default:0"`
CreatedTime int64 `json:"created_time" gorm:"bigint"`
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_model_name_delete_at,priority:2"`

BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
Expand Down Expand Up @@ -78,7 +80,7 @@ func (mi *Model) Update() error {
mi.UpdatedTime = common.GetTimestamp()
// 使用 Select 强制更新所有字段,包括零值
return DB.Model(&Model{}).Where("id = ?", mi.Id).
Select("model_name", "description", "icon", "tags", "vendor_id", "endpoints", "status", "sync_official", "name_rule", "updated_time").
Select("model_name", "description", "icon", "tags", "vendor_id", "endpoints", "status", "sync_official", "system_prompt", "system_prompt_mode", "name_rule", "updated_time").
Updates(mi).Error
}

Expand Down Expand Up @@ -215,3 +217,14 @@ func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Mode
}
return models, total, nil
}

// GetModelSystemPrompt 根据模型名查找系统提示词配置
// 返回 systemPrompt(可能为多语言JSON: {"en":"...", "zh":"..."})和 mode(0=禁用,1=注入,2=覆写,3=附加)
func GetModelSystemPrompt(modelName string) (systemPrompt string, mode int) {
var m Model
err := DB.Where("model_name = ? AND status = ?", modelName, 1).First(&m).Error
if err != nil || m.SystemPrompt == "" || m.SystemPromptMode == 0 {
return "", 0
}
return m.SystemPrompt, m.SystemPromptMode
}
Comment on lines +220 to +230

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Consider logging unexpected database errors in GetModelSystemPrompt.

The function returns "", 0 on any err != nil, which is a safe fallback. However, non-ErrRecordNotFound errors (e.g., connection failures) are silently swallowed with no log trail, making it difficult to diagnose why a model prompt isn't applying in production.

🛡️ Suggested improvement
 func GetModelSystemPrompt(modelName string) (systemPrompt string, mode int) {
 	var m Model
 	err := DB.Where("model_name = ? AND status = ?", modelName, 1).First(&m).Error
-	if err != nil || m.SystemPrompt == "" || m.SystemPromptMode == 0 {
+	if err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			logger.SysError("failed to query model system prompt for %s: %s", modelName, err.Error())
+		}
+		return "", 0
+	}
+	if m.SystemPrompt == "" || m.SystemPromptMode == 0 {
 		return "", 0
 	}
 	return m.SystemPrompt, m.SystemPromptMode
 }
📝 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
// GetModelSystemPrompt 根据模型名查找系统提示词配置
// 返回 systemPrompt(可能为多语言JSON: {"en":"...", "zh":"..."})和 mode(0=禁用,1=注入,2=覆写,3=附加)
func GetModelSystemPrompt(modelName string) (systemPrompt string, mode int) {
var m Model
err := DB.Where("model_name = ? AND status = ?", modelName, 1).First(&m).Error
if err != nil || m.SystemPrompt == "" || m.SystemPromptMode == 0 {
return "", 0
}
return m.SystemPrompt, m.SystemPromptMode
}
// GetModelSystemPrompt 根据模型名查找系统提示词配置
// 返回 systemPrompt(可能为多语言JSON: {"en":"...", "zh":"..."})和 mode(0=禁用,1=注入,2=覆写,3=附加)
func GetModelSystemPrompt(modelName string) (systemPrompt string, mode int) {
var m Model
err := DB.Where("model_name = ? AND status = ?", modelName, 1).First(&m).Error
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
logger.SysError("failed to query model system prompt for %s: %s", modelName, err.Error())
}
return "", 0
}
if m.SystemPrompt == "" || m.SystemPromptMode == 0 {
return "", 0
}
return m.SystemPrompt, m.SystemPromptMode
}
🤖 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 `@model/model_meta.go` around lines 220 - 230, Update GetModelSystemPrompt to
distinguish gorm.ErrRecordNotFound from other database errors; retain the silent
empty fallback for missing records, but log unexpected query failures before
returning "", 0 using the project’s established logging mechanism.

3 changes: 3 additions & 0 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
info.UpstreamModelName = request.Model
}

// 模型级系统提示词(在渠道级之前)
helper.ApplyModelSystemPromptToClaude(c, request)

if info.ChannelSetting.SystemPrompt != "" {
if request.System == nil {
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
Expand Down
7 changes: 7 additions & 0 deletions relay/compatible_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
!passThroughGlobal &&
!info.ChannelSetting.PassThroughBodyEnabled &&
service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) {
// 先应用模型级系统提示词
helper.ApplyModelSystemPromptToOpenAI(c, request)
applySystemPromptIfNeeded(c, info, request)
usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request)
if newApiErr != nil {
Expand Down Expand Up @@ -112,6 +114,11 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
}
relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)

// 先应用模型级系统提示词
if request, ok := convertedRequest.(*dto.GeneralOpenAIRequest); ok {
helper.ApplyModelSystemPromptToOpenAI(c, request)
}

if info.ChannelSetting.SystemPrompt != "" {
// 如果有系统提示,则将其添加到请求中
request, ok := convertedRequest.(*dto.GeneralOpenAIRequest)
Expand Down
3 changes: 3 additions & 0 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ

adaptor.Init(info)

// 模型级系统提示词(在渠道级之前)
helper.ApplyModelSystemPromptToGemini(c, request)

if info.ChannelSetting.SystemPrompt != "" {
if request.SystemInstructions == nil {
request.SystemInstructions = &dto.GeminiChatContent{
Expand Down
237 changes: 237 additions & 0 deletions relay/helper/system_prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package helper

import (
"encoding/json"
"strings"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"

"github.com/gin-gonic/gin"
)

// modelSystemPromptMode values
const (
ModelSystemPromptModeDisabled = iota // 0: 禁用
ModelSystemPromptModeInject // 1: 注入(只有无system消息时才注入)
ModelSystemPromptModeOverride // 2: 覆写(始终替换原有system消息)
ModelSystemPromptModeAppend // 3: 附加(拼接到原有system消息前面)
)

// resolveModelSystemPrompt 解析多语言系统提示词,按语言匹配
// systemPromptJSON 格式: {"en":"Hello", "zh":"你好"}
func resolveModelSystemPrompt(systemPromptJSON, language string) string {
if systemPromptJSON == "" {
return ""
}
// 尝试作为 JSON 解析多语言消息
var jsonValue map[string]string
if err := json.Unmarshal([]byte(systemPromptJSON), &jsonValue); err != nil || len(jsonValue) == 0 {
// 不是 JSON,直接作为纯文本返回
return systemPromptJSON
}
// 尝试匹配用户语言
if text, ok := jsonValue[language]; ok && text != "" {
return text
}
// 回退到 en
if text, ok := jsonValue["en"]; ok && text != "" {
return text
}
// 回退到 zh
if text, ok := jsonValue["zh"]; ok && text != "" {
return text
}
// 回退到第一个非空值
for _, text := range jsonValue {
if text != "" {
return text
}
}
return ""
}

// GetResolvedModelSystemPrompt 从请求上下文获取已解析的模型系统提示词
func GetResolvedModelSystemPrompt(c *gin.Context) string {
systemPromptJSON := c.GetString(string(constant.ContextKeyModelSystemPrompt))
if systemPromptJSON == "" {
return ""
}
language := c.GetString(string(constant.ContextKeyLanguage))
if language == "" {
language = "en"
}
return resolveModelSystemPrompt(systemPromptJSON, language)
}

// getModelSystemPromptMode 获取模型系统提示词的匹配模式
func getModelSystemPromptMode(c *gin.Context) int {
mode := c.GetInt(string(constant.ContextKeyModelSystemPromptMode))
return mode
}

// SetupModelSystemPrompt 在分发阶段设置模型系统提示词上下文
func SetupModelSystemPrompt(c *gin.Context, modelName string) {
systemPrompt, mode := model.GetModelSystemPrompt(modelName)
if systemPrompt == "" || mode == ModelSystemPromptModeDisabled {
return
}
c.Set(string(constant.ContextKeyModelSystemPrompt), systemPrompt)
c.Set(string(constant.ContextKeyModelSystemPromptMode), mode)
}

// ApplyModelSystemPromptToOpenAI 对 OpenAI 格式请求应用模型级系统提示词
func ApplyModelSystemPromptToOpenAI(c *gin.Context, request *dto.GeneralOpenAIRequest) {
mode := getModelSystemPromptMode(c)
if mode == ModelSystemPromptModeDisabled {
return
}
systemPrompt := GetResolvedModelSystemPrompt(c)
if systemPrompt == "" {
return
}

systemRole := request.GetSystemRoleName()
containSystemPrompt := false
for _, message := range request.Messages {
if message.Role == systemRole {
containSystemPrompt = true
break
}
}

if !containSystemPrompt {
// 无 system 消息时,注入新的
systemMessage := dto.Message{
Role: systemRole,
Content: systemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
return
}

// 已有 system 消息,根据模式处理
switch mode {
case ModelSystemPromptModeInject:
// 注入模式:已有则不处理
return
case ModelSystemPromptModeOverride:
// 覆写模式:替换原有内容
for i, message := range request.Messages {
if message.Role == systemRole {
request.Messages[i].SetStringContent(systemPrompt)
return
}
}
case ModelSystemPromptModeAppend:
// 附加模式:拼接到原有内容前面
for i, message := range request.Messages {
if message.Role == systemRole {
if message.IsStringContent() {
existing := strings.TrimSpace(message.StringContent())
if existing == "" {
request.Messages[i].SetStringContent(systemPrompt)
} else {
request.Messages[i].SetStringContent(systemPrompt + "\n" + existing)
}
} else {
contents := message.ParseContent()
contents = append([]dto.MediaContent{
{Type: dto.ContentTypeText, Text: systemPrompt},
}, contents...)
request.Messages[i].Content = contents
}
return
}
}
}
}

// ApplyModelSystemPromptToGemini 对 Gemini 格式请求应用模型级系统提示词
func ApplyModelSystemPromptToGemini(c *gin.Context, request *dto.GeminiChatRequest) {
mode := getModelSystemPromptMode(c)
if mode == ModelSystemPromptModeDisabled {
return
}
systemPrompt := GetResolvedModelSystemPrompt(c)
if systemPrompt == "" {
return
}

switch mode {
case ModelSystemPromptModeInject:
if request.SystemInstructions == nil {
request.SystemInstructions = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{{Text: systemPrompt}},
}
}
case ModelSystemPromptModeOverride:
request.SystemInstructions = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{{Text: systemPrompt}},
}
case ModelSystemPromptModeAppend:
if request.SystemInstructions == nil || len(request.SystemInstructions.Parts) == 0 {
request.SystemInstructions = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{{Text: systemPrompt}},
}
} else {
merged := false
for i := range request.SystemInstructions.Parts {
if request.SystemInstructions.Parts[i].Text == "" {
continue
}
request.SystemInstructions.Parts[i].Text = systemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
merged = true
break
}
if !merged {
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: systemPrompt}}, request.SystemInstructions.Parts...)
}
}
}
}

// ApplyModelSystemPromptToClaude 对 Claude 格式请求应用模型级系统提示词
func ApplyModelSystemPromptToClaude(c *gin.Context, request *dto.ClaudeRequest) {
mode := getModelSystemPromptMode(c)
if mode == ModelSystemPromptModeDisabled {
return
}
systemPrompt := GetResolvedModelSystemPrompt(c)
if systemPrompt == "" {
return
}

switch mode {
case ModelSystemPromptModeInject:
// 注入模式:只在无 system 时注入
if request.System == nil {
request.SetStringSystem(systemPrompt)
}
case ModelSystemPromptModeOverride:
// 覆写模式:始终替换
request.SetStringSystem(systemPrompt)
case ModelSystemPromptModeAppend:
// 附加模式:拼接到原有内容前面
if request.System == nil {
request.SetStringSystem(systemPrompt)
} else if request.IsStringSystem() {
existing := strings.TrimSpace(request.GetStringSystem())
if existing == "" {
request.SetStringSystem(systemPrompt)
} else {
request.SetStringSystem(systemPrompt + "\n" + existing)
}
} else {
systemContents := request.ParseSystem()
newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText}
newSystem.SetText(systemPrompt)
if len(systemContents) == 0 {
request.System = []dto.ClaudeMediaMessage{newSystem}
} else {
request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...)
}
}
}
}
Loading