diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go
index 7b502c2..cbed0bd 100644
--- a/relay/channel/api_request.go
+++ b/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)
}
diff --git a/relay/channel/api_request_test.go b/relay/channel/api_request_test.go
index 6c7834e..ede3f41 100644
--- a/relay/channel/api_request_test.go
+++ b/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()
diff --git a/setting/model_setting/global.go b/setting/model_setting/global.go
index 56d8fb7..e175536 100644
--- a/setting/model_setting/global.go
+++ b/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",
diff --git a/web/src/components/settings/ModelSetting.jsx b/web/src/components/settings/ModelSetting.jsx
index 8cf42f2..71116d3 100644
--- a/web/src/components/settings/ModelSetting.jsx
+++ b/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;
diff --git a/web/src/pages/Setting/Model/SettingGlobalModel.jsx b/web/src/pages/Setting/Model/SettingGlobalModel.jsx
index 328ba11..e30c397 100644
--- a/web/src/pages/Setting/Model/SettingGlobalModel.jsx
+++ b/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) {
/>
+
+
+
+ setInputs({
+ ...inputs,
+ 'global.passthrough_apply_model_mapping': value,
+ })
+ }
+ extraText={t(
+ '开启后,在请求透传模式下,如果渠道配置了模型映射,将把请求体中的 model 替换为映射后的上游模型名',
+ )}
+ />
+
+