Merge pull request #2457 from wucm667/fix/openai-fast-policy-default-pass

fix: 默认透传 OpenAI service_tier
This commit is contained in:
Wesley Liddick 2026-05-19 14:34:37 +08:00 committed by GitHub
commit ae4c738887
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 102 additions and 93 deletions

View File

@ -788,14 +788,7 @@ func TestAPIContracts(t *testing.T) {
"payment_visible_method_wxpay_enabled": false,
"openai_advanced_scheduler_enabled": true,
"openai_fast_policy_settings": {
"rules": [
{
"service_tier": "priority",
"action": "filter",
"scope": "all",
"fallback_action": "pass"
}
]
"rules": []
},
"custom_menu_items": [],
"custom_endpoints": [],
@ -1003,14 +996,7 @@ func TestAPIContracts(t *testing.T) {
"payment_visible_method_wxpay_enabled": false,
"openai_advanced_scheduler_enabled": false,
"openai_fast_policy_settings": {
"rules": [
{
"service_tier": "priority",
"action": "filter",
"scope": "all",
"fallback_action": "pass"
}
]
"rules": []
},
"payment_enabled": false,
"payment_min_amount": 0,

View File

@ -8,6 +8,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
type openAIFastPolicyRepoStub struct {
@ -62,25 +63,33 @@ func newOpenAIGatewayServiceWithSettings(t *testing.T, settings *OpenAIFastPolic
}
}
func TestEvaluateOpenAIFastPolicy_DefaultFiltersAllModelsPriority(t *testing.T) {
func openAIFastFilterPriorityPolicy() *OpenAIFastPolicySettings {
return &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{{
ServiceTier: OpenAIFastTierPriority,
Action: BetaPolicyActionFilter,
Scope: BetaPolicyScopeAll,
ModelWhitelist: []string{},
FallbackAction: BetaPolicyActionPass,
}},
}
}
func TestEvaluateOpenAIFastPolicy_DefaultPassesKnownTiers(t *testing.T) {
require.Empty(t, DefaultOpenAIFastPolicySettings().Rules, "default policy must not rewrite service_tier unless admin configured rules")
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
// 默认策略对所有模型生效whitelist 为空),因为 codex 的 service_tier=fast
// 是用户级开关,与 model 正交。
// gpt-5.5 + priority → filter
action, _ := svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", OpenAIFastTierPriority)
require.Equal(t, BetaPolicyActionFilter, action)
require.Equal(t, BetaPolicyActionPass, action)
// gpt-5.5-turbo → filter
action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5-turbo", OpenAIFastTierPriority)
require.Equal(t, BetaPolicyActionFilter, action)
require.Equal(t, BetaPolicyActionPass, action)
// gpt-4 + priority → filter默认策略覆盖所有模型
action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-4", OpenAIFastTierPriority)
require.Equal(t, BetaPolicyActionFilter, action)
require.Equal(t, BetaPolicyActionPass, action)
// gpt-5.5 + flex → pass (tier doesn't match)
action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", OpenAIFastTierFlex)
require.Equal(t, BetaPolicyActionPass, action)
@ -129,27 +138,24 @@ func TestEvaluateOpenAIFastPolicy_ScopeFiltersOAuth(t *testing.T) {
require.Equal(t, BetaPolicyActionPass, action)
}
func TestApplyOpenAIFastPolicyToBody_FilterRemovesField(t *testing.T) {
func TestApplyOpenAIFastPolicyToBody_DefaultPassesPriorityAndFast(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
// gpt-5.5 fast → service_tier stripped
body := []byte(`{"model":"gpt-5.5","service_tier":"priority","messages":[]}`)
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.NotContains(t, string(updated), `"service_tier"`)
require.Equal(t, string(body), string(updated))
// Client sending "fast" (alias for priority) also filtered
body = []byte(`{"model":"gpt-5.5","service_tier":"fast"}`)
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.NotContains(t, string(updated), `"service_tier"`)
require.Equal(t, "priority", gjson.GetBytes(updated, "service_tier").String())
// gpt-4 priority → 默认策略对所有模型 filterservice_tier 被移除
body = []byte(`{"model":"gpt-4","service_tier":"priority"}`)
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-4", body)
require.NoError(t, err)
require.NotContains(t, string(updated), `"service_tier"`)
require.Equal(t, string(body), string(updated))
// No service_tier → no-op
body = []byte(`{"model":"gpt-5.5"}`)
@ -158,9 +164,23 @@ func TestApplyOpenAIFastPolicyToBody_FilterRemovesField(t *testing.T) {
require.Equal(t, string(body), string(updated))
}
// TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule 验证扩展白名单后
// 客户端显式发送的 OpenAI 官方合法 tierauto/default/scale能透传到上游而不被
// 静默剥离。默认策略只针对 priority所以这些 tier 落在 fall-through pass 分支。
func TestApplyOpenAIFastPolicyToBody_ExplicitFilterRemovesField(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
body := []byte(`{"model":"gpt-5.5","service_tier":"priority","messages":[]}`)
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.NotContains(t, string(updated), `"service_tier"`)
body = []byte(`{"model":"gpt-5.5","service_tier":"fast"}`)
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err)
require.NotContains(t, string(updated), `"service_tier"`)
}
// TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule 验证默认配置
// 下客户端显式发送的 OpenAI 官方合法 tier 能透传到上游而不被静默剥离。
func TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
@ -170,10 +190,10 @@ func TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule(t *testing.T
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
require.NoError(t, err, "tier %q should pass without error", tier)
require.Contains(t, string(updated), `"service_tier":"`+tier+`"`,
"tier %q should be preserved in body under default rule", tier)
"tier %q should be preserved in body under default policy", tier)
}
// evaluate 层也应判定为 pass默认规则 ServiceTier=priority 与 auto/default/scale 不匹配
// evaluate 层也应判定为 pass默认配置没有内置规则)
for _, tier := range []string{"auto", "default", "scale"} {
action, _ := svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", tier)
require.Equal(t, BetaPolicyActionPass, action, "tier %q should evaluate to pass", tier)

View File

@ -22,7 +22,7 @@ import (
// --- Helper-level (unit) tests for applyOpenAIFastPolicyToWSResponseCreate ---
func TestWSResponseCreate_FilterStripsServiceTier(t *testing.T) {
func TestWSResponseCreate_DefaultPassesPriorityAndNormalizesFast(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
@ -30,26 +30,37 @@ func TestWSResponseCreate_FilterStripsServiceTier(t *testing.T) {
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
require.NotContains(t, string(updated), `"service_tier"`, "filter action should strip service_tier")
require.Equal(t, "priority", gjson.GetBytes(updated, "service_tier").String(), "default policy should preserve priority tier")
// Other fields preserved.
require.Equal(t, "response.create", gjson.GetBytes(updated, "type").String())
require.Equal(t, "gpt-5.5", gjson.GetBytes(updated, "model").String())
require.Equal(t, "hi", gjson.GetBytes(updated, "input.0.text").String())
frame = []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"fast"}`)
updated, blocked, err = svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
require.Equal(t, "priority", gjson.GetBytes(updated, "service_tier").String(), "fast alias should normalize before reaching upstream")
// Mixed-case + whitespace variant should also normalize.
frame = []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":" Fast "}`)
updated, blocked, err = svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
require.Equal(t, "priority", gjson.GetBytes(updated, "service_tier").String())
}
func TestWSResponseCreate_FastNormalizedToPriorityThenFiltered(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
func TestWSResponseCreate_ExplicitFilterStripsServiceTier(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
// Verbatim "fast" → normalized to "priority" → matches default rule → filter.
frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"fast"}`)
frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"priority","input":[{"type":"input_text","text":"hi"}]}`)
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
require.NotContains(t, string(updated), `"service_tier"`)
require.NotContains(t, string(updated), `"service_tier"`, "filter action should strip service_tier")
// Mixed-case + whitespace variant should also normalize and filter.
frame = []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":" Fast "}`)
frame = []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"fast"}`)
updated, blocked, err = svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
require.Nil(t, blocked)
@ -60,7 +71,7 @@ func TestWSResponseCreate_FlexPassThrough(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
// Default policy targets priority only; flex is left untouched.
// Default policy has no rules; flex is left untouched.
frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"flex"}`)
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
require.NoError(t, err)
@ -220,8 +231,8 @@ func (f *fakePassthroughFrameConn) Close() error {
}
// gpt55WhitelistFastPolicy 返回一份强制带 model whitelist 的策略,用于
// 验证 capturedSessionModel fallback 的语义(默认策略 whitelist 为空时
// fallback 路径无法被观察到)。
// 验证 capturedSessionModel fallback 的语义(默认配置没有规则fallback
// 路径无法被观察到)。
func gpt55WhitelistFastPolicy() *OpenAIFastPolicySettings {
return &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{{
@ -242,7 +253,7 @@ func gpt55WhitelistFastPolicy() *OpenAIFastPolicySettings {
// through to the upstream.
func TestPolicyEnforcingFrameConn_FollowupFrameWithoutModelUsesCapturedModel(t *testing.T) {
// 此处特意使用带 whitelist 的策略,以便观察 capturedSessionModel
// fallback 是否生效(默认策略 whitelist 为空fallback 与否结果一致,
// fallback 是否生效(默认配置没有规则fallback 与否结果一致,
// 不能用来覆盖此回归)。
svc := newOpenAIGatewayServiceWithSettings(t, gpt55WhitelistFastPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
@ -310,13 +321,13 @@ func TestPolicyEnforcingFrameConn_WithoutCapturedFallbackPolicyMisses(t *testing
"sanity: without capturedSessionModel fallback the leak (D5) reproduces — confirms the fix is load-bearing")
}
// --- Ingress end-to-end test (filter path) ---
// --- Ingress end-to-end test (explicit filter path) ---
// TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream wires up the
// real ProxyResponsesWebSocketFromClient ingress session pipeline against a
// captureConn upstream and asserts that a client frame with service_tier=fast
// is normalized + filtered out before being written upstream. This is the
// integration flavour of TestWSResponseCreate_FilterStripsServiceTier.
// is normalized + filtered out by an explicit admin policy before being
// written upstream.
func TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream(t *testing.T) {
gin.SetMode(gin.TestMode)
@ -345,9 +356,9 @@ func TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream(t *testing.T)
pool.setClientDialerForTest(captureDialer)
repo := &openAIFastPolicyRepoStub{values: map[string]string{}}
defaultJSON, err := json.Marshal(DefaultOpenAIFastPolicySettings())
filterPolicyJSON, err := json.Marshal(openAIFastFilterPriorityPolicy())
require.NoError(t, err)
repo.values[SettingKeyOpenAIFastPolicySettings] = string(defaultJSON)
repo.values[SettingKeyOpenAIFastPolicySettings] = string(filterPolicyJSON)
svc := &OpenAIGatewayService{
cfg: cfg,
@ -631,13 +642,13 @@ func TestApplyOpenAIFastPolicyToBody_BlockShortCircuitsUpstream(t *testing.T) {
require.Equal(t, string(body), string(updated), "block must not mutate body")
}
// TestForwardAsAnthropicMessages_BetaFastModeTriggersOpenAIFastPolicy verifies
// the Anthropic-compat entrypoint chain: anthropic-beta: fast-mode → BetaFastMode
// detection → ServiceTier="priority" injection (openai_gateway_messages.go:60)
// → applyOpenAIFastPolicyToBody filter on default policy → upstream body has
// no service_tier. We exercise the same internal pipeline (Anthropic→Responses
// + BetaFastMode + policy) without spinning up a real upstream HTTP server.
func TestForwardAsAnthropicMessages_BetaFastModeTriggersOpenAIFastPolicy(t *testing.T) {
// TestForwardAsAnthropicMessages_BetaFastModePassesOpenAIFastPolicyByDefault
// verifies the Anthropic-compat entrypoint chain: anthropic-beta: fast-mode →
// BetaFastMode detection → ServiceTier="priority" injection
// (openai_gateway_messages.go:60) → default OpenAI fast policy pass. We
// exercise the same internal pipeline (Anthropic→Responses + BetaFastMode +
// policy) without spinning up a real upstream HTTP server.
func TestForwardAsAnthropicMessages_BetaFastModePassesOpenAIFastPolicyByDefault(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
@ -663,8 +674,9 @@ func TestForwardAsAnthropicMessages_BetaFastModeTriggersOpenAIFastPolicy(t *test
upstreamBody, policyErr := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", responsesBody)
require.NoError(t, policyErr)
// Step 4: assert that policy filtered the field before the upstream HTTP request.
require.NotContains(t, string(upstreamBody), `"service_tier"`, "default policy 命中 gpt-5.5 priority 应当 filter 掉 service_tier")
// Step 4: default policy must preserve the explicit fast/priority request.
require.Equal(t, "priority", gjson.GetBytes(upstreamBody, "service_tier").String(),
"default policy should pass service_tier=priority through to upstream")
}
// --- Fix1: passthrough capturedSessionModel must follow session.update ---
@ -808,7 +820,7 @@ func TestApplyOpenAIFastPolicyToBody_PassNormalizesFastAlias(t *testing.T) {
// tier) instead of the user-requested "priority". This test pins the
// contract those two helpers must uphold for the adapter's billing path.
func TestPassthroughBilling_PostFilterServiceTier(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
raw := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"priority"}`)
@ -821,7 +833,7 @@ func TestPassthroughBilling_PostFilterServiceTier(t *testing.T) {
require.Equal(t, "priority", *pre,
"sanity: raw first frame carries priority that pre-fix billing would have reported")
// Apply policy filter (default rule: gpt-5.5 + priority → filter).
// Apply explicit policy filter (gpt-5.5 + priority → filter).
filtered, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", raw)
require.NoError(t, err)
require.Nil(t, blocked)
@ -890,9 +902,9 @@ func TestApplyOpenAIFastPolicyToBody_NonStringServiceTier(t *testing.T) {
// atomic.Pointer[string] on every successful response.create frame.
//
// This test pins the four legs of the semantic contract:
// - turn 1: service_tier=priority hits the default whitelist filter, so
// - turn 1: service_tier=priority hits the explicit filter rule, so
// after filter the upstream sees no tier → billing is nil.
// - turn 2: service_tier=flex passes (default rule targets priority only),
// - turn 2: service_tier=flex passes (the filter rule targets priority only),
// billing should now reflect "flex".
// - turn 3: response.create without any service_tier — the upstream will
// treat it as default; we choose to mirror that and overwrite billing
@ -900,7 +912,7 @@ func TestApplyOpenAIFastPolicyToBody_NonStringServiceTier(t *testing.T) {
// - non-response.create frame (response.cancel here) carrying a stray
// service_tier-shaped field must NOT clobber the billing pointer.
func TestPassthroughBilling_MultiTurnServiceTierFollowsFilteredFrames(t *testing.T) {
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
// Mirror the production filter closure (openai_ws_v2_passthrough_adapter.go

View File

@ -6241,7 +6241,7 @@ func writeOpenAIFastPolicyBlockedResponse(c *gin.Context, err *OpenAIFastBlocked
// applyOpenAIFastPolicyToBody contract but operates on a Realtime/Responses
// WS payload:
//
// - pass: returns frame unchanged (newBytes == frame, blocked == nil)
// - pass: keeps service_tier, normalizing aliases such as "fast" to "priority"
// - filter: returns a copy with top-level service_tier removed
// - block: returns (frame, *OpenAIFastBlockedError)
//
@ -6305,7 +6305,14 @@ func (s *OpenAIGatewayService) applyOpenAIFastPolicyToWSResponseCreate(
}
return trimmed, nil, nil
default:
return frame, nil, nil
if normTier == rawTier {
return frame, nil, nil
}
updated, err := sjson.SetBytes(frame, "service_tier", normTier)
if err != nil {
return frame, nil, fmt.Errorf("normalize service_tier in ws frame: %w", err)
}
return updated, nil, nil
}
}

View File

@ -267,9 +267,8 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
// omits "model" — Realtime clients are allowed to send response.create
// without re-stating the model, in which case the upstream uses the model
// negotiated at session.update time. Without this fallback, an empty
// model would miss the default ["gpt-5.5","gpt-5.5*"] whitelist and be
// silently passed through, defeating the policy on every frame after
// the first.
// model would miss any admin-configured model whitelist and be silently
// passed through, defeating that policy on every frame after the first.
capturedSessionModel := openAIWSPassthroughPolicyModelForFrame(account, firstClientMessage)
initialRequestModel := ""
if hooks != nil {

View File

@ -491,25 +491,10 @@ type OpenAIFastPolicySettings struct {
}
// DefaultOpenAIFastPolicySettings 返回默认的 OpenAI fast 策略配置。
// 默认对所有模型的 priorityfast请求执行 filter即剔除 service_tier 字段,
// 让上游按 normal 优先级处理。
//
// 为什么 ModelWhitelist 为空(=对所有模型生效):
// codex 客户端的 service_tier=fast 是用户级开关,与 model 字段正交。即使
// 用户使用 gpt-4 + fastpriority 配额仍会被消耗。如果默认规则只锁
// gpt-5.5*"用 gpt-4 + fast 透传 priority 上游" 这条路径就会绕过策略。
// 与 codex 真实语义对齐,默认对所有模型生效;管理员若需要只针对特定
// 模型,可在 admin UI 中显式配置 model_whitelist。
// 默认不配置任何规则,保留 OpenAI 上游 service_tier 语义;管理员如需
// 限制 priority/flex可以在 admin UI 中显式配置 filter 或 block 规则。
func DefaultOpenAIFastPolicySettings() *OpenAIFastPolicySettings {
return &OpenAIFastPolicySettings{
Rules: []OpenAIFastPolicyRule{
{
ServiceTier: OpenAIFastTierPriority,
Action: BetaPolicyActionFilter,
Scope: BetaPolicyScopeAll,
ModelWhitelist: []string{},
FallbackAction: BetaPolicyActionPass,
},
},
Rules: []OpenAIFastPolicyRule{},
}
}