fix(openai): force priority when service tier is omitted

This commit is contained in:
xuhaihan (cc)
2026-09-11 21:16:12 +08:00
parent 4726bdd08b
commit 8e7954438d
12 changed files with 156 additions and 9 deletions
+24
View File
@@ -227,6 +227,30 @@ underscores_in_headers on;
Nginx 默认会丢弃名称中含下划线的请求头(如 `session_id`),这会导致多账号环境下的粘性会话功能失效。
## Codex Fast/Flex 策略说明
管理员后台的 `系统设置 -> 网关服务 -> OpenAI Fast/Flex 策略` 只负责处理请求体中的 `service_tier`,不会修改 Codex 客户端的模型目录,也不会让 Codex UI 自动出现 Speed 或 `/fast` 选项。
策略支持以下处理方式:
- `pass`:保留客户端传入的 `service_tier`;`fast` 会规范为上游使用的 `priority`。
- `filter`:移除 `service_tier`,按普通优先级请求。
- `block`:拒绝匹配的 Fast/Flex 请求。
- `force_priority`:将匹配请求强制设置为 `priority`,会按 Priority/Fast 价格计费。为避免升级后改变既有规则语义,`all` 只匹配显式存在的 tier;如需让省略 `service_tier` 的 OpenAI 请求也强制升级,必须新增 `service_tier=missing + force_priority` 规则。非 OpenAI 平台不会执行缺省 tier 注入。这种方式可以让请求实际使用 Fast,但 Codex UI 仍可能不显示 Fast 状态。
Codex 的 Fast 入口由客户端模型目录驱动。只有当前模型目录声明了 `additional_speed_tiers: ["fast"]` 和对应的 `service_tiers`,Codex 才会显示 `/fast`。通过 API Key 或自定义模型提供商连接 Sub2API 时,如果模型目录没有这些字段,即使后台配置了 `force_priority`,重启 Codex 后也不会出现 Speed 选项。
客户端可在 `~/.codex/config.toml` 中直接指定默认请求级别:
```toml
service_tier = "fast"
[features]
fast_mode = true
```
其中 `service_tier = "fast"` 会让请求携带 Fast 设置;`features.fast_mode` 只启用客户端 Fast 功能。模型目录没有声明 Fast 能力时,`/fast` 仍可能不显示。可通过 Sub2API 使用记录确认最终 `service_tier` 是否为 `priority`。
---
## 部署方式
@@ -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 所属用户
+1 -1
View File
@@ -1441,7 +1441,7 @@ export async function updateRectifierSettings(
* Matches backend dto.OpenAIFastPolicyRule.
*/
export interface OpenAIFastPolicyRule {
service_tier: "all" | "priority" | "flex" | "ultrafast";
service_tier: "all" | "priority" | "flex" | "ultrafast" | "missing";
action: "pass" | "filter" | "block" | "force_priority";
scope: "all" | "oauth" | "apikey" | "bedrock";
user_ids?: number[];
@@ -32,6 +32,7 @@ describe('OpenAI Fast/Flex policy locale keys', () => {
expect(zh.admin.settings.openaiFastPolicy).toMatchObject({
tierAll: '全部 tier 值',
tierUltrafast: 'ultrafast',
tierMissing: '省略 tier',
modelWhitelist: '目标模型',
fallbackAction: '其他模型处理方式',
summaryTargetModels: '目标模型',
@@ -45,6 +46,7 @@ describe('OpenAI Fast/Flex policy locale keys', () => {
expect(en.admin.settings.openaiFastPolicy).toMatchObject({
tierAll: 'All tier values',
tierUltrafast: 'ultrafast',
tierMissing: 'Omitted tier',
modelWhitelist: 'Target models',
fallbackAction: 'Other models action',
summaryTargetModels: 'Target models',
@@ -1100,7 +1100,7 @@ export default {
},
openaiFastPolicy: {
title: 'OpenAI Fast/Flex Policy',
description: 'Intercept, filter, or pass OpenAI fast(priority), ultrafast, or flex requests based on the request body service_tier field. Applies to the OpenAI gateway only.',
description: 'Intercept, filter, or pass OpenAI fast(priority), ultrafast, or flex requests based on the request body service_tier field. Applies to the OpenAI gateway only. "All tier values" includes explicitly sent tiers only.',
empty: 'No rules configured. Click the button below to add one.',
ruleHeader: 'Rule #{index}',
removeRule: 'Remove rule',
@@ -1108,6 +1108,7 @@ export default {
saveHint: 'Saved together with system settings (click the global Save button at the bottom of the page).',
serviceTier: 'service_tier match',
tierAll: 'All tier values',
tierMissing: 'Omitted tier',
tierPriority: 'priority (fast)',
tierUltrafast: 'ultrafast',
tierFlex: 'flex',
@@ -1094,7 +1094,7 @@ export default {
},
openaiFastPolicy: {
title: 'OpenAI Fast/Flex 策略',
description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority)、ultrafast 与 flex 请求;仅作用于 OpenAI 网关。',
description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority)、ultrafast 与 flex 请求;仅作用于 OpenAI 网关。“全部 tier 值”仅包含显式传入的 tier。',
empty: '尚未配置任何规则。点击下方按钮新增。',
ruleHeader: '规则 #{index}',
removeRule: '删除规则',
@@ -1102,6 +1102,7 @@ export default {
saveHint: '保存时随系统设置一起提交(点击页面底部「保存」按钮)。',
serviceTier: 'service_tier 匹配',
tierAll: '全部 tier 值',
tierMissing: '省略 tier',
tierPriority: 'priority(fast)',
tierUltrafast: 'ultrafast',
tierFlex: 'flex',
@@ -1184,6 +1184,7 @@
| 'all'
| 'priority'
| 'flex'
| 'missing'
"
:options="openaiFastPolicyTierOptions"
/>
@@ -12179,6 +12180,7 @@ const openaiFastPolicyTierOptions = computed(() => [
label: t("admin.settings.openaiFastPolicy.tierUltrafast"),
},
{ value: "flex", label: t("admin.settings.openaiFastPolicy.tierFlex") },
{ value: "missing", label: t("admin.settings.openaiFastPolicy.tierMissing") },
]);
const openaiFastPolicyActionOptions = computed(() => [