diff --git a/backend/internal/handler/openai_chat_completions.go b/backend/internal/handler/openai_chat_completions.go index e74b11b9b6..147f4460a8 100644 --- a/backend/internal/handler/openai_chat_completions.go +++ b/backend/internal/handler/openai_chat_completions.go @@ -116,6 +116,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) { // 解析渠道级模型映射 channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel) + forwardModel := openAIChannelForwardModel(channelMapping, reqModel) if h.errorPassthroughService != nil { service.BindErrorPassthroughService(c, h.errorPassthroughService) @@ -170,7 +171,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) { apiKey.GroupID, "", sessionHash, - reqModel, + forwardModel, failedAccountIDs, service.OpenAIUpstreamTransportAny, service.OpenAIEndpointCapabilityChatCompletions, diff --git a/backend/internal/handler/openai_embeddings.go b/backend/internal/handler/openai_embeddings.go index 913131114e..8dda9faf3f 100644 --- a/backend/internal/handler/openai_embeddings.go +++ b/backend/internal/handler/openai_embeddings.go @@ -85,6 +85,7 @@ func (h *OpenAIGatewayHandler) Embeddings(c *gin.Context) { } channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel) + forwardModel := openAIChannelForwardModel(channelMapping, reqModel) subscription, _ := middleware2.GetSubscriptionFromContext(c) service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds()) @@ -127,7 +128,7 @@ func (h *OpenAIGatewayHandler) Embeddings(c *gin.Context) { apiKey.GroupID, "", "", - reqModel, + forwardModel, failedAccountIDs, service.OpenAIUpstreamTransportHTTPSSE, service.OpenAIEndpointCapabilityEmbeddings, diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index ce67e07e85..317b17fe1a 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -140,6 +140,18 @@ func openAIWSTurnBillingModel(result *service.OpenAIForwardResult, mapping servi return billingModel } +// openAIChannelForwardModel returns the model used for upstream capability +// routing. The client-requested model remains separate for logs, billing, and +// responses. +func openAIChannelForwardModel(mapping service.ChannelMappingResult, requestedModel string) string { + if mapping.Mapped { + if mappedModel := strings.TrimSpace(mapping.MappedModel); mappedModel != "" { + return mappedModel + } + } + return requestedModel +} + type grokMediaEligibilityProber interface { ProbeMediaEligibility(ctx context.Context, accountID int64) (bool, string, error) } @@ -533,10 +545,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) seedOpenAIForwardImageIntentHint(c, channelMapping.Mapped, imageIntent) - forwardModel := reqModel - if channelMapping.Mapped { - forwardModel = channelMapping.MappedModel - } + forwardModel := openAIChannelForwardModel(channelMapping, reqModel) c.Request = c.Request.WithContext(service.WithOpenAIForwardModel( c.Request.Context(), forwardModel, @@ -629,7 +638,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { apiKey.GroupID, previousResponseID, sessionHash, - reqModel, + forwardModel, failedAccountIDs, service.OpenAIUpstreamTransportAny, requiredCapability, @@ -2327,10 +2336,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) { // 解析渠道级模型映射 channelMappingWS, _ := h.gatewayService.ResolveChannelMappingAndRestrict(ctx, apiKey.GroupID, reqModel) - wsForwardModel := reqModel - if channelMappingWS.Mapped && strings.TrimSpace(channelMappingWS.MappedModel) != "" { - wsForwardModel = strings.TrimSpace(channelMappingWS.MappedModel) - } + wsForwardModel := openAIChannelForwardModel(channelMappingWS, reqModel) var currentUserRelease func() var currentAccountRelease func() @@ -2493,7 +2499,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) { apiKey.GroupID, previousResponseID, sessionHash, - reqModel, + wsForwardModel, failedAccountIDs, requiredTransport, requiredCapability, diff --git a/backend/internal/handler/openai_gateway_handler_test.go b/backend/internal/handler/openai_gateway_handler_test.go index 23acd158b3..54747cfdbd 100644 --- a/backend/internal/handler/openai_gateway_handler_test.go +++ b/backend/internal/handler/openai_gateway_handler_test.go @@ -1530,7 +1530,7 @@ func TestOpenAIResponsesWebSocket_PassthroughTracksModelPerTurn(t *testing.T) { "each turn must be billed with its own channel-mapped model") } -func TestOpenAIResponsesWebSocket_UnchangedChannelTargetOutsideAccountMappingKeysRemainsValid(t *testing.T) { +func TestOpenAIResponsesWebSocket_ChannelMappedTargetSelectsAccountWithoutRequestedAlias(t *testing.T) { got := runOpenAIResponsesWebSocketUsageLogCase(t, openAIResponsesWSUsageLogCase{ firstPayload: `{"type":"response.create","model":"public-alias","stream":false}`, secondPayload: `{"type":"response.create","stream":false}`, @@ -1538,7 +1538,7 @@ func TestOpenAIResponsesWebSocket_UnchangedChannelTargetOutsideAccountMappingKey "public-alias": "gpt-5.6-sol", }, accountModelMapping: map[string]any{ - "public-alias": "gpt-5.6-terra", + "gpt-5.6-sol": "gpt-5.6-sol", }, }) @@ -1710,6 +1710,40 @@ func TestOpenAIAccountScheduleModelUsesActualOrSharedResolver(t *testing.T) { require.Equal(t, "attempt-actual", openAIAccountScheduleModel(c, account, "public", true, nil)) } +func TestOpenAIChannelForwardModelForScheduler(t *testing.T) { + tests := []struct { + name string + mapping service.ChannelMappingResult + requested string + want string + }{ + { + name: "mapped model is used for capability filtering", + mapping: service.ChannelMappingResult{Mapped: true, MappedModel: " gpt-forward "}, + requested: "client-alias", + want: "gpt-forward", + }, + { + name: "unmapped request preserves client model", + mapping: service.ChannelMappingResult{MappedModel: "client-model"}, + requested: "client-model", + want: "client-model", + }, + { + name: "empty mapped model safely preserves client model", + mapping: service.ChannelMappingResult{Mapped: true, MappedModel: " "}, + requested: "client-model", + want: "client-model", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, openAIChannelForwardModel(tt.mapping, tt.requested)) + }) + } +} + func TestShouldReportOpenAIWSProxyAccountFailure(t *testing.T) { t.Run("unsupported client model switch does not penalize account", func(t *testing.T) { err := fmt.Errorf("wrapped ingress turn: %w", newOpenAIWSUnsupportedModelSwitchError("gpt-unsupported"))