mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +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:
@@ -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 {
|
||||
|
||||
@@ -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 所属用户
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
Reference in New Issue
Block a user