mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 16:23:31 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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 所属用户
|
||||
|
||||
Reference in New Issue
Block a user