fix(openai): pass service_tier by default
This commit is contained in:
parent
18790386a7
commit
e9637148dd
@ -784,14 +784,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"payment_visible_method_wxpay_enabled": false,
|
"payment_visible_method_wxpay_enabled": false,
|
||||||
"openai_advanced_scheduler_enabled": true,
|
"openai_advanced_scheduler_enabled": true,
|
||||||
"openai_fast_policy_settings": {
|
"openai_fast_policy_settings": {
|
||||||
"rules": [
|
"rules": []
|
||||||
{
|
|
||||||
"service_tier": "priority",
|
|
||||||
"action": "filter",
|
|
||||||
"scope": "all",
|
|
||||||
"fallback_action": "pass"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"custom_menu_items": [],
|
"custom_menu_items": [],
|
||||||
"custom_endpoints": [],
|
"custom_endpoints": [],
|
||||||
@ -999,14 +992,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"payment_visible_method_wxpay_enabled": false,
|
"payment_visible_method_wxpay_enabled": false,
|
||||||
"openai_advanced_scheduler_enabled": false,
|
"openai_advanced_scheduler_enabled": false,
|
||||||
"openai_fast_policy_settings": {
|
"openai_fast_policy_settings": {
|
||||||
"rules": [
|
"rules": []
|
||||||
{
|
|
||||||
"service_tier": "priority",
|
|
||||||
"action": "filter",
|
|
||||||
"scope": "all",
|
|
||||||
"fallback_action": "pass"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"payment_enabled": false,
|
"payment_enabled": false,
|
||||||
"payment_min_amount": 0,
|
"payment_min_amount": 0,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
type openAIFastPolicyRepoStub struct {
|
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())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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)
|
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)
|
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)
|
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)
|
action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", OpenAIFastTierFlex)
|
||||||
require.Equal(t, BetaPolicyActionPass, action)
|
require.Equal(t, BetaPolicyActionPass, action)
|
||||||
|
|
||||||
@ -129,27 +138,24 @@ func TestEvaluateOpenAIFastPolicy_ScopeFiltersOAuth(t *testing.T) {
|
|||||||
require.Equal(t, BetaPolicyActionPass, action)
|
require.Equal(t, BetaPolicyActionPass, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyOpenAIFastPolicyToBody_FilterRemovesField(t *testing.T) {
|
func TestApplyOpenAIFastPolicyToBody_DefaultPassesPriorityAndFast(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
||||||
|
|
||||||
// gpt-5.5 fast → service_tier stripped
|
|
||||||
body := []byte(`{"model":"gpt-5.5","service_tier":"priority","messages":[]}`)
|
body := []byte(`{"model":"gpt-5.5","service_tier":"priority","messages":[]}`)
|
||||||
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
|
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
|
||||||
require.NoError(t, err)
|
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"}`)
|
body = []byte(`{"model":"gpt-5.5","service_tier":"fast"}`)
|
||||||
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
|
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotContains(t, string(updated), `"service_tier"`)
|
require.Equal(t, "priority", gjson.GetBytes(updated, "service_tier").String())
|
||||||
|
|
||||||
// gpt-4 priority → 默认策略对所有模型 filter,service_tier 被移除
|
|
||||||
body = []byte(`{"model":"gpt-4","service_tier":"priority"}`)
|
body = []byte(`{"model":"gpt-4","service_tier":"priority"}`)
|
||||||
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-4", body)
|
updated, err = svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-4", body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotContains(t, string(updated), `"service_tier"`)
|
require.Equal(t, string(body), string(updated))
|
||||||
|
|
||||||
// No service_tier → no-op
|
// No service_tier → no-op
|
||||||
body = []byte(`{"model":"gpt-5.5"}`)
|
body = []byte(`{"model":"gpt-5.5"}`)
|
||||||
@ -158,9 +164,23 @@ func TestApplyOpenAIFastPolicyToBody_FilterRemovesField(t *testing.T) {
|
|||||||
require.Equal(t, string(body), string(updated))
|
require.Equal(t, string(body), string(updated))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule 验证扩展白名单后
|
func TestApplyOpenAIFastPolicyToBody_ExplicitFilterRemovesField(t *testing.T) {
|
||||||
// 客户端显式发送的 OpenAI 官方合法 tier(auto/default/scale)能透传到上游而不被
|
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
|
||||||
// 静默剥离。默认策略只针对 priority,所以这些 tier 落在 fall-through pass 分支。
|
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) {
|
func TestApplyOpenAIFastPolicyToBody_OfficialTiersBypassDefaultRule(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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)
|
updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body)
|
||||||
require.NoError(t, err, "tier %q should pass without error", tier)
|
require.NoError(t, err, "tier %q should pass without error", tier)
|
||||||
require.Contains(t, string(updated), `"service_tier":"`+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"} {
|
for _, tier := range []string{"auto", "default", "scale"} {
|
||||||
action, _ := svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", tier)
|
action, _ := svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", tier)
|
||||||
require.Equal(t, BetaPolicyActionPass, action, "tier %q should evaluate to pass", tier)
|
require.Equal(t, BetaPolicyActionPass, action, "tier %q should evaluate to pass", tier)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
// --- Helper-level (unit) tests for applyOpenAIFastPolicyToWSResponseCreate ---
|
// --- Helper-level (unit) tests for applyOpenAIFastPolicyToWSResponseCreate ---
|
||||||
|
|
||||||
func TestWSResponseCreate_FilterStripsServiceTier(t *testing.T) {
|
func TestWSResponseCreate_DefaultPassesPriorityAndNormalizesFast(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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)
|
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, blocked)
|
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.
|
// Other fields preserved.
|
||||||
require.Equal(t, "response.create", gjson.GetBytes(updated, "type").String())
|
require.Equal(t, "response.create", gjson.GetBytes(updated, "type").String())
|
||||||
require.Equal(t, "gpt-5.5", gjson.GetBytes(updated, "model").String())
|
require.Equal(t, "gpt-5.5", gjson.GetBytes(updated, "model").String())
|
||||||
require.Equal(t, "hi", gjson.GetBytes(updated, "input.0.text").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) {
|
func TestWSResponseCreate_ExplicitFilterStripsServiceTier(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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":"priority","input":[{"type":"input_text","text":"hi"}]}`)
|
||||||
frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"fast"}`)
|
|
||||||
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, blocked)
|
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)
|
updated, blocked, err = svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, blocked)
|
require.Nil(t, blocked)
|
||||||
@ -60,7 +71,7 @@ func TestWSResponseCreate_FlexPassThrough(t *testing.T) {
|
|||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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"}`)
|
frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"flex"}`)
|
||||||
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -220,8 +231,8 @@ func (f *fakePassthroughFrameConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gpt55WhitelistFastPolicy 返回一份强制带 model whitelist 的策略,用于
|
// gpt55WhitelistFastPolicy 返回一份强制带 model whitelist 的策略,用于
|
||||||
// 验证 capturedSessionModel fallback 的语义(默认策略 whitelist 为空时
|
// 验证 capturedSessionModel fallback 的语义(默认配置没有规则,fallback
|
||||||
// fallback 路径无法被观察到)。
|
// 路径无法被观察到)。
|
||||||
func gpt55WhitelistFastPolicy() *OpenAIFastPolicySettings {
|
func gpt55WhitelistFastPolicy() *OpenAIFastPolicySettings {
|
||||||
return &OpenAIFastPolicySettings{
|
return &OpenAIFastPolicySettings{
|
||||||
Rules: []OpenAIFastPolicyRule{{
|
Rules: []OpenAIFastPolicyRule{{
|
||||||
@ -242,7 +253,7 @@ func gpt55WhitelistFastPolicy() *OpenAIFastPolicySettings {
|
|||||||
// through to the upstream.
|
// through to the upstream.
|
||||||
func TestPolicyEnforcingFrameConn_FollowupFrameWithoutModelUsesCapturedModel(t *testing.T) {
|
func TestPolicyEnforcingFrameConn_FollowupFrameWithoutModelUsesCapturedModel(t *testing.T) {
|
||||||
// 此处特意使用带 whitelist 的策略,以便观察 capturedSessionModel
|
// 此处特意使用带 whitelist 的策略,以便观察 capturedSessionModel
|
||||||
// fallback 是否生效(默认策略 whitelist 为空,fallback 与否结果一致,
|
// fallback 是否生效(默认配置没有规则,fallback 与否结果一致,
|
||||||
// 不能用来覆盖此回归)。
|
// 不能用来覆盖此回归)。
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, gpt55WhitelistFastPolicy())
|
svc := newOpenAIGatewayServiceWithSettings(t, gpt55WhitelistFastPolicy())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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")
|
"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
|
// TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream wires up the
|
||||||
// real ProxyResponsesWebSocketFromClient ingress session pipeline against a
|
// real ProxyResponsesWebSocketFromClient ingress session pipeline against a
|
||||||
// captureConn upstream and asserts that a client frame with service_tier=fast
|
// captureConn upstream and asserts that a client frame with service_tier=fast
|
||||||
// is normalized + filtered out before being written upstream. This is the
|
// is normalized + filtered out by an explicit admin policy before being
|
||||||
// integration flavour of TestWSResponseCreate_FilterStripsServiceTier.
|
// written upstream.
|
||||||
func TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream(t *testing.T) {
|
func TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
@ -345,9 +356,9 @@ func TestWSResponseCreate_IngressFiltersServiceTierBeforeUpstream(t *testing.T)
|
|||||||
pool.setClientDialerForTest(captureDialer)
|
pool.setClientDialerForTest(captureDialer)
|
||||||
|
|
||||||
repo := &openAIFastPolicyRepoStub{values: map[string]string{}}
|
repo := &openAIFastPolicyRepoStub{values: map[string]string{}}
|
||||||
defaultJSON, err := json.Marshal(DefaultOpenAIFastPolicySettings())
|
filterPolicyJSON, err := json.Marshal(openAIFastFilterPriorityPolicy())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
repo.values[SettingKeyOpenAIFastPolicySettings] = string(defaultJSON)
|
repo.values[SettingKeyOpenAIFastPolicySettings] = string(filterPolicyJSON)
|
||||||
|
|
||||||
svc := &OpenAIGatewayService{
|
svc := &OpenAIGatewayService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -631,13 +642,13 @@ func TestApplyOpenAIFastPolicyToBody_BlockShortCircuitsUpstream(t *testing.T) {
|
|||||||
require.Equal(t, string(body), string(updated), "block must not mutate body")
|
require.Equal(t, string(body), string(updated), "block must not mutate body")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestForwardAsAnthropicMessages_BetaFastModeTriggersOpenAIFastPolicy verifies
|
// TestForwardAsAnthropicMessages_BetaFastModePassesOpenAIFastPolicyByDefault
|
||||||
// the Anthropic-compat entrypoint chain: anthropic-beta: fast-mode → BetaFastMode
|
// verifies the Anthropic-compat entrypoint chain: anthropic-beta: fast-mode →
|
||||||
// detection → ServiceTier="priority" injection (openai_gateway_messages.go:60)
|
// BetaFastMode detection → ServiceTier="priority" injection
|
||||||
// → applyOpenAIFastPolicyToBody filter on default policy → upstream body has
|
// (openai_gateway_messages.go:60) → default OpenAI fast policy pass. We
|
||||||
// no service_tier. We exercise the same internal pipeline (Anthropic→Responses
|
// exercise the same internal pipeline (Anthropic→Responses + BetaFastMode +
|
||||||
// + BetaFastMode + policy) without spinning up a real upstream HTTP server.
|
// policy) without spinning up a real upstream HTTP server.
|
||||||
func TestForwardAsAnthropicMessages_BetaFastModeTriggersOpenAIFastPolicy(t *testing.T) {
|
func TestForwardAsAnthropicMessages_BetaFastModePassesOpenAIFastPolicyByDefault(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
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)
|
upstreamBody, policyErr := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", responsesBody)
|
||||||
require.NoError(t, policyErr)
|
require.NoError(t, policyErr)
|
||||||
|
|
||||||
// Step 4: assert that policy filtered the field before the upstream HTTP request.
|
// Step 4: default policy must preserve the explicit fast/priority request.
|
||||||
require.NotContains(t, string(upstreamBody), `"service_tier"`, "default policy 命中 gpt-5.5 priority 应当 filter 掉 service_tier")
|
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 ---
|
// --- 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
|
// tier) instead of the user-requested "priority". This test pins the
|
||||||
// contract those two helpers must uphold for the adapter's billing path.
|
// contract those two helpers must uphold for the adapter's billing path.
|
||||||
func TestPassthroughBilling_PostFilterServiceTier(t *testing.T) {
|
func TestPassthroughBilling_PostFilterServiceTier(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
||||||
|
|
||||||
raw := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"priority"}`)
|
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,
|
require.Equal(t, "priority", *pre,
|
||||||
"sanity: raw first frame carries priority that pre-fix billing would have reported")
|
"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)
|
filtered, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", raw)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, blocked)
|
require.Nil(t, blocked)
|
||||||
@ -890,9 +902,9 @@ func TestApplyOpenAIFastPolicyToBody_NonStringServiceTier(t *testing.T) {
|
|||||||
// atomic.Pointer[string] on every successful response.create frame.
|
// atomic.Pointer[string] on every successful response.create frame.
|
||||||
//
|
//
|
||||||
// This test pins the four legs of the semantic contract:
|
// 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.
|
// 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".
|
// billing should now reflect "flex".
|
||||||
// - turn 3: response.create without any service_tier — the upstream will
|
// - turn 3: response.create without any service_tier — the upstream will
|
||||||
// treat it as default; we choose to mirror that and overwrite billing
|
// 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
|
// - non-response.create frame (response.cancel here) carrying a stray
|
||||||
// service_tier-shaped field must NOT clobber the billing pointer.
|
// service_tier-shaped field must NOT clobber the billing pointer.
|
||||||
func TestPassthroughBilling_MultiTurnServiceTierFollowsFilteredFrames(t *testing.T) {
|
func TestPassthroughBilling_MultiTurnServiceTierFollowsFilteredFrames(t *testing.T) {
|
||||||
svc := newOpenAIGatewayServiceWithSettings(t, DefaultOpenAIFastPolicySettings())
|
svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy())
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
||||||
|
|
||||||
// Mirror the production filter closure (openai_ws_v2_passthrough_adapter.go
|
// Mirror the production filter closure (openai_ws_v2_passthrough_adapter.go
|
||||||
|
|||||||
@ -6098,7 +6098,7 @@ func writeOpenAIFastPolicyBlockedResponse(c *gin.Context, err *OpenAIFastBlocked
|
|||||||
// applyOpenAIFastPolicyToBody contract but operates on a Realtime/Responses
|
// applyOpenAIFastPolicyToBody contract but operates on a Realtime/Responses
|
||||||
// WS payload:
|
// 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
|
// - filter: returns a copy with top-level service_tier removed
|
||||||
// - block: returns (frame, *OpenAIFastBlockedError)
|
// - block: returns (frame, *OpenAIFastBlockedError)
|
||||||
//
|
//
|
||||||
@ -6162,7 +6162,14 @@ func (s *OpenAIGatewayService) applyOpenAIFastPolicyToWSResponseCreate(
|
|||||||
}
|
}
|
||||||
return trimmed, nil, nil
|
return trimmed, nil, nil
|
||||||
default:
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -267,9 +267,8 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
|
|||||||
// omits "model" — Realtime clients are allowed to send response.create
|
// omits "model" — Realtime clients are allowed to send response.create
|
||||||
// without re-stating the model, in which case the upstream uses the model
|
// without re-stating the model, in which case the upstream uses the model
|
||||||
// negotiated at session.update time. Without this fallback, an empty
|
// negotiated at session.update time. Without this fallback, an empty
|
||||||
// model would miss the default ["gpt-5.5","gpt-5.5*"] whitelist and be
|
// model would miss any admin-configured model whitelist and be silently
|
||||||
// silently passed through, defeating the policy on every frame after
|
// passed through, defeating that policy on every frame after the first.
|
||||||
// the first.
|
|
||||||
capturedSessionModel := openAIWSPassthroughPolicyModelForFrame(account, firstClientMessage)
|
capturedSessionModel := openAIWSPassthroughPolicyModelForFrame(account, firstClientMessage)
|
||||||
initialRequestModel := ""
|
initialRequestModel := ""
|
||||||
if hooks != nil {
|
if hooks != nil {
|
||||||
|
|||||||
@ -491,25 +491,10 @@ type OpenAIFastPolicySettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOpenAIFastPolicySettings 返回默认的 OpenAI fast 策略配置。
|
// DefaultOpenAIFastPolicySettings 返回默认的 OpenAI fast 策略配置。
|
||||||
// 默认对所有模型的 priority(fast)请求执行 filter,即剔除 service_tier 字段,
|
// 默认不配置任何规则,保留 OpenAI 上游 service_tier 语义;管理员如需
|
||||||
// 让上游按 normal 优先级处理。
|
// 限制 priority/flex,可以在 admin UI 中显式配置 filter 或 block 规则。
|
||||||
//
|
|
||||||
// 为什么 ModelWhitelist 为空(=对所有模型生效):
|
|
||||||
// codex 客户端的 service_tier=fast 是用户级开关,与 model 字段正交。即使
|
|
||||||
// 用户使用 gpt-4 + fast,priority 配额仍会被消耗。如果默认规则只锁
|
|
||||||
// gpt-5.5*,"用 gpt-4 + fast 透传 priority 上游" 这条路径就会绕过策略。
|
|
||||||
// 与 codex 真实语义对齐,默认对所有模型生效;管理员若需要只针对特定
|
|
||||||
// 模型,可在 admin UI 中显式配置 model_whitelist。
|
|
||||||
func DefaultOpenAIFastPolicySettings() *OpenAIFastPolicySettings {
|
func DefaultOpenAIFastPolicySettings() *OpenAIFastPolicySettings {
|
||||||
return &OpenAIFastPolicySettings{
|
return &OpenAIFastPolicySettings{
|
||||||
Rules: []OpenAIFastPolicyRule{
|
Rules: []OpenAIFastPolicyRule{},
|
||||||
{
|
|
||||||
ServiceTier: OpenAIFastTierPriority,
|
|
||||||
Action: BetaPolicyActionFilter,
|
|
||||||
Scope: BetaPolicyScopeAll,
|
|
||||||
ModelWhitelist: []string{},
|
|
||||||
FallbackAction: BetaPolicyActionPass,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user