Просмотр исходного кода

feat: passthrough model mapping

master
fengsilin 2 недель назад
Родитель
Сommit
6434a523a1
6 измененных файлов: 143 добавлений и 5 удалений
  1. +34
    -1
      relay/channel/api_request.go
  2. +72
    -0
      relay/channel/api_request_test.go
  3. +2
    -0
      setting/model_setting/global.go
  4. +6
    -1
      web/src/components/settings/ModelSetting.jsx
  5. +18
    -0
      web/src/pages/Setting/Model/SettingGlobalModel.jsx
  6. +11
    -3
      web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx

+ 34
- 1
relay/channel/api_request.go Просмотреть файл

@@ -18,12 +18,14 @@ import (
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/types"

"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/tidwall/sjson"
)

func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Header) {
@@ -277,6 +279,29 @@ func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]s
}
}

func applyPassthroughModelMapping(body []byte, info *common.RelayInfo) []byte {
if len(body) == 0 || info == nil || info.ChannelMeta == nil {
return body
}
if !model_setting.GetGlobalSettings().PassthroughApplyModelMapping {
return body
}
if !model_setting.GetGlobalSettings().PassThroughRequestEnabled && !info.ChannelMeta.ChannelSetting.PassThroughBodyEnabled {
return body
}
if !info.ChannelMeta.IsModelMapped || strings.TrimSpace(info.ChannelMeta.UpstreamModelName) == "" {
return body
}
if common2.GetJsonType(body) != "object" {
return body
}
modified, err := sjson.SetBytes(body, "model", info.ChannelMeta.UpstreamModelName)
if err != nil {
return body
}
return modified
}

func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
fullRequestURL, err := a.GetRequestURL(info)
if err != nil {
@@ -285,7 +310,15 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
if common2.DebugEnabled {
common2.SysLog("fullRequestURL: " + fullRequestURL)
}
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
var bodyBytes []byte
if requestBody != nil {
bodyBytes, err = io.ReadAll(requestBody)
if err != nil {
return nil, fmt.Errorf("read request body failed: %w", err)
}
}
bodyBytes = applyPassthroughModelMapping(bodyBytes, info)
req, err := http.NewRequest(c.Request.Method, fullRequestURL, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("new request failed: %w", err)
}


+ 72
- 0
relay/channel/api_request_test.go Просмотреть файл

@@ -6,10 +6,82 @@ import (
"testing"

relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/model_setting"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestApplyPassthroughModelMapping(t *testing.T) {
body := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}`)
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{IsModelMapped: true, UpstreamModelName: "gpt-4.1"}}

origRequestEnabled := model_setting.GetGlobalSettings().PassThroughRequestEnabled
origApplyMapping := model_setting.GetGlobalSettings().PassthroughApplyModelMapping
defer func() {
model_setting.GetGlobalSettings().PassThroughRequestEnabled = origRequestEnabled
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = origApplyMapping
}()

model_setting.GetGlobalSettings().PassThroughRequestEnabled = true
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = true

got := applyPassthroughModelMapping(body, info)
assert.JSONEq(t, `{"model":"gpt-4.1","messages":[{"role":"user","content":"hi"}]}`, string(got))
}

func TestApplyPassthroughModelMapping_NonJSONKeepsOriginal(t *testing.T) {
body := []byte(`model=gpt-4o`)
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{IsModelMapped: true, UpstreamModelName: "gpt-4.1"}}

origRequestEnabled := model_setting.GetGlobalSettings().PassThroughRequestEnabled
origApplyMapping := model_setting.GetGlobalSettings().PassthroughApplyModelMapping
defer func() {
model_setting.GetGlobalSettings().PassThroughRequestEnabled = origRequestEnabled
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = origApplyMapping
}()

model_setting.GetGlobalSettings().PassThroughRequestEnabled = true
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = true

got := applyPassthroughModelMapping(body, info)
assert.Equal(t, body, got)
}

func TestApplyPassthroughModelMapping_DisabledKeepsOriginal(t *testing.T) {
body := []byte(`{"model":"gpt-4o"}`)
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{IsModelMapped: false, UpstreamModelName: "gpt-4.1"}}

origRequestEnabled := model_setting.GetGlobalSettings().PassThroughRequestEnabled
origApplyMapping := model_setting.GetGlobalSettings().PassthroughApplyModelMapping
defer func() {
model_setting.GetGlobalSettings().PassThroughRequestEnabled = origRequestEnabled
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = origApplyMapping
}()

model_setting.GetGlobalSettings().PassThroughRequestEnabled = true
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = true

got := applyPassthroughModelMapping(body, info)
assert.Equal(t, body, got)
}

func TestDoApiRequest_DoesNotChangeNonPassthroughBody(t *testing.T) {
origRequestEnabled := model_setting.GetGlobalSettings().PassThroughRequestEnabled
origApplyMapping := model_setting.GetGlobalSettings().PassthroughApplyModelMapping
defer func() {
model_setting.GetGlobalSettings().PassThroughRequestEnabled = origRequestEnabled
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = origApplyMapping
}()

model_setting.GetGlobalSettings().PassThroughRequestEnabled = false
model_setting.GetGlobalSettings().PassthroughApplyModelMapping = true

body := []byte(`{"model":"gpt-4o"}`)
got := applyPassthroughModelMapping(body, &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{IsModelMapped: true, UpstreamModelName: "gpt-4.1"}})
assert.Equal(t, body, got)
}

func TestProcessHeaderOverride_ChannelTestSkipsPassthroughRules(t *testing.T) {
t.Parallel()



+ 2
- 0
setting/model_setting/global.go Просмотреть файл

@@ -34,6 +34,7 @@ func (p ChatCompletionsToResponsesPolicy) IsChannelEnabled(channelID int, channe

type GlobalSettings struct {
PassThroughRequestEnabled bool `json:"pass_through_request_enabled"`
PassthroughApplyModelMapping bool `json:"passthrough_apply_model_mapping"`
ThinkingModelBlacklist []string `json:"thinking_model_blacklist"`
ChatCompletionsToResponsesPolicy ChatCompletionsToResponsesPolicy `json:"chat_completions_to_responses_policy"`
EndpointFormatRestrictionEnabled bool `json:"endpoint_format_restriction_enabled"`
@@ -42,6 +43,7 @@ type GlobalSettings struct {
// 默认配置
var defaultOpenaiSettings = GlobalSettings{
PassThroughRequestEnabled: false,
PassthroughApplyModelMapping: false,
ThinkingModelBlacklist: []string{
"moonshotai/kimi-k2-thinking",
"kimi-k2-thinking",


+ 6
- 1
web/src/components/settings/ModelSetting.jsx Просмотреть файл

@@ -40,6 +40,7 @@ const ModelSetting = () => {
'claude.default_max_tokens': '',
'claude.thinking_adapter_budget_tokens_percentage': 0.8,
'global.pass_through_request_enabled': false,
'global.passthrough_apply_model_mapping': false,
'global.endpoint_format_restriction_enabled': false,
'global.thinking_model_blacklist': '[]',
'global.chat_completions_to_responses_policy': '{}',
@@ -78,7 +79,11 @@ const ModelSetting = () => {
}
}
// Keep boolean config keys ending with enabled/Enabled so UI parses correctly.
if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) {
if (
item.key.endsWith('Enabled') ||
item.key.endsWith('enabled') ||
item.key === 'global.passthrough_apply_model_mapping'
) {
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;


+ 18
- 0
web/src/pages/Setting/Model/SettingGlobalModel.jsx Просмотреть файл

@@ -68,6 +68,7 @@ const chatCompletionsToResponsesPolicyAllChannelsExample = JSON.stringify(

const defaultGlobalSettingInputs = {
'global.pass_through_request_enabled': false,
'global.passthrough_apply_model_mapping': false,
'global.endpoint_format_restriction_enabled': false,
'global.thinking_model_blacklist': '[]',
'global.chat_completions_to_responses_policy': '{}',
@@ -206,6 +207,23 @@ export default function SettingGlobalModel(props) {
/>
</Col>
</Row>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
label={t('透传时应用模型映射')}
field={'global.passthrough_apply_model_mapping'}
onChange={(value) =>
setInputs({
...inputs,
'global.passthrough_apply_model_mapping': value,
})
}
extraText={t(
'开启后,在请求透传模式下,如果渠道配置了模型映射,将把请求体中的 model 替换为映射后的上游模型名',
)}
/>
</Col>
</Row>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch


+ 11
- 3
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx Просмотреть файл

@@ -131,8 +131,16 @@ export default function SettingsHeaderNavModules(props) {
async function onSubmit() {
const customLinkText = (headerNavModules.customLink?.text || '').trim();
const customLinkUrl = (headerNavModules.customLink?.url || '').trim();
const normalizedHeaderNavModules = {
...headerNavModules,
customLink: {
...headerNavModules.customLink,
text: customLinkText,
url: customLinkUrl,
},
};
if (
headerNavModules.customLink?.enabled &&
normalizedHeaderNavModules.customLink?.enabled &&
(!customLinkText || !customLinkUrl)
) {
showError(t('启用自定义链接前请先填写完整的导航文案和跳转链接'));
@@ -142,7 +150,7 @@ export default function SettingsHeaderNavModules(props) {
try {
const res = await API.put('/api/option/', {
key: 'HeaderNavModules',
value: JSON.stringify(headerNavModules),
value: JSON.stringify(normalizedHeaderNavModules),
});
const { success, message } = res.data;
if (success) {
@@ -153,7 +161,7 @@ export default function SettingsHeaderNavModules(props) {
type: 'set',
payload: {
...statusState.status,
HeaderNavModules: JSON.stringify(headerNavModules),
HeaderNavModules: JSON.stringify(normalizedHeaderNavModules),
},
});



Загрузка…
Отмена
Сохранить