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
39 changes: 16 additions & 23 deletions docs/channel/other_setting.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
# 渠道而外设置说明
# 渠道额外设置说明

该配置用于设置一些额外的渠道参数,可以通过 JSON 对象进行配置。主要包含以下两个设置项
该配置用于设置一些额外的渠道参数,可以通过前端页面进行配置。主要包含以下设置项

1. force_format
1. 强制格式化
- 用于标识是否对数据进行强制格式化为 OpenAI 格式
- 类型为布尔值,设置为 true 时启用强制格式化

2. proxy
- 用于配置网络代理
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)

3. thinking_to_content
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回
- 类型为布尔值,设置为 true 时启用思考内容转换
2. 思考内容转换
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回

--------------------------------------------------------------
3. 透传请求体
- 用于将自定义请求体发送到上游

## JSON 格式示例
4. 代理地址
- 用于配置网络代理,需要填写代理地址(支持socks5协议)

以下是一个示例配置,启用强制格式化并设置了代理地址:
5. 系统提示词
- 输入系统提示词,用户的系统提示词将优先于此设置
- 用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置

```json
{
"force_format": true,
"thinking_to_content": true,
"proxy": "socks5://xxxxxxx"
}
```
6. 系统提示词拼接
- 如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面

--------------------------------------------------------------

通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
7. add_think_first
- 在通过vllm和sglang自部署模型使用一些自带`<think>\n`的`chat_template`时,将`<think>\n`标签拼接到响应的开头
1 change: 1 addition & 0 deletions dto/channel_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions i18n/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,8 @@
"系统提示词": "系统提示词",
"输入系统提示词,用户的系统提示词将优先于此设置": "输入系统提示词,用户的系统提示词将优先于此设置",
"用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置",
"开头补充<think>": "开头补充<think>",
"将<think>\\n拼接到响应的开头(只适用于OpenAI渠道类型)": "将<think>\\n拼接到响应的开头(只适用于OpenAI渠道类型)",
"参数覆盖": "参数覆盖",
"此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:",
"请输入组织org-xxx": "请输入组织org-xxx",
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/gemini/relay-gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions relay/channel/openai/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 39 additions & 4 deletions relay/channel/openai/relay-openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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 := "<think>\n" + content
lastStreamResponse.Choices[i].Delta.Content = &newContent
}
}

if !thinkToContent {
return helper.ObjectData(c, lastStreamResponse)
}
Expand Down Expand Up @@ -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())
}
Expand All @@ -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)
}
}
Comment on lines 170 to 174

@coderabbitai coderabbitai Bot Aug 21, 2025

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.

⚠️ Potential issue

Single-chunk streaming bug: prefix not applied to the only chunk.

When the stream consists of a single data chunk, the first (and only) chunk is flushed in the “last response” block with addThink=false, so “\n” is never prepended. Fix by reusing the same firstChunk&&addThinkFirst logic for the final flush.

-		if shouldSendLastResp {
-			_ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent, false)
-		}
+		if shouldSendLastResp {
+			// Apply think-first if the last chunk is also the first (single-chunk stream)
+			_ = sendStreamData(
+				c,
+				info,
+				lastStreamData,
+				info.ChannelSetting.ForceFormat,
+				info.ChannelSetting.ThinkingToContent,
+				addThinkFirst && firstChunk,
+			)
+		}
📝 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
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)
}
}
if info.RelayFormat == types.RelayFormatOpenAI {
if shouldSendLastResp {
// Apply think-first if the last chunk is also the first (single-chunk stream)
_ = sendStreamData(
c,
info,
lastStreamData,
info.ChannelSetting.ForceFormat,
info.ChannelSetting.ThinkingToContent,
addThinkFirst && firstChunk,
)
}
}
🤖 Prompt for AI Agents
In relay/channel/openai/relay-openai.go around lines 170 to 174, the final flush
path uses addThink=false so when a stream contains a single chunk the Thinking
prefix is never applied; change the final sendStreamData call to reuse the same
firstChunk && addThinkFirst logic used for earlier chunks (i.e. compute
addThinkFirst = firstChunk && info.ChannelSetting.ThinkingToContent and pass
that boolean into sendStreamData on the last-response branch), and ensure
firstChunk is updated/treated consistently so the only chunk receives the
Thinking prefix when appropriate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last chunk doesn't need <think>\n.

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.

@jiajunly Thank you for the clarification! You're absolutely right.

Looking at the streaming logic more carefully, I can see that the <think>\n prefix is only intended for the very first chunk of the stream, and the final flush with addThink=false is correct by design. Even in single-chunk scenarios, the prefix would have been applied during the normal scanner processing loop, not during the final response handling.

My analysis was incorrect - the current implementation properly ensures only the first chunk gets the prefix, and the last chunk (whether it's part of a multi-chunk or single-chunk stream) correctly doesn't receive the prefix.


✏️ Learnings added
Learnt from: jiajunly
PR: QuantumNous/new-api#1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.045Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.


Expand Down Expand Up @@ -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
Expand All @@ -210,6 +234,17 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
}
}

if addThinkFirst {
for i := range simpleResponse.Choices {
newContent := "<think>\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 {
Expand Down
19 changes: 19 additions & 0 deletions web/src/components/table/channels/modals/EditChannelModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -585,6 +592,7 @@ const EditChannelModal = (props) => {
pass_through_body_enabled: false,
system_prompt: '',
system_prompt_override: false,
add_think_first: false,
});
// 重置密钥模式状态
setKeyMode('append');
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -1786,6 +1796,15 @@ const EditChannelModal = (props) => {
onChange={(value) => handleChannelSettingsChange('system_prompt_override', value)}
extraText={t('如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面')}
/>

<Form.Switch
field='add_think_first'
label={t('开头补充<think>')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) => handleChannelSettingsChange('add_think_first', value)}
extraText={t('将<think>\\n拼接到响应的开头(只适用于OpenAI渠道类型)')}
/>
</Card>
</div>
</Spin>
Expand Down
2 changes: 2 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"开头补充<think>": "Add <think> at the beginning",
"将<think>\\n拼接到响应的开头(只适用于OpenAI渠道类型)": "Add <think>\\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)",
Expand Down