diff --git a/constant/context_key.go b/constant/context_key.go index 32dd9617..26ff1738 100644 --- a/constant/context_key.go +++ b/constant/context_key.go @@ -40,4 +40,6 @@ const ( ContextKeyUserGroup ContextKey = "user_group" ContextKeyUsingGroup ContextKey = "group" ContextKeyUserName ContextKey = "username" + + ContextKeySystemPromptOverride ContextKey = "system_prompt_override" ) diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 47f8bf95..1c697048 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -6,4 +6,5 @@ type ChannelSettings struct { Proxy string `json:"proxy"` PassThroughBodyEnabled bool `json:"pass_through_body_enabled,omitempty"` SystemPrompt string `json:"system_prompt,omitempty"` + SystemPromptOverride bool `json:"system_prompt_override,omitempty"` } diff --git a/dto/openai_request.go b/dto/openai_request.go index fcd47d07..f33b2c1e 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -78,6 +78,8 @@ func (r *GeneralOpenAIRequest) GetSystemRoleName() string { if !strings.HasPrefix(r.Model, "o1-mini") && !strings.HasPrefix(r.Model, "o1-preview") { return "developer" } + } else if strings.HasPrefix(r.Model, "gpt-5") { + return "developer" } return "system" } diff --git a/middleware/distributor.go b/middleware/distributor.go index e8abcbe9..dea30abf 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -267,6 +267,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode common.SetContextKey(c, constant.ContextKeyChannelKey, key) common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, channel.GetBaseURL()) + common.SetContextKey(c, constant.ContextKeySystemPromptOverride, false) + // TODO: api_version统一 switch channel.Type { case constant.ChannelTypeAzure: diff --git a/relay/relay-text.go b/relay/relay-text.go index f175dbfb..1e014615 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -201,6 +201,26 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) { Content: relayInfo.ChannelSetting.SystemPrompt, } request.Messages = append([]dto.Message{systemMessage}, request.Messages...) + } else if relayInfo.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(relayInfo.ChannelSetting.SystemPrompt + "\n" + message.StringContent()) + } else { + contents := message.ParseContent() + contents = append([]dto.MediaContent{ + { + Type: dto.ContentTypeText, + Text: relayInfo.ChannelSetting.SystemPrompt, + }, + }, contents...) + request.Messages[i].Content = contents + } + break + } + } } } diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 020a2ba9..0dae9a03 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -28,6 +28,12 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m other["is_model_mapped"] = true other["upstream_model_name"] = relayInfo.UpstreamModelName } + + isSystemPromptOverwritten := common.GetContextKeyBool(ctx, constant.ContextKeySystemPromptOverride) + if isSystemPromptOverwritten { + other["is_system_prompt_overwritten"] = true + } + adminInfo := make(map[string]interface{}) adminInfo["use_channel"] = ctx.GetStringSlice("use_channel") isMultiKey := common.GetContextKeyBool(ctx, constant.ContextKeyChannelIsMultiKey) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 40aedcbf..b86aade5 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -131,6 +131,7 @@ const EditChannelModal = (props) => { proxy: '', pass_through_body_enabled: false, system_prompt: '', + system_prompt_override: false, }; const [batch, setBatch] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false); @@ -340,12 +341,15 @@ const EditChannelModal = (props) => { data.proxy = parsedSettings.proxy || ''; data.pass_through_body_enabled = parsedSettings.pass_through_body_enabled || false; data.system_prompt = parsedSettings.system_prompt || ''; + data.system_prompt_override = parsedSettings.system_prompt_override || false; } catch (error) { console.error('解析渠道设置失败:', error); data.force_format = false; data.thinking_to_content = false; data.proxy = ''; data.pass_through_body_enabled = false; + data.system_prompt = ''; + data.system_prompt_override = false; } } else { data.force_format = false; @@ -353,6 +357,7 @@ const EditChannelModal = (props) => { data.proxy = ''; data.pass_through_body_enabled = false; data.system_prompt = ''; + data.system_prompt_override = false; } setInputs(data); @@ -372,6 +377,7 @@ const EditChannelModal = (props) => { proxy: data.proxy, pass_through_body_enabled: data.pass_through_body_enabled, system_prompt: data.system_prompt, + system_prompt_override: data.system_prompt_override || false, }); // console.log(data); } else { @@ -573,6 +579,7 @@ const EditChannelModal = (props) => { proxy: '', pass_through_body_enabled: false, system_prompt: '', + system_prompt_override: false, }); // 重置密钥模式状态 setKeyMode('append'); @@ -721,6 +728,7 @@ const EditChannelModal = (props) => { proxy: localInputs.proxy || '', pass_through_body_enabled: localInputs.pass_through_body_enabled || false, system_prompt: localInputs.system_prompt || '', + system_prompt_override: localInputs.system_prompt_override || false, }; localInputs.setting = JSON.stringify(channelExtraSettings); @@ -730,6 +738,7 @@ const EditChannelModal = (props) => { delete localInputs.proxy; delete localInputs.pass_through_body_enabled; delete localInputs.system_prompt; + delete localInputs.system_prompt_override; let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; @@ -1722,6 +1731,14 @@ const EditChannelModal = (props) => { showClear extraText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} /> + handleChannelSettingsChange('system_prompt_override', value)} + extraText={t('如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面')} + /> diff --git a/web/src/components/table/usage-logs/UsageLogsColumnDefs.js b/web/src/components/table/usage-logs/UsageLogsColumnDefs.js index d4ff1713..250e0be5 100644 --- a/web/src/components/table/usage-logs/UsageLogsColumnDefs.js +++ b/web/src/components/table/usage-logs/UsageLogsColumnDefs.js @@ -34,7 +34,6 @@ import { getLogOther, renderModelTag, renderClaudeLogContent, - renderClaudeModelPriceSimple, renderLogContent, renderModelPriceSimple, renderAudioModelPrice, @@ -538,7 +537,7 @@ export const getLogsColumns = ({ ); } let content = other?.claude - ? renderClaudeModelPriceSimple( + ? renderModelPriceSimple( other.model_ratio, other.model_price, other.group_ratio, @@ -547,6 +546,10 @@ export const getLogsColumns = ({ other.cache_ratio || 1.0, other.cache_creation_tokens || 0, other.cache_creation_ratio || 1.0, + false, + 1.0, + other?.is_system_prompt_overwritten, + 'claude' ) : renderModelPriceSimple( other.model_ratio, @@ -555,13 +558,19 @@ export const getLogsColumns = ({ other?.user_group_ratio, other.cache_tokens || 0, other.cache_ratio || 1.0, + 0, + 1.0, + false, + 1.0, + other?.is_system_prompt_overwritten, + 'openai' ); return ( {content} diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 1186c19f..4a1c3b9e 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -953,6 +953,71 @@ function getEffectiveRatio(groupRatio, user_group_ratio) { }; } +// Shared core for simple price rendering (used by OpenAI-like and Claude-like variants) +function renderPriceSimpleCore({ + modelRatio, + modelPrice = -1, + groupRatio, + user_group_ratio, + cacheTokens = 0, + cacheRatio = 1.0, + cacheCreationTokens = 0, + cacheCreationRatio = 1.0, + image = false, + imageRatio = 1.0, + isSystemPromptOverride = false +}) { + const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( + groupRatio, + user_group_ratio, + ); + const finalGroupRatio = effectiveGroupRatio; + + if (modelPrice !== -1) { + return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { + price: modelPrice, + ratioType: ratioLabel, + ratio: finalGroupRatio, + }); + } + + const parts = []; + // base: model ratio + parts.push(i18next.t('模型: {{ratio}}')); + + // cache part (label differs when with image) + if (cacheTokens !== 0) { + parts.push(i18next.t('缓存: {{cacheRatio}}')); + } + + // cache creation part (Claude specific if passed) + if (cacheCreationTokens !== 0) { + parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}')); + } + + // image part + if (image) { + parts.push(i18next.t('图片输入: {{imageRatio}}')); + } + + parts.push(`{{ratioType}}: {{groupRatio}}`); + + let result = i18next.t(parts.join(' * '), { + ratio: modelRatio, + ratioType: ratioLabel, + groupRatio: finalGroupRatio, + cacheRatio: cacheRatio, + cacheCreationRatio: cacheCreationRatio, + imageRatio: imageRatio, + }) + + if (isSystemPromptOverride) { + result += '\n\r' + i18next.t('系统提示覆盖'); + } + + return result; +} + export function renderModelPrice( inputTokens, completionTokens, @@ -1245,56 +1310,26 @@ export function renderModelPriceSimple( user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, + cacheCreationTokens = 0, + cacheCreationRatio = 1.0, image = false, imageRatio = 1.0, + isSystemPromptOverride = false, + provider = 'openai', ) { - const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio); - groupRatio = effectiveGroupRatio; - if (modelPrice !== -1) { - return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { - price: modelPrice, - ratioType: ratioLabel, - ratio: groupRatio, - }); - } else { - if (image && cacheTokens !== 0) { - return i18next.t( - '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}', - { - ratio: modelRatio, - ratioType: ratioLabel, - groupRatio: groupRatio, - cacheRatio: cacheRatio, - imageRatio: imageRatio, - }, - ); - } else if (image) { - return i18next.t( - '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}', - { - ratio: modelRatio, - ratioType: ratioLabel, - groupRatio: groupRatio, - imageRatio: imageRatio, - }, - ); - } else if (cacheTokens !== 0) { - return i18next.t( - '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', - { - ratio: modelRatio, - groupRatio: groupRatio, - cacheRatio: cacheRatio, - }, - ); - } else { - return i18next.t('模型: {{ratio}} * {{ratioType}}:{{groupRatio}}', { - ratio: modelRatio, - ratioType: ratioLabel, - groupRatio: groupRatio, - }); - } - } + return renderPriceSimpleCore({ + modelRatio, + modelPrice, + groupRatio, + user_group_ratio, + cacheTokens, + cacheRatio, + cacheCreationTokens, + cacheCreationRatio, + image, + imageRatio, + isSystemPromptOverride + }); } export function renderAudioModelPrice( @@ -1635,46 +1670,7 @@ export function renderClaudeLogContent( } } -export function renderClaudeModelPriceSimple( - modelRatio, - modelPrice = -1, - groupRatio, - user_group_ratio, - cacheTokens = 0, - cacheRatio = 1.0, - cacheCreationTokens = 0, - cacheCreationRatio = 1.0, -) { - const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio); - groupRatio = effectiveGroupRatio; - - if (modelPrice !== -1) { - return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { - price: modelPrice, - ratioType: ratioLabel, - ratio: groupRatio, - }); - } else { - if (cacheTokens !== 0 || cacheCreationTokens !== 0) { - return i18next.t( - '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}', - { - ratio: modelRatio, - ratioType: ratioLabel, - groupRatio: groupRatio, - cacheRatio: cacheRatio, - cacheCreationRatio: cacheCreationRatio, - }, - ); - } else { - return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', { - ratio: modelRatio, - ratioType: ratioLabel, - groupRatio: groupRatio, - }); - } - } -} +// 已统一至 renderModelPriceSimple,若仍有遗留引用,请改为传入 provider='claude' /** * rehype 插件:将段落等文本节点拆分为逐词 ,并添加淡入动画 class。 diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b965c192..f732f68b 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1804,5 +1804,11 @@ "已选择 {{selected}} / {{total}}": "Selected {{selected}} / {{total}}", "新获取的模型": "New models", "已有的模型": "Existing models", - "搜索模型": "Search models" + "搜索模型": "Search models", + "缓存: {{cacheRatio}}": "Cache: {{cacheRatio}}", + "缓存创建: {{cacheCreationRatio}}": "Cache creation: {{cacheCreationRatio}}", + "图片输入: {{imageRatio}}": "Image input: {{imageRatio}}", + "系统提示覆盖": "System prompt override", + "模型: {{ratio}}": "Model: {{ratio}}", + "专属倍率": "Exclusive group ratio" } \ No newline at end of file