Merge pull request #5833 from xuhaihan/fix/openai-fast-missing-tier

fix(openai): force priority when fast tier is omitted
This commit is contained in:
Wesley Liddick
2026-09-12 19:31:21 +08:00
committed by GitHub
12 changed files with 156 additions and 9 deletions
@@ -205,6 +205,64 @@ func TestApplyOpenAIFastPolicyToBody_DefaultPassesPriorityAndFast(t *testing.T)
require.Equal(t, string(body), string(updated))
}
func TestApplyOpenAIFastPolicyToBody_ForcePriorityInjectsMissingTier(t *testing.T) {
settings := &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{{
ServiceTier: OpenAIFastTierMissing,
Action: OpenAIFastPolicyActionForcePriority,
Scope: BetaPolicyScopeAll,
UserIDs: []int64{42},
}},
}
svc := newOpenAIGatewayServiceWithSettings(t, settings)
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
body := []byte(`{"model":"gpt-5.5"}`)
matchingCtx := context.WithValue(context.Background(), ctxkey.UserID, int64(42))
updated, err := svc.applyOpenAIFastPolicyToBody(matchingCtx, account, "gpt-5.5", body)
require.NoError(t, err)
require.Equal(t, OpenAIFastTierPriority, gjson.GetBytes(updated, "service_tier").String())
otherCtx := context.WithValue(context.Background(), ctxkey.UserID, int64(43))
updated, err = svc.applyOpenAIFastPolicyToBody(otherCtx, account, "gpt-5.5", body)
require.NoError(t, err)
require.False(t, gjson.GetBytes(updated, "service_tier").Exists())
}
func TestApplyOpenAIFastPolicyToBody_LegacyAllRuleDoesNotInjectMissingTier(t *testing.T) {
settings := &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{{
ServiceTier: OpenAIFastTierAny,
Action: OpenAIFastPolicyActionForcePriority,
Scope: BetaPolicyScopeAll,
}},
}
svc := newOpenAIGatewayServiceWithSettings(t, settings)
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
body := []byte(`{"model":"gpt-5.5"}`)
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.Equal(t, string(body), string(updated))
// Explicitly selected tiers continue to be forced by the existing rule.
body = []byte(`{"model":"gpt-5.5","service_tier":"flex"}`)
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.Equal(t, OpenAIFastTierPriority, gjson.GetBytes(updated, "service_tier").String())
}
func TestApplyOpenAIFastPolicyToBody_MissingTierIgnoresNonForceRule(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
body := []byte(`{"model":"gpt-5.5"}`)
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.Equal(t, string(body), string(updated))
}
func TestApplyOpenAIFastPolicyToBody_ExplicitFilterRemovesField(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
@@ -505,16 +563,23 @@ func TestSetOpenAIFastPolicySettings_Validation(t *testing.T) {
Action: BetaPolicyActionPass,
Scope: BetaPolicyScopeAll,
},
{
ServiceTier: OpenAIFastTierMissing,
Action: OpenAIFastPolicyActionForcePriority,
Scope: BetaPolicyScopeAll,
},
},
})
require.NoError(t, err)
got, err := svc.GetOpenAIFastPolicySettings(context.Background())
require.NoError(t, err)
require.Len(t, got.Rules, 2)
require.Len(t, got.Rules, 3)
require.Equal(t, OpenAIFastTierPriority, got.Rules[0].ServiceTier)
require.Equal(t, OpenAIFastTierUltrafast, got.Rules[1].ServiceTier)
require.Equal(t, OpenAIFastTierMissing, got.Rules[2].ServiceTier)
require.Equal(t, OpenAIFastPolicyActionForcePriority, got.Rules[0].Action)
require.Equal(t, []int64{42, 43}, got.Rules[0].UserIDs)
require.Equal(t, OpenAIFastTierUltrafast, got.Rules[1].ServiceTier)
require.Equal(t, BetaPolicyActionPass, got.Rules[1].Action)
require.Equal(t, OpenAIFastPolicyActionForcePriority, got.Rules[2].Action)
}
@@ -136,6 +136,24 @@ func TestWSResponseCreate_GroupForceInjectsMissingTier(t *testing.T) {
require.Equal(t, OpenAIFastTierPriority, gjson.GetBytes(updated, "service_tier").String())
}
func TestWSResponseCreate_ForcePriorityInjectsMissingTier(t *testing.T) {
settings := &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{{
ServiceTier: OpenAIFastTierMissing,
Action: OpenAIFastPolicyActionForcePriority,
Scope: BetaPolicyScopeAll,
}},
}
svc := newOpenAIGatewayServiceWithSettings(t, settings)
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
frame := []byte(`{"type":"response.create","model":"gpt-5.5"}`)
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
require.Equal(t, OpenAIFastTierPriority, gjson.GetBytes(updated, "service_tier").String())
}
func TestWSResponseCreate_GroupForceDoesNotTouchOtherFrames(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
@@ -681,6 +681,8 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
}
}
}
} else if s.shouldForceOpenAIFastPriorityForMissingTier(ctx, account, upstreamModel) {
markPatchSet("service_tier", OpenAIFastTierPriority)
}
if account.UsesOpenAICodexProtocol() {
@@ -1605,7 +1605,7 @@ func (e *OpenAIFastBlockedError) Error() string { return e.Message }
// Matching rules:
// - Scope filters by account type (all / oauth / apikey / bedrock)
// - UserIDs, when present, filters by the trusted Sub2API user that owns the API key
// - ServiceTier must be empty (= any), "all", or equal the normalized tier
// - ServiceTier must be empty (= any), "all", "missing", or equal the normalized tier
// - ModelWhitelist narrows the rule to specific models; FallbackAction
// handles the non-matching case (default: pass)
// - User-specific rules take precedence over global rules; each group keeps
@@ -1638,6 +1638,18 @@ func (s *OpenAIGatewayService) evaluateOpenAIFastPolicy(ctx context.Context, acc
return evaluateOpenAIFastPolicyWithSettings(settings, openAIFastPolicyUserID(ctx), account, model, tier)
}
// shouldForceOpenAIFastPriorityForMissingTier reports whether a request that
// omitted service_tier should be upgraded to priority. This is opt-in through
// the dedicated "missing" tier matcher; legacy "all" rules continue to apply
// only to requests that explicitly selected a recognized tier.
func (s *OpenAIGatewayService) shouldForceOpenAIFastPriorityForMissingTier(ctx context.Context, account *Account, model string) bool {
if account == nil || account.Platform != PlatformOpenAI {
return false
}
action, _ := s.evaluateOpenAIFastPolicy(ctx, account, model, OpenAIFastTierMissing)
return action == OpenAIFastPolicyActionForcePriority
}
// evaluateOpenAIFastPolicyWithSettings is the pure-function core extracted so
// long-lived sessions (e.g. WS) can prefetch settings once and avoid hitting
// the settingService on every frame. See WSSession entry and
@@ -1660,7 +1672,11 @@ func evaluateOpenAIFastPolicyWithSettings(settings *OpenAIFastPolicySettings, us
continue
}
ruleTier := strings.ToLower(strings.TrimSpace(rule.ServiceTier))
if ruleTier != "" && ruleTier != OpenAIFastTierAny && ruleTier != tier {
if tier == OpenAIFastTierMissing {
if ruleTier != OpenAIFastTierMissing {
continue
}
} else if ruleTier != "" && ruleTier != OpenAIFastTierAny && ruleTier != tier {
continue
}
eff := BetaPolicyRule{
@@ -1765,6 +1781,13 @@ func (s *OpenAIGatewayService) applyOpenAIFastPolicyToBody(ctx context.Context,
}
rawTier := gjson.GetBytes(body, "service_tier").String()
if rawTier == "" {
if s.shouldForceOpenAIFastPriorityForMissingTier(ctx, account, model) {
updated, err := sjson.SetBytes(body, "service_tier", OpenAIFastTierPriority)
if err != nil {
return body, fmt.Errorf("force missing service_tier priority on body: %w", err)
}
return updated, nil
}
return body, nil
}
normTier := normalizedOpenAIServiceTierValue(rawTier)
@@ -1884,6 +1907,13 @@ func (s *OpenAIGatewayService) applyOpenAIFastPolicyToWSResponseCreate(
}
rawTier := gjson.GetBytes(frame, "service_tier").String()
if rawTier == "" {
if s.shouldForceOpenAIFastPriorityForMissingTier(ctx, account, model) {
updated, err := sjson.SetBytes(frame, "service_tier", OpenAIFastTierPriority)
if err != nil {
return frame, nil, fmt.Errorf("force missing service_tier priority in ws frame: %w", err)
}
return updated, nil, nil
}
return frame, nil, nil
}
normTier := normalizedOpenAIServiceTierValue(rawTier)
@@ -1017,6 +1017,7 @@ func (s *SettingService) SetOpenAIFastPolicySettings(ctx context.Context, settin
}
validTiers := map[string]bool{
OpenAIFastTierAny: true, OpenAIFastTierPriority: true, OpenAIFastTierUltrafast: true, OpenAIFastTierFlex: true,
OpenAIFastTierMissing: true,
}
for i, rule := range settings.Rules {
+3 -2
View File
@@ -683,7 +683,7 @@ func DefaultBetaPolicySettings() *BetaPolicySettings {
// - "priority"(客户端可传 "fast",归一化为 "priority"):fast 模式
// - "ultrafast":Codex/API 的 Ultrafast 档位
// - "flex":低优先级模式
// - 省略:normal 默认
// - 省略:normal 默认;策略中可用专用 "missing" 条件显式匹配
//
// 本策略复用 BetaPolicyAction*/BetaPolicyScope* 常量语义,只是匹配键从
// anthropic-beta header 换成 body 的 service_tier 字段。
@@ -692,6 +692,7 @@ const (
OpenAIFastTierPriority = "priority" // 仅匹配 fast(priority)
OpenAIFastTierUltrafast = "ultrafast" // 仅匹配 ultrafast
OpenAIFastTierFlex = "flex" // 仅匹配 flex
OpenAIFastTierMissing = "missing" // 仅匹配省略 service_tier 的请求
// OpenAIFastPolicyActionForcePriority 会保留 service_tier 字段并强制写成
// priority,用于把 flex/auto/default/scale 等已识别 tier 收敛为 fast。
@@ -700,7 +701,7 @@ const (
// OpenAIFastPolicyRule 单条 OpenAI fast/flex 策略规则
type OpenAIFastPolicyRule struct {
ServiceTier string `json:"service_tier"` // "priority" | "ultrafast" | "flex" | "auto" | "default" | "scale" | "all"
ServiceTier string `json:"service_tier"` // "priority" | "ultrafast" | "flex" | "missing" | "all"
Action string `json:"action"` // "pass" | "filter" | "block" | "force_priority"
Scope string `json:"scope"` // "all" | "oauth" | "apikey" | "bedrock"
UserIDs []int64 `json:"user_ids,omitempty"` // 空=所有 Sub2API 用户;非空=仅指定 API Key 所属用户