diff --git a/docs/channel/other_setting.md b/docs/channel/other_setting.md index 43341660b886..c443e1fc1498 100644 --- a/docs/channel/other_setting.md +++ b/docs/channel/other_setting.md @@ -1,33 +1,26 @@ -# 渠道而外设置说明 +# 渠道额外设置说明 -该配置用于设置一些额外的渠道参数,可以通过 JSON 对象进行配置。主要包含以下两个设置项: +该配置用于设置一些额外的渠道参数,可以通过前端页面进行配置。主要包含以下设置项: -1. force_format +1. 强制格式化 - 用于标识是否对数据进行强制格式化为 OpenAI 格式 - - 类型为布尔值,设置为 true 时启用强制格式化 -2. proxy - - 用于配置网络代理 - - 类型为字符串,填写代理地址(例如 socks5 协议的代理地址) -3. thinking_to_content - - 用于标识是否将思考内容`reasoning_content`转换为``标签拼接到内容中返回 - - 类型为布尔值,设置为 true 时启用思考内容转换 +2. 思考内容转换 + - 用于标识是否将思考内容`reasoning_content`转换为``标签拼接到内容中返回 --------------------------------------------------------------- +3. 透传请求体 + - 用于将自定义请求体发送到上游 -## JSON 格式示例 +4. 代理地址 + - 用于配置网络代理,需要填写代理地址(支持socks5协议) -以下是一个示例配置,启用强制格式化并设置了代理地址: +5. 系统提示词 + - 输入系统提示词,用户的系统提示词将优先于此设置 + - 用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置 -```json -{ - "force_format": true, - "thinking_to_content": true, - "proxy": "socks5://xxxxxxx" -} -``` +6. 系统提示词拼接 + - 如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面 --------------------------------------------------------------- - -通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。 +7. add_think_first + - 在通过vllm和sglang自部署模型使用一些自带`\n`的`chat_template`时,将`\n`标签拼接到响应的开头 diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 2c58795cb23c..d30f3729ced4 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -7,6 +7,7 @@ type ChannelSettings struct { PassThroughBodyEnabled bool `json:"pass_through_body_enabled,omitempty"` SystemPrompt string `json:"system_prompt,omitempty"` SystemPromptOverride bool `json:"system_prompt_override,omitempty"` + AddThinkFirst bool `json:"add_think_first,omitempty"` } type ChannelOtherSettings struct { diff --git a/i18n/zh-cn.json b/i18n/zh-cn.json index dc7a1e4cc226..93f28c1f4e10 100644 --- a/i18n/zh-cn.json +++ b/i18n/zh-cn.json @@ -598,6 +598,8 @@ "系统提示词": "系统提示词", "输入系统提示词,用户的系统提示词将优先于此设置": "输入系统提示词,用户的系统提示词将优先于此设置", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置", + "开头补充": "开头补充", + "将\\n拼接到响应的开头(只适用于OpenAI渠道类型)": "将\\n拼接到响应的开头(只适用于OpenAI渠道类型)", "参数覆盖": "参数覆盖", "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:", "请输入组织org-xxx": "请输入组织org-xxx", diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index c54eb5b6b030..de7fba82ff60 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -873,7 +873,7 @@ func handleStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCom if err != nil { return fmt.Errorf("failed to marshal stream response: %w", err) } - err = openai.HandleStreamFormat(c, info, string(streamData), info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) + err = openai.HandleStreamFormat(c, info, string(streamData), info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent, false) if err != nil { return fmt.Errorf("failed to handle stream format: %w", err) } diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index e84f6cc4a91b..88001868ba2a 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -18,12 +18,12 @@ import ( ) // 辅助函数 -func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { +func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool, addThink bool) error { info.SendResponseCount++ switch info.RelayFormat { case types.RelayFormatOpenAI: - return sendStreamData(c, info, data, forceFormat, thinkToContent) + return sendStreamData(c, info, data, forceFormat, thinkToContent, addThink) case types.RelayFormatClaude: return handleClaudeFormat(c, data, info) case types.RelayFormatGemini: diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 00dde46d628a..50479d10aa0e 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -26,12 +26,12 @@ import ( "github.com/pkg/errors" ) -func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { +func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool, addThink bool) error { if data == "" { return nil } - if !forceFormat && !thinkToContent { + if !forceFormat && !thinkToContent && !addThink { return helper.StringData(c, data) } @@ -40,6 +40,17 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo return err } + if addThink { + for i := range lastStreamResponse.Choices { + var content string + if lastStreamResponse.Choices[i].Delta.Content != nil { + content = *lastStreamResponse.Choices[i].Delta.Content + } + newContent := "\n" + content + lastStreamResponse.Choices[i].Delta.Content = &newContent + } + } + if !thinkToContent { return helper.ObjectData(c, lastStreamResponse) } @@ -125,10 +136,19 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re var usage = &dto.Usage{} var streamItems []string // store stream items var lastStreamData string + var addThinkFirst bool + + if info.ChannelSetting.AddThinkFirst { + addThinkFirst = true + } + var addThink bool + var firstChunk = true helper.StreamScannerHandler(c, resp, info, func(data string) bool { if lastStreamData != "" { - err := HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) + addThink = firstChunk && addThinkFirst + firstChunk = false + err := HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent, addThink) if err != nil { common.SysLog("error handling stream format: " + err.Error()) } @@ -149,7 +169,7 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re if info.RelayFormat == types.RelayFormatOpenAI { if shouldSendLastResp { - _ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) + _ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent, false) } } @@ -196,6 +216,10 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo if info.ChannelSetting.ForceFormat { forceFormat = true } + addThinkFirst := false + if info.ChannelSetting.AddThinkFirst { + addThinkFirst = true + } if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) { completionTokens := 0 @@ -210,6 +234,17 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo } } + if addThinkFirst { + for i := range simpleResponse.Choices { + newContent := "\n" + simpleResponse.Choices[i].Message.StringContent() + simpleResponse.Choices[i].Message.Content = &newContent + } + responseBody, err = common.Marshal(simpleResponse) + if err != nil { + return nil, types.NewError(err, types.ErrorCodeBadResponseBody) + } + } + switch info.RelayFormat { case types.RelayFormatOpenAI: if forceFormat { diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 23fe2c584626..4480fbc84dbc 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -132,6 +132,7 @@ const EditChannelModal = (props) => { pass_through_body_enabled: false, system_prompt: '', system_prompt_override: false, + add_think_first: false, settings: '', }; const [batch, setBatch] = useState(false); @@ -165,6 +166,8 @@ const EditChannelModal = (props) => { proxy: '', pass_through_body_enabled: false, system_prompt: '', + system_prompt_override: false, + add_think_first: false, }); const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示) const getInitValues = () => ({ ...originInputs }); @@ -336,6 +339,7 @@ const EditChannelModal = (props) => { 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; + data.add_think_first = parsedSettings.add_think_first || false; } catch (error) { console.error('解析渠道设置失败:', error); data.force_format = false; @@ -344,6 +348,7 @@ const EditChannelModal = (props) => { data.pass_through_body_enabled = false; data.system_prompt = ''; data.system_prompt_override = false; + data.add_think_first = false; } } else { data.force_format = false; @@ -352,6 +357,7 @@ const EditChannelModal = (props) => { data.pass_through_body_enabled = false; data.system_prompt = ''; data.system_prompt_override = false; + data.add_think_first = false; } if (data.settings) { @@ -383,6 +389,7 @@ const EditChannelModal = (props) => { pass_through_body_enabled: data.pass_through_body_enabled, system_prompt: data.system_prompt, system_prompt_override: data.system_prompt_override || false, + add_think_first: data.add_think_first, }); // console.log(data); } else { @@ -585,6 +592,7 @@ const EditChannelModal = (props) => { pass_through_body_enabled: false, system_prompt: '', system_prompt_override: false, + add_think_first: false, }); // 重置密钥模式状态 setKeyMode('append'); @@ -736,6 +744,7 @@ const EditChannelModal = (props) => { pass_through_body_enabled: localInputs.pass_through_body_enabled || false, system_prompt: localInputs.system_prompt || '', system_prompt_override: localInputs.system_prompt_override || false, + add_think_first: localInputs.add_think_first || false, }; localInputs.setting = JSON.stringify(channelExtraSettings); @@ -746,6 +755,7 @@ const EditChannelModal = (props) => { delete localInputs.pass_through_body_enabled; delete localInputs.system_prompt; delete localInputs.system_prompt_override; + delete localInputs.add_think_first; let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; @@ -1786,6 +1796,15 @@ const EditChannelModal = (props) => { onChange={(value) => handleChannelSettingsChange('system_prompt_override', value)} extraText={t('如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面')} /> + + ')} + checkedText={t('开')} + uncheckedText={t('关')} + onChange={(value) => handleChannelSettingsChange('add_think_first', value)} + extraText={t('将\\n拼接到响应的开头(只适用于OpenAI渠道类型)')} + /> diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 6db0e7eea741..4e79917e196a 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1347,6 +1347,8 @@ "系统提示词": "System Prompt", "输入系统提示词,用户的系统提示词将优先于此设置": "Enter system prompt, user's system prompt will take priority over this setting", "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "User priority: If the user specifies a system prompt in the request, the user's setting will be used first", + "开头补充": "Add at the beginning", + "将\\n拼接到响应的开头(只适用于OpenAI渠道类型)": "Add \\n at the beginning of the response (Only for OpenAI channel types)", "参数覆盖": "Parameters override", "模型请求速率限制": "Model request rate limit", "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",