fix(openai): schedule accounts with mapped model

This commit is contained in:
wucm667
2026-09-03 10:51:12 +08:00
parent 5097b31457
commit 5e4958c889
4 changed files with 56 additions and 14 deletions
@@ -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,
@@ -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,
@@ -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,
@@ -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"))