diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index 80e00b7fc4..65bf172ecd 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -47,6 +47,30 @@ func resolveOpenAIMessagesDispatchMappedModel(apiKey *service.APIKey, requestedM return strings.TrimSpace(apiKey.Group.ResolveMessagesDispatchModel(requestedModel)) } +type openAIModelBodyReplaceFunc func([]byte, string) []byte + +func openAIModelMappedBody(body []byte, mapped bool, mappedModel string, replace openAIModelBodyReplaceFunc) []byte { + if !mapped || replace == nil { + return body + } + return replace(body, mappedModel) +} + +func newOpenAIModelMappedBodyCache(body []byte, replace openAIModelBodyReplaceFunc) func(bool, string) []byte { + replacedBodies := make(map[string][]byte) + return func(mapped bool, mappedModel string) []byte { + if !mapped { + return body + } + if cachedBody, ok := replacedBodies[mappedModel]; ok { + return cachedBody + } + replacedBody := openAIModelMappedBody(body, true, mappedModel, replace) + replacedBodies[mappedModel] = replacedBody + return replacedBody + } +} + func usageRecordContext(parent context.Context, base context.Context) context.Context { if base == nil { base = context.Background() @@ -241,6 +265,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { // 解析渠道级模型映射 channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel) + forwardBody := openAIModelMappedBody(body, channelMapping.Mapped, channelMapping.MappedModel, h.gatewayService.ReplaceModelInBody) // 提前校验 function_call_output 是否具备可关联上下文,避免上游 400。 if !h.validateFunctionCallOutputRequest(c, body, reqLog) { @@ -353,11 +378,6 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { // Forward request service.SetOpsLatencyMs(c, service.OpsRoutingLatencyMsKey, time.Since(routingStart).Milliseconds()) forwardStart := time.Now() - // 应用渠道模型映射到请求体 - forwardBody := body - if channelMapping.Mapped { - forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMapping.MappedModel) - } writerSizeBeforeForward := c.Writer.Size() result, err := func() (*service.OpenAIForwardResult, error) { defer func() { @@ -660,6 +680,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) { // 解析渠道级模型映射 channelMappingMsg, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel) + mappedBodyForMessages := newOpenAIModelMappedBodyCache(body, h.gatewayService.ReplaceModelInBody) // 绑定错误透传服务,允许 service 层在非 failover 错误场景复用规则。 if h.errorPassthroughService != nil { @@ -758,10 +779,7 @@ func (h *OpenAIGatewayHandler) Messages(c *gin.Context) { defaultMappedModel := strings.TrimSpace(effectiveMappedModel) // 应用渠道模型映射到请求体 - forwardBody := body - if channelMappingMsg.Mapped { - forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMappingMsg.MappedModel) - } + forwardBody := mappedBodyForMessages(channelMappingMsg.Mapped, channelMappingMsg.MappedModel) writerSizeBeforeForward := c.Writer.Size() result, err := func() (*service.OpenAIForwardResult, error) { defer func() { diff --git a/backend/internal/handler/openai_gateway_handler_test.go b/backend/internal/handler/openai_gateway_handler_test.go index e7605e35e8..7743d6ab62 100644 --- a/backend/internal/handler/openai_gateway_handler_test.go +++ b/backend/internal/handler/openai_gateway_handler_test.go @@ -445,6 +445,41 @@ func TestResolveOpenAIMessagesDispatchMappedModel(t *testing.T) { }) } +func TestOpenAIModelMappedBody(t *testing.T) { + body := []byte(`{"model":"alias","input":"hello"}`) + calls := 0 + + forwardBody := openAIModelMappedBody(body, true, "gpt-5.4", func(body []byte, newModel string) []byte { + calls++ + return service.ReplaceModelInBody(body, newModel) + }) + + require.Equal(t, 1, calls) + require.Equal(t, "gpt-5.4", gjson.GetBytes(forwardBody, "model").String()) + require.Equal(t, "alias", gjson.GetBytes(body, "model").String()) +} + +func TestOpenAIModelMappedBodyCache(t *testing.T) { + body := []byte(`{"model":"alias","input":"hello"}`) + calls := 0 + mappedBody := newOpenAIModelMappedBodyCache(body, func(body []byte, newModel string) []byte { + calls++ + return service.ReplaceModelInBody(body, newModel) + }) + + first := mappedBody(true, "gpt-5.4") + second := mappedBody(true, "gpt-5.4") + third := mappedBody(true, "gpt-5.3-codex") + unmapped := mappedBody(false, "ignored") + + require.Equal(t, 2, calls) + require.Equal(t, "gpt-5.4", gjson.GetBytes(first, "model").String()) + require.Equal(t, "gpt-5.4", gjson.GetBytes(second, "model").String()) + require.Equal(t, "gpt-5.3-codex", gjson.GetBytes(third, "model").String()) + require.Equal(t, body, unmapped) + require.Same(t, &first[0], &second[0]) +} + func TestOpenAIResponses_MissingDependencies_ReturnsServiceUnavailable(t *testing.T) { gin.SetMode(gin.TestMode)