Merge pull request #3176 from jianjianai/fix/precompute-model-body-replacement

优化 OpenAI 网关 failover 流程,避免账号切换时重复对请求体执行 JSON model 替换。
This commit is contained in:
Wesley Liddick
2026-06-10 09:58:19 +08:00
committed by GitHub
2 changed files with 62 additions and 9 deletions
@@ -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() {
@@ -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)