Merge pull request #1441 from QuantumNous/system_prompt

feat: 支持渠道级透传选项,支持设置渠道系统提示词
This commit is contained in:
Calcium-Ion
2025-07-26 12:16:16 +08:00
committed by GitHub
13 changed files with 446 additions and 86 deletions

View File

@@ -1,7 +1,9 @@
package dto package dto
type ChannelSettings struct { type ChannelSettings struct {
ForceFormat bool `json:"force_format,omitempty"` ForceFormat bool `json:"force_format,omitempty"`
ThinkingToContent bool `json:"thinking_to_content,omitempty"` ThinkingToContent bool `json:"thinking_to_content,omitempty"`
Proxy string `json:"proxy"` Proxy string `json:"proxy"`
PassThroughBodyEnabled bool `json:"pass_through_body_enabled,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
} }

View File

@@ -7,15 +7,15 @@ import (
) )
type ResponseFormat struct { type ResponseFormat struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
JsonSchema *FormatJsonSchema `json:"json_schema,omitempty"` JsonSchema json.RawMessage `json:"json_schema,omitempty"`
} }
type FormatJsonSchema struct { type FormatJsonSchema struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Schema any `json:"schema,omitempty"` Schema any `json:"schema,omitempty"`
Strict any `json:"strict,omitempty"` Strict json.RawMessage `json:"strict,omitempty"`
} }
type GeneralOpenAIRequest struct { type GeneralOpenAIRequest struct {
@@ -73,6 +73,15 @@ func (r *GeneralOpenAIRequest) ToMap() map[string]any {
return result return result
} }
func (r *GeneralOpenAIRequest) GetSystemRoleName() string {
if strings.HasPrefix(r.Model, "o") {
if !strings.HasPrefix(r.Model, "o1-mini") && !strings.HasPrefix(r.Model, "o1-preview") {
return "developer"
}
}
return "system"
}
type ToolCallRequest struct { type ToolCallRequest struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Type string `json:"type"` Type string `json:"type"`

View File

@@ -585,6 +585,20 @@
"渠道权重": "渠道权重", "渠道权重": "渠道权重",
"渠道额外设置": "渠道额外设置", "渠道额外设置": "渠道额外设置",
"此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:", "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:",
"强制格式化": "强制格式化",
"强制格式化只适用于OpenAI渠道类型": "强制格式化只适用于OpenAI渠道类型",
"强制将响应格式化为 OpenAI 标准格式": "强制将响应格式化为 OpenAI 标准格式",
"思考内容转换": "思考内容转换",
"将 reasoning_content 转换为 <think> 标签拼接到内容中": "将 reasoning_content 转换为 <think> 标签拼接到内容中",
"透传请求体": "透传请求体",
"启用请求体透传功能": "启用请求体透传功能",
"代理地址": "代理地址",
"例如: socks5://user:pass@host:port": "例如: socks5://user:pass@host:port",
"用于配置网络代理": "用于配置网络代理",
"用于配置网络代理,支持 socks5 协议": "用于配置网络代理,支持 socks5 协议",
"系统提示词": "系统提示词",
"输入系统提示词,用户的系统提示词将优先于此设置": "输入系统提示词,用户的系统提示词将优先于此设置",
"用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置",
"参数覆盖": "参数覆盖", "参数覆盖": "参数覆盖",
"此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:", "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:",
"请输入组织org-xxx": "请输入组织org-xxx", "请输入组织org-xxx": "请输入组织org-xxx",

View File

@@ -111,18 +111,17 @@ func Distribute() func(c *gin.Context) {
if userGroup == "auto" { if userGroup == "auto" {
showGroup = fmt.Sprintf("auto(%s)", selectGroup) showGroup = fmt.Sprintf("auto(%s)", selectGroup)
} }
message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败distributor: %s", showGroup, modelRequest.Model, err.Error()) message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(数据库一致性已被破坏,distributor: %s", showGroup, modelRequest.Model, err.Error())
// 如果错误,但是渠道不为空,说明是数据库一致性问题 // 如果错误,但是渠道不为空,说明是数据库一致性问题
if channel != nil { //if channel != nil {
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) // common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
message = "数据库一致性已被破坏,请联系管理员" // message = "数据库一致性已被破坏,请联系管理员"
} //}
// 如果错误,而且渠道为空,说明是没有可用渠道
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message) abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message)
return return
} }
if channel == nil { if channel == nil {
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 可用渠道不存在(数据库一致性已被破坏,distributor", userGroup, modelRequest.Model)) abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 可用渠道distributor", userGroup, modelRequest.Model))
return return
} }
} }

View File

@@ -219,9 +219,13 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") { if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
geminiRequest.GenerationConfig.ResponseMimeType = "application/json" geminiRequest.GenerationConfig.ResponseMimeType = "application/json"
if textRequest.ResponseFormat.JsonSchema != nil && textRequest.ResponseFormat.JsonSchema.Schema != nil { if len(textRequest.ResponseFormat.JsonSchema) > 0 {
cleanedSchema := removeAdditionalPropertiesWithDepth(textRequest.ResponseFormat.JsonSchema.Schema, 0) // 先将json.RawMessage解析
geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema var jsonSchema dto.FormatJsonSchema
if err := common.Unmarshal(textRequest.ResponseFormat.JsonSchema, &jsonSchema); err == nil {
cleanedSchema := removeAdditionalPropertiesWithDepth(jsonSchema.Schema, 0)
geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema
}
} }
} }
tool_call_ids := make(map[string]string) tool_call_ids := make(map[string]string)

View File

@@ -80,7 +80,6 @@ func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType) return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType)
} }
adaptor.Init(relayInfo) adaptor.Init(relayInfo)
var requestBody io.Reader
if textRequest.MaxTokens == 0 { if textRequest.MaxTokens == 0 {
textRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model)) textRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model))
@@ -108,18 +107,41 @@ func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
relayInfo.UpstreamModelName = textRequest.Model relayInfo.UpstreamModelName = textRequest.Model
} }
convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest) var requestBody io.Reader
if err != nil { if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) body, err := common.GetRequestBody(c)
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
jsonData, err := common.Marshal(convertedRequest)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
_ = common.Unmarshal(jsonData, &reqMap)
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = common.Marshal(reqMap)
if err != nil {
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
}
if common.DebugEnabled {
println("requestBody: ", string(jsonData))
}
requestBody = bytes.NewBuffer(jsonData)
} }
jsonData, err := common.Marshal(convertedRequest)
if common.DebugEnabled {
println("requestBody: ", string(jsonData))
}
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
requestBody = bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping") statusCodeMappingStr := c.GetString("status_code_mapping")
var httpResp *http.Response var httpResp *http.Response

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/dto" "one-api/dto"
@@ -194,16 +195,39 @@ func GeminiHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
} }
} }
requestBody, err := json.Marshal(req) var requestBody io.Reader
if err != nil { if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) body, err := common.GetRequestBody(c)
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
}
requestBody = bytes.NewReader(body)
} else {
jsonData, err := json.Marshal(req)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
_ = common.Unmarshal(jsonData, &reqMap)
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = common.Marshal(reqMap)
if err != nil {
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
}
if common.DebugEnabled {
println("Gemini request body: %s", string(jsonData))
}
requestBody = bytes.NewReader(jsonData)
} }
if common.DebugEnabled { resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
println("Gemini request body: %s", string(requestBody))
}
resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody))
if err != nil { if err != nil {
common.LogError(c, "Do gemini request failed: "+err.Error()) common.LogError(c, "Do gemini request failed: "+err.Error())
return types.NewError(err, types.ErrorCodeDoRequestFailed) return types.NewError(err, types.ErrorCodeDoRequestFailed)

View File

@@ -16,6 +16,7 @@ import (
"one-api/relay/helper" "one-api/relay/helper"
"one-api/service" "one-api/service"
"one-api/setting" "one-api/setting"
"one-api/setting/model_setting"
"one-api/types" "one-api/types"
"strings" "strings"
@@ -187,22 +188,43 @@ func ImageHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
var requestBody io.Reader var requestBody io.Reader
convertedRequest, err := adaptor.ConvertImageRequest(c, relayInfo, *imageRequest) if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled {
if err != nil { body, err := common.GetRequestBody(c)
return types.NewError(err, types.ErrorCodeConvertRequestFailed) if err != nil {
} return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
if relayInfo.RelayMode == relayconstant.RelayModeImagesEdits { }
requestBody = convertedRequest.(io.Reader) requestBody = bytes.NewBuffer(body)
} else { } else {
jsonData, err := json.Marshal(convertedRequest) convertedRequest, err := adaptor.ConvertImageRequest(c, relayInfo, *imageRequest)
if err != nil { if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) return types.NewError(err, types.ErrorCodeConvertRequestFailed)
} }
requestBody = bytes.NewBuffer(jsonData) if relayInfo.RelayMode == relayconstant.RelayModeImagesEdits {
} requestBody = convertedRequest.(io.Reader)
} else {
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
if common.DebugEnabled { // apply param override
println(fmt.Sprintf("image request body: %s", requestBody)) if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
_ = common.Unmarshal(jsonData, &reqMap)
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = common.Marshal(reqMap)
if err != nil {
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
}
if common.DebugEnabled {
println(fmt.Sprintf("image request body: %s", string(jsonData)))
}
requestBody = bytes.NewBuffer(jsonData)
}
} }
statusCodeMappingStr := c.GetString("status_code_mapping") statusCodeMappingStr := c.GetString("status_code_mapping")

View File

@@ -2,7 +2,6 @@ package relay
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -171,18 +170,42 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) {
adaptor.Init(relayInfo) adaptor.Init(relayInfo)
var requestBody io.Reader var requestBody io.Reader
if model_setting.GetGlobalSettings().PassThroughRequestEnabled { if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled {
body, err := common.GetRequestBody(c) body, err := common.GetRequestBody(c)
if err != nil { if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
} }
if common.DebugEnabled {
println("requestBody: ", string(body))
}
requestBody = bytes.NewBuffer(body) requestBody = bytes.NewBuffer(body)
} else { } else {
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, relayInfo, textRequest) convertedRequest, err := adaptor.ConvertOpenAIRequest(c, relayInfo, textRequest)
if err != nil { if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) return types.NewError(err, types.ErrorCodeConvertRequestFailed)
} }
jsonData, err := json.Marshal(convertedRequest)
if relayInfo.ChannelSetting.SystemPrompt != "" {
// 如果有系统提示,则将其添加到请求中
request := convertedRequest.(*dto.GeneralOpenAIRequest)
containSystemPrompt := false
for _, message := range request.Messages {
if message.Role == request.GetSystemRoleName() {
containSystemPrompt = true
break
}
}
if !containSystemPrompt {
// 如果没有系统提示,则添加系统提示
systemMessage := dto.Message{
Role: request.GetSystemRoleName(),
Content: relayInfo.ChannelSetting.SystemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
}
}
jsonData, err := common.Marshal(convertedRequest)
if err != nil { if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) return types.NewError(err, types.ErrorCodeConvertRequestFailed)
} }

View File

@@ -3,12 +3,14 @@ package relay
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/dto" "one-api/dto"
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
"one-api/relay/helper" "one-api/relay/helper"
"one-api/service" "one-api/service"
"one-api/setting/model_setting"
"one-api/types" "one-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -70,18 +72,42 @@ func RerankHelper(c *gin.Context, relayMode int) (newAPIError *types.NewAPIError
} }
adaptor.Init(relayInfo) adaptor.Init(relayInfo)
convertedRequest, err := adaptor.ConvertRerankRequest(c, relayInfo.RelayMode, *rerankRequest) var requestBody io.Reader
if err != nil { if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled {
return types.NewError(err, types.ErrorCodeConvertRequestFailed) body, err := common.GetRequestBody(c)
} if err != nil {
jsonData, err := common.Marshal(convertedRequest) return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest)
if err != nil { }
return types.NewError(err, types.ErrorCodeConvertRequestFailed) requestBody = bytes.NewBuffer(body)
} } else {
requestBody := bytes.NewBuffer(jsonData) convertedRequest, err := adaptor.ConvertRerankRequest(c, relayInfo.RelayMode, *rerankRequest)
if common.DebugEnabled { if err != nil {
println(fmt.Sprintf("Rerank request body: %s", requestBody.String())) return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
jsonData, err := common.Marshal(convertedRequest)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
_ = common.Unmarshal(jsonData, &reqMap)
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = common.Marshal(reqMap)
if err != nil {
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid)
}
}
if common.DebugEnabled {
println(fmt.Sprintf("Rerank request body: %s", string(jsonData)))
}
requestBody = bytes.NewBuffer(jsonData)
} }
resp, err := adaptor.DoRequest(c, relayInfo, requestBody) resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
if err != nil { if err != nil {
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError) return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)

View File

@@ -28,6 +28,7 @@ const (
ErrorTypeMidjourneyError ErrorType = "midjourney_error" ErrorTypeMidjourneyError ErrorType = "midjourney_error"
ErrorTypeGeminiError ErrorType = "gemini_error" ErrorTypeGeminiError ErrorType = "gemini_error"
ErrorTypeRerankError ErrorType = "rerank_error" ErrorTypeRerankError ErrorType = "rerank_error"
ErrorTypeUpstreamError ErrorType = "upstream_error"
) )
type ErrorCode string type ErrorCode string
@@ -194,6 +195,9 @@ func WithOpenAIError(openAIError OpenAIError, statusCode int) *NewAPIError {
if !ok { if !ok {
code = fmt.Sprintf("%v", openAIError.Code) code = fmt.Sprintf("%v", openAIError.Code)
} }
if openAIError.Type == "" {
openAIError.Type = "upstream_error"
}
return &NewAPIError{ return &NewAPIError{
RelayError: openAIError, RelayError: openAIError,
errorType: ErrorTypeOpenAIError, errorType: ErrorTypeOpenAIError,
@@ -204,6 +208,9 @@ func WithOpenAIError(openAIError OpenAIError, statusCode int) *NewAPIError {
} }
func WithClaudeError(claudeError ClaudeError, statusCode int) *NewAPIError { func WithClaudeError(claudeError ClaudeError, statusCode int) *NewAPIError {
if claudeError.Type == "" {
claudeError.Type = "upstream_error"
}
return &NewAPIError{ return &NewAPIError{
RelayError: claudeError, RelayError: claudeError,
errorType: ErrorTypeClaudeError, errorType: ErrorTypeClaudeError,

View File

@@ -121,6 +121,12 @@ const EditChannelModal = (props) => {
weight: 0, weight: 0,
tag: '', tag: '',
multi_key_mode: 'random', multi_key_mode: 'random',
// 渠道额外设置的默认值
force_format: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
}; };
const [batch, setBatch] = useState(false); const [batch, setBatch] = useState(false);
const [multiToSingle, setMultiToSingle] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false);
@@ -142,8 +148,69 @@ const EditChannelModal = (props) => {
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false); const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
const [channelSearchValue, setChannelSearchValue] = useState(''); const [channelSearchValue, setChannelSearchValue] = useState('');
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式 const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
// 渠道额外设置状态
const [channelSettings, setChannelSettings] = useState({
force_format: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
});
const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示) const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示)
const getInitValues = () => ({ ...originInputs }); const getInitValues = () => ({ ...originInputs });
// 处理渠道额外设置的更新
const handleChannelSettingsChange = (key, value) => {
// 更新内部状态
setChannelSettings(prev => ({ ...prev, [key]: value }));
// 同步更新到表单字段
if (formApiRef.current) {
formApiRef.current.setValue(key, value);
}
// 同步更新inputs状态
setInputs(prev => ({ ...prev, [key]: value }));
// 生成setting JSON并更新
const newSettings = { ...channelSettings, [key]: value };
const settingsJson = JSON.stringify(newSettings);
handleInputChange('setting', settingsJson);
};
// 解析渠道设置JSON为单独的状态
const parseChannelSettings = (settingJson) => {
try {
if (settingJson && settingJson.trim()) {
const parsed = JSON.parse(settingJson);
setChannelSettings({
force_format: parsed.force_format || false,
thinking_to_content: parsed.thinking_to_content || false,
proxy: parsed.proxy || '',
pass_through_body_enabled: parsed.pass_through_body_enabled || false,
system_prompt: parsed.system_prompt || '',
});
} else {
setChannelSettings({
force_format: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
});
}
} catch (error) {
console.error('解析渠道设置失败:', error);
setChannelSettings({
force_format: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
});
}
};
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValue(name, value); formApiRef.current.setValue(name, value);
@@ -256,6 +323,30 @@ const EditChannelModal = (props) => {
setBatch(false); setBatch(false);
setMultiToSingle(false); setMultiToSingle(false);
} }
// 解析渠道额外设置并合并到data中
if (data.setting) {
try {
const parsedSettings = JSON.parse(data.setting);
data.force_format = parsedSettings.force_format || false;
data.thinking_to_content = parsedSettings.thinking_to_content || false;
data.proxy = parsedSettings.proxy || '';
data.pass_through_body_enabled = parsedSettings.pass_through_body_enabled || false;
data.system_prompt = parsedSettings.system_prompt || '';
} catch (error) {
console.error('解析渠道设置失败:', error);
data.force_format = false;
data.thinking_to_content = false;
data.proxy = '';
data.pass_through_body_enabled = false;
}
} else {
data.force_format = false;
data.thinking_to_content = false;
data.proxy = '';
data.pass_through_body_enabled = false;
data.system_prompt = '';
}
setInputs(data); setInputs(data);
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValues(data); formApiRef.current.setValues(data);
@@ -266,6 +357,14 @@ const EditChannelModal = (props) => {
setAutoBan(true); setAutoBan(true);
} }
setBasicModels(getChannelModels(data.type)); setBasicModels(getChannelModels(data.type));
// 同步更新channelSettings状态显示
setChannelSettings({
force_format: data.force_format,
thinking_to_content: data.thinking_to_content,
proxy: data.proxy,
pass_through_body_enabled: data.pass_through_body_enabled,
system_prompt: data.system_prompt,
});
// console.log(data); // console.log(data);
} else { } else {
showError(message); showError(message);
@@ -446,6 +545,14 @@ const EditChannelModal = (props) => {
setUseManualInput(false); setUseManualInput(false);
} else { } else {
formApiRef.current?.reset(); formApiRef.current?.reset();
// 重置渠道设置状态
setChannelSettings({
force_format: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
});
} }
}, [props.visible, channelId]); }, [props.visible, channelId]);
@@ -579,6 +686,24 @@ const EditChannelModal = (props) => {
if (localInputs.type === 18 && localInputs.other === '') { if (localInputs.type === 18 && localInputs.other === '') {
localInputs.other = 'v2.1'; localInputs.other = 'v2.1';
} }
// 生成渠道额外设置JSON
const channelExtraSettings = {
force_format: localInputs.force_format || false,
thinking_to_content: localInputs.thinking_to_content || false,
proxy: localInputs.proxy || '',
pass_through_body_enabled: localInputs.pass_through_body_enabled || false,
system_prompt: localInputs.system_prompt || '',
};
localInputs.setting = JSON.stringify(channelExtraSettings);
// 清理不需要发送到后端的字段
delete localInputs.force_format;
delete localInputs.thinking_to_content;
delete localInputs.proxy;
delete localInputs.pass_through_body_enabled;
delete localInputs.system_prompt;
let res; let res;
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
localInputs.models = localInputs.models.join(','); localInputs.models = localInputs.models.join(',');
@@ -1446,33 +1571,103 @@ const EditChannelModal = (props) => {
showClear showClear
/> />
<Form.TextArea <div className="mb-6">
field='setting' <Text className="text-sm font-medium mb-3 block">
label={t('渠道额外设置')} {t('渠道额外设置')}
placeholder={ </Text>
t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + <div className="space-y-6 p-4 bg-gray-50 rounded-lg">
'\n{\n "force_format": true\n}' <Row gutter={16}>
} <Col span={16}>
autosize <div>
onChange={(value) => handleInputChange('setting', value)} <Text className="font-medium block mb-1">{t('强制格式化只适用于OpenAI渠道类型')}</Text>
extraText={( <Text type="tertiary" size="small">
<Space wrap> {t('强制将响应格式化为 OpenAI 标准格式')}
</Text>
</div>
</Col>
<Col span={8} className="flex items-center justify-end">
<Form.Switch
field='force_format'
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(val) => handleChannelSettingsChange('force_format', val)}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<div>
<Text className="font-medium block mb-1">{t('思考内容转换')}</Text>
<Text type="tertiary" size="small">
{t('将 reasoning_content 转换为 <think> 标签拼接到内容中')}
</Text>
</div>
</Col>
<Col span={8} className="flex items-center justify-end">
<Form.Switch
field='thinking_to_content'
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(val) => handleChannelSettingsChange('thinking_to_content', val)}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<div>
<Text className="font-medium block mb-1">{t('透传请求体')}</Text>
<Text type="tertiary" size="small">
{t('启用请求体透传功能')}
</Text>
</div>
</Col>
<Col span={8} className="flex items-center justify-end">
<Form.Switch
field='pass_through_body_enabled'
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(val) => handleChannelSettingsChange('pass_through_body_enabled', val)}
/>
</Col>
</Row>
<div>
<Form.Input
field='proxy'
label={t('代理地址')}
placeholder={t('例如: socks5://user:pass@host:port')}
onChange={(val) => handleChannelSettingsChange('proxy', val)}
showClear
helpText={t('用于配置网络代理')}
/>
</div>
<div>
<Form.TextArea
field='system_prompt'
label={t('系统提示词')}
placeholder={t('输入系统提示词,用户的系统提示词将优先于此设置')}
onChange={(val) => handleChannelSettingsChange('system_prompt', val)}
autosize
showClear
helpText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')}
/>
</div>
<div className="text-right pt-2 border-t border-gray-200">
<Text <Text
className="!text-semi-color-primary cursor-pointer" className="!text-semi-color-primary cursor-pointer text-sm"
onClick={() => handleInputChange('setting', JSON.stringify({ force_format: true }, null, 2))}
>
{t('填入模板')}
</Text>
<Text
className="!text-semi-color-primary cursor-pointer"
onClick={() => window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')} onClick={() => window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')}
> >
{t('设置说明')} {t('设置说明')}
</Text> </Text>
</Space> </div>
)} </div>
showClear </div>
/>
</Card> </Card>
</div> </div>
</Spin> </Spin>

View File

@@ -1330,6 +1330,19 @@
"API地址": "Base URL", "API地址": "Base URL",
"对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", "对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
"渠道额外设置": "Channel extra settings", "渠道额外设置": "Channel extra settings",
"强制格式化": "Force format",
"强制格式化只适用于OpenAI渠道类型": "Force format (Only for OpenAI channel types)",
"强制将响应格式化为 OpenAI 标准格式": "Force format responses to OpenAI standard format",
"思考内容转换": "Thinking content conversion",
"将 reasoning_content 转换为 <think> 标签拼接到内容中": "Convert reasoning_content to <think> tags and append to content",
"透传请求体": "Pass through body",
"启用请求体透传功能": "Enable request body pass-through functionality",
"代理地址": "Proxy address",
"例如: socks5://user:pass@host:port": "e.g.: socks5://user:pass@host:port",
"用于配置网络代理,支持 socks5 协议": "Used to configure network proxy, supports socks5 protocol",
"系统提示词": "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",
"参数覆盖": "Parameters override", "参数覆盖": "Parameters override",
"模型请求速率限制": "Model request rate limit", "模型请求速率限制": "Model request rate limit",
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)", "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",