From 126ac24c8cb4d98ff2a02f46f0008201cd419c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E4=BD=B3=E6=9E=97?= Date: Fri, 4 Sep 2026 10:14:29 +0800 Subject: [PATCH] feat(codex): support ultrafast service tier Accept service_tier=ultrafast on the OpenAI/Codex gateway, advertise it on GPT-5.6 Sol in the Codex model catalog, and include it in ChatGPT OAuth routing hints so Codex clients can select Ultrafast. --- .../handler/admin/admin_helpers_test.go | 6 ++- .../openai_service_tier_validation_test.go | 2 +- backend/internal/service/billing_service.go | 2 +- .../internal/service/billing_service_test.go | 1 + .../channel_pricing_multipliers_test.go | 2 + .../service/openai_codex_models_service.go | 33 +++++++++++----- .../openai_codex_models_service_test.go | 32 +++++++++++++++- .../service/openai_fast_policy_test.go | 38 +++++++++++++++---- .../service/openai_fast_policy_ws_test.go | 2 +- .../service/openai_fast_service_tier_test.go | 2 +- .../openai_gateway_chat_completions_test.go | 5 +++ .../openai_gateway_record_usage_test.go | 7 ++-- .../service/openai_gateway_request_body.go | 14 +++---- .../internal/service/openai_routing_hint.go | 12 +++--- .../service/openai_routing_hint_test.go | 3 ++ backend/internal/service/setting_features.go | 2 +- backend/internal/service/settings_view.go | 10 +++-- frontend/src/api/admin/settings.ts | 2 +- .../admin/usage/__tests__/UsageTable.spec.ts | 1 + .../__tests__/openaiFastPolicyLocales.spec.ts | 2 + .../__tests__/usageServiceTierLocales.spec.ts | 2 + .../src/i18n/locales/en/admin/settings.ts | 3 +- frontend/src/i18n/locales/en/dashboard.ts | 1 + .../src/i18n/locales/zh/admin/settings.ts | 3 +- frontend/src/i18n/locales/zh/dashboard.ts | 1 + .../utils/__tests__/usageServiceTier.spec.ts | 3 ++ frontend/src/utils/usageServiceTier.ts | 3 +- frontend/src/views/admin/SettingsView.vue | 4 ++ 28 files changed, 149 insertions(+), 49 deletions(-) diff --git a/backend/internal/handler/admin/admin_helpers_test.go b/backend/internal/handler/admin/admin_helpers_test.go index c0775db6b6..980c26bab9 100644 --- a/backend/internal/handler/admin/admin_helpers_test.go +++ b/backend/internal/handler/admin/admin_helpers_test.go @@ -278,13 +278,15 @@ func TestOpenAIFastPolicySettingsFromDTO_NormalizesServiceTier(t *testing.T) { Rules: []dto.OpenAIFastPolicyRule{ {ServiceTier: "priority", Action: "filter", Scope: "all"}, {ServiceTier: "flex", Action: "block", Scope: "oauth"}, + {ServiceTier: "ultrafast", Action: "pass", Scope: "all"}, {ServiceTier: "all", Action: "pass", Scope: "apikey"}, }, } out := openaiFastPolicySettingsFromDTO(in) - require.Len(t, out.Rules, 3) + require.Len(t, out.Rules, 4) require.Equal(t, service.OpenAIFastTierPriority, out.Rules[0].ServiceTier) require.Equal(t, service.OpenAIFastTierFlex, out.Rules[1].ServiceTier) - require.Equal(t, service.OpenAIFastTierAny, out.Rules[2].ServiceTier) + require.Equal(t, service.OpenAIFastTierUltrafast, out.Rules[2].ServiceTier) + require.Equal(t, service.OpenAIFastTierAny, out.Rules[3].ServiceTier) }) } diff --git a/backend/internal/handler/openai_service_tier_validation_test.go b/backend/internal/handler/openai_service_tier_validation_test.go index 97da6f682c..bcd032c3ac 100644 --- a/backend/internal/handler/openai_service_tier_validation_test.go +++ b/backend/internal/handler/openai_service_tier_validation_test.go @@ -17,7 +17,7 @@ import ( // 上以 OpenAI 兼容错误结构返回 HTTP 400。这些用例在 handler 的 service_tier // 校验处短路,不会进入账号选择/重试。 // -// 合法值(fast/priority/flex/auto/default/scale)与省略/null 的接受语义由 +// 合法值(fast/priority/flex/auto/default/scale/ultrafast)与省略/null 的接受语义由 // service 层纯校验函数 TestValidateOpenAIServiceTierField 覆盖,避免 handler // 测试走入真实账号选择/重试路径。 diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 1f3a346f64..35862163ba 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -137,7 +137,7 @@ func usePriorityServiceTierPricing(serviceTier string, pricing *ModelPricing) bo func serviceTierCostMultiplier(serviceTier string) float64 { switch normalizeBillingServiceTier(serviceTier) { - case "priority", "fast": + case "priority", "fast", OpenAIFastTierUltrafast: return 2.0 case "flex": return 0.5 diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 956fe73d33..afcb543695 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -1431,6 +1431,7 @@ func TestCalculateCost_LargeTokenCount(t *testing.T) { func TestServiceTierCostMultiplier(t *testing.T) { require.InDelta(t, 2.0, serviceTierCostMultiplier("priority"), 1e-12) require.InDelta(t, 2.0, serviceTierCostMultiplier(" Priority "), 1e-12) + require.InDelta(t, 2.0, serviceTierCostMultiplier("ultrafast"), 1e-12) require.InDelta(t, 0.5, serviceTierCostMultiplier("flex"), 1e-12) require.InDelta(t, 1.0, serviceTierCostMultiplier(""), 1e-12) require.InDelta(t, 1.0, serviceTierCostMultiplier("default"), 1e-12) diff --git a/backend/internal/service/channel_pricing_multipliers_test.go b/backend/internal/service/channel_pricing_multipliers_test.go index 5b38fddec2..11aea750ff 100644 --- a/backend/internal/service/channel_pricing_multipliers_test.go +++ b/backend/internal/service/channel_pricing_multipliers_test.go @@ -19,6 +19,8 @@ func TestConfiguredServiceTierMultiplier(t *testing.T) { {name: "priority alias", serviceTier: "priority", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2)}, want: 2}, {name: "flex configured", serviceTier: "flex", pricing: &ModelPricing{FlexMultiplier: pricingMultiplier(0.4)}, want: 0.4}, {name: "legacy fast default", serviceTier: "fast", pricing: &ModelPricing{}, want: 2}, + {name: "ultrafast default", serviceTier: "ultrafast", pricing: &ModelPricing{}, want: 2}, + {name: "ultrafast ignores fast multiplier", serviceTier: "ultrafast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2}, {name: "legacy flex default", serviceTier: "flex", pricing: &ModelPricing{}, want: 0.5}, } diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index 6c97eb9b84..32b63e725f 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -469,15 +469,7 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr descriptor.DisplayName = openaiCodexDisplayName(modelID) descriptor.Description = "OpenAI GPT coding model routed through Sub2API." descriptor.SupportsParallelToolCalls = true - if configuredCodexSupportsPriorityServiceTier(modelID) { - descriptor.ServiceTiers = []configuredCodexServiceTier{ - { - ID: "priority", - Name: "Fast", - Description: "Priority processing for lower latency.", - }, - } - } + descriptor.ServiceTiers = configuredCodexServiceTiersForModel(modelID) if isOpenAICodexReasoningGPTModel(modelID) { defaultReasoningLevel := "medium" if getNormalizedCodexModel(modelID) == "gpt-5.6-sol" { @@ -501,6 +493,25 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr return descriptor } +func configuredCodexServiceTiersForModel(modelID string) []configuredCodexServiceTier { + tiers := make([]configuredCodexServiceTier, 0, 2) + if configuredCodexSupportsPriorityServiceTier(modelID) { + tiers = append(tiers, configuredCodexServiceTier{ + ID: OpenAIFastTierPriority, + Name: "Fast", + Description: "Priority processing for lower latency.", + }) + } + if configuredCodexSupportsUltrafastServiceTier(modelID) { + tiers = append(tiers, configuredCodexServiceTier{ + ID: OpenAIFastTierUltrafast, + Name: "Ultrafast", + Description: "Ultra-low latency processing.", + }) + } + return tiers +} + func configuredCodexSupportsPriorityServiceTier(modelID string) bool { normalized := canonicalizeOpenAIModelAliasSpelling(modelID) for _, family := range []string{"gpt-5.4", "gpt-5.5", "gpt-5.6"} { @@ -511,6 +522,10 @@ func configuredCodexSupportsPriorityServiceTier(modelID string) bool { return false } +func configuredCodexSupportsUltrafastServiceTier(modelID string) bool { + return normalizeKnownOpenAICodexModel(modelID) == "gpt-5.6-sol" +} + func configuredCodexGrokReasoningLevels(modelID string) []configuredCodexReasoningLevel { levels := []configuredCodexReasoningLevel{ {Effort: "low", Description: "Fast responses with lighter reasoning"}, diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index 49b5f4e462..66da1d85fa 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -434,7 +434,7 @@ func TestBuildCodexModelsManifestAdvertisesPriorityServiceTierForFastGPTModels(t body, err := BuildCodexModelsManifest([]string{ "gpt-5.4-mini", "gpt-5.5", - "gpt-5.6-sol", + "gpt-5.6-terra", }) require.NoError(t, err) models := decodeCodexManifestModels(t, body) @@ -452,6 +452,36 @@ func TestBuildCodexModelsManifestAdvertisesPriorityServiceTierForFastGPTModels(t } } +// Scenario: GPT-5.6 Sol 在 Fast 之外额外声明 ultrafast service tier。 +func TestBuildCodexModelsManifestAdvertisesUltrafastServiceTierForSol(t *testing.T) { + t.Parallel() + + body, err := BuildCodexModelsManifest([]string{ + "gpt-5.6-sol", + "gpt-5.6", + }) + require.NoError(t, err) + models := decodeCodexManifestModels(t, body) + require.Len(t, models, 2) + + wantTiers := []any{ + map[string]any{ + "id": "priority", + "name": "Fast", + "description": "Priority processing for lower latency.", + }, + map[string]any{ + "id": "ultrafast", + "name": "Ultrafast", + "description": "Ultra-low latency processing.", + }, + } + for _, model := range models { + require.Equal(t, wantTiers, model["service_tiers"]) + require.Nil(t, model["default_service_tier"]) + } +} + // Scenario: 未明确支持 Fast 的型号不推测 service tier。 func TestBuildCodexModelsManifestLeavesServiceTiersEmptyForOtherModels(t *testing.T) { t.Parallel() diff --git a/backend/internal/service/openai_fast_policy_test.go b/backend/internal/service/openai_fast_policy_test.go index 23486ba2f8..a29e7ffd21 100644 --- a/backend/internal/service/openai_fast_policy_test.go +++ b/backend/internal/service/openai_fast_policy_test.go @@ -100,6 +100,9 @@ func TestEvaluateOpenAIFastPolicy_DefaultPassesKnownTiers(t *testing.T) { action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", OpenAIFastTierFlex) require.Equal(t, BetaPolicyActionPass, action) + action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.6-sol", OpenAIFastTierUltrafast) + require.Equal(t, BetaPolicyActionPass, action) + // empty tier → pass action, _ = svc.evaluateOpenAIFastPolicy(context.Background(), account, "gpt-5.5", "") require.Equal(t, BetaPolicyActionPass, action) @@ -248,6 +251,16 @@ func TestApplyOpenAIFastPolicyToBody_UserScopedRuleOverridesGlobalRule(t *testin require.NotContains(t, string(updated), `"service_tier"`) } +func TestApplyOpenAIFastPolicyToBody_PriorityFilterLeavesUltrafast(t *testing.T) { + svc := newOpenAIGatewayServiceWithSettings(t, openAIFastFilterPriorityPolicy()) + account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey} + body := []byte(`{"model":"gpt-5.6-sol","service_tier":"ultrafast"}`) + + updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.6-sol", body) + require.NoError(t, err) + require.Equal(t, OpenAIFastTierUltrafast, gjson.GetBytes(updated, "service_tier").String()) +} + func TestApplyOpenAIFastPolicyToBody_ForcePriorityRewritesKnownTier(t *testing.T) { settings := &OpenAIFastPolicySettings{ Rules: []OpenAIFastPolicyRule{{ @@ -259,7 +272,7 @@ func TestApplyOpenAIFastPolicyToBody_ForcePriorityRewritesKnownTier(t *testing.T svc := newOpenAIGatewayServiceWithSettings(t, settings) account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey} - for _, tier := range []string{"flex", "auto", "default", "scale", "fast", "priority"} { + for _, tier := range []string{"flex", "auto", "default", "scale", "fast", "priority", "ultrafast"} { body := []byte(`{"model":"gpt-5.5","service_tier":"` + tier + `"}`) updated, err := svc.applyOpenAIFastPolicyToBody(context.Background(), account, "gpt-5.5", body) require.NoError(t, err) @@ -480,19 +493,28 @@ func TestSetOpenAIFastPolicySettings_Validation(t *testing.T) { // Valid settings persisted err = svc.SetOpenAIFastPolicySettings(context.Background(), &OpenAIFastPolicySettings{ - Rules: []OpenAIFastPolicyRule{{ - ServiceTier: OpenAIFastTierPriority, - Action: OpenAIFastPolicyActionForcePriority, - Scope: BetaPolicyScopeAll, - UserIDs: []int64{42, 43}, - }}, + Rules: []OpenAIFastPolicyRule{ + { + ServiceTier: OpenAIFastTierPriority, + Action: OpenAIFastPolicyActionForcePriority, + Scope: BetaPolicyScopeAll, + UserIDs: []int64{42, 43}, + }, + { + ServiceTier: OpenAIFastTierUltrafast, + Action: BetaPolicyActionPass, + Scope: BetaPolicyScopeAll, + }, + }, }) require.NoError(t, err) got, err := svc.GetOpenAIFastPolicySettings(context.Background()) require.NoError(t, err) - require.Len(t, got.Rules, 1) + require.Len(t, got.Rules, 2) require.Equal(t, OpenAIFastTierPriority, got.Rules[0].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) } diff --git a/backend/internal/service/openai_fast_policy_ws_test.go b/backend/internal/service/openai_fast_policy_ws_test.go index 607d32a22a..c154f32abf 100644 --- a/backend/internal/service/openai_fast_policy_ws_test.go +++ b/backend/internal/service/openai_fast_policy_ws_test.go @@ -112,7 +112,7 @@ func TestWSResponseCreate_ForcePriorityRewritesKnownTier(t *testing.T) { svc := newOpenAIGatewayServiceWithSettings(t, settings) account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey} - for _, tier := range []string{"flex", "auto", "default", "scale", "fast", "priority"} { + for _, tier := range []string{"flex", "auto", "default", "scale", "fast", "priority", "ultrafast"} { frame := []byte(`{"type":"response.create","model":"gpt-5.5","service_tier":"` + tier + `"}`) updated, blocked, err := svc.applyOpenAIFastPolicyToWSResponseCreate(context.Background(), account, "gpt-5.5", frame) require.NoError(t, err) diff --git a/backend/internal/service/openai_fast_service_tier_test.go b/backend/internal/service/openai_fast_service_tier_test.go index 69ea1372ee..96a4f8ddfa 100644 --- a/backend/internal/service/openai_fast_service_tier_test.go +++ b/backend/internal/service/openai_fast_service_tier_test.go @@ -43,7 +43,7 @@ func TestValidateOpenAIServiceTierField(t *testing.T) { }) t.Run("official tiers pass through", func(t *testing.T) { - for _, tier := range []string{"flex", "auto", "default", "scale"} { + for _, tier := range []string{"flex", "auto", "default", "scale", "ultrafast"} { norm, err := ValidateOpenAIServiceTierField([]byte(`{"model":"gpt-5.5","service_tier":"` + tier + `"}`)) require.NoError(t, err, "tier %q must be accepted", tier) require.Equal(t, tier, norm) diff --git a/backend/internal/service/openai_gateway_chat_completions_test.go b/backend/internal/service/openai_gateway_chat_completions_test.go index 7225e11cf5..35f6de0ed1 100644 --- a/backend/internal/service/openai_gateway_chat_completions_test.go +++ b/backend/internal/service/openai_gateway_chat_completions_test.go @@ -148,6 +148,11 @@ func TestNormalizeResponsesBodyServiceTier(t *testing.T) { require.Equal(t, "scale", tier) require.Equal(t, "scale", gjson.GetBytes(body, "service_tier").String()) + body, tier, err = normalizeResponsesBodyServiceTier([]byte(`{"model":"gpt-5.6-sol","service_tier":"ultrafast"}`)) + require.NoError(t, err) + require.Equal(t, "ultrafast", tier) + require.Equal(t, "ultrafast", gjson.GetBytes(body, "service_tier").String()) + // 真未知值才会被删除。 body, tier, err = normalizeResponsesBodyServiceTier([]byte(`{"model":"gpt-5.1","service_tier":"turbo"}`)) require.NoError(t, err) diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index 1973bba56e..b3f4153784 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -1466,9 +1466,8 @@ func TestNormalizeOpenAIServiceTier(t *testing.T) { t.Run("openai official tiers preserved", func(t *testing.T) { // OpenAI 官方文档定义的合法 tier 值都应被透传保留,避免因白名单过窄 - // 静默剥离客户端显式发送的合法字段。Codex 客户端只发 priority/flex, - // 所以扩大白名单对 Codex 流量零影响(见 codex-rs/core/src/client.rs)。 - for _, tier := range []string{"priority", "flex", "auto", "default", "scale"} { + // 静默剥离客户端显式发送的合法字段。Codex 会发 priority/flex/ultrafast。 + for _, tier := range []string{"priority", "flex", "auto", "default", "scale", "ultrafast"} { got := normalizeOpenAIServiceTier(tier) require.NotNil(t, got, "tier %q should not be normalized to nil", tier) require.Equal(t, tier, *got) @@ -1487,6 +1486,7 @@ func TestExtractOpenAIServiceTier(t *testing.T) { require.Equal(t, "auto", *extractOpenAIServiceTier(map[string]any{"service_tier": "auto"})) require.Equal(t, "default", *extractOpenAIServiceTier(map[string]any{"service_tier": "default"})) require.Equal(t, "scale", *extractOpenAIServiceTier(map[string]any{"service_tier": "scale"})) + require.Equal(t, "ultrafast", *extractOpenAIServiceTier(map[string]any{"service_tier": "ultrafast"})) require.Nil(t, extractOpenAIServiceTier(map[string]any{"service_tier": 1})) require.Nil(t, extractOpenAIServiceTier(nil)) } @@ -1497,6 +1497,7 @@ func TestExtractOpenAIServiceTierFromBody(t *testing.T) { require.Equal(t, "auto", *extractOpenAIServiceTierFromBody([]byte(`{"service_tier":"auto"}`))) require.Equal(t, "default", *extractOpenAIServiceTierFromBody([]byte(`{"service_tier":"default"}`))) require.Equal(t, "scale", *extractOpenAIServiceTierFromBody([]byte(`{"service_tier":"scale"}`))) + require.Equal(t, "ultrafast", *extractOpenAIServiceTierFromBody([]byte(`{"service_tier":"ultrafast"}`))) require.Nil(t, extractOpenAIServiceTierFromBody([]byte(`{"service_tier":"turbo"}`))) require.Nil(t, extractOpenAIServiceTierFromBody(nil)) } diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index 46f69b54d6..b137005a59 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -1435,12 +1435,12 @@ func normalizeOpenAIServiceTier(raw string) *string { if value == "fast" { value = "priority" } - // 放过 OpenAI 官方文档定义的所有合法 tier 值:priority/flex/auto/default/scale。 - // 对 Codex 客户端零影响(Codex 只发 priority 或 flex,见 codex-rs/core/src/client.rs), - // 但能让直连 OpenAI SDK 的用户透传 auto/default/scale 以便抓包/调试。 - // 真未知值仍返回 nil,由 normalizeResponsesBodyServiceTier 从 body 中删除。 + // 放过 OpenAI 官方文档定义的合法 tier 值,以及 Codex/API 新增的 ultrafast。 + // Codex 客户端会发 priority、flex 或 ultrafast;直连 OpenAI SDK 的用户还会 + // 透传 auto/default/scale。真未知值仍返回 nil,由 + // normalizeResponsesBodyServiceTier 从 body 中删除。 switch value { - case "priority", "flex", "auto", "default", "scale": + case "priority", "flex", "auto", "default", "scale", OpenAIFastTierUltrafast: return &value default: return nil @@ -1457,7 +1457,7 @@ type ErrInvalidOpenAIServiceTier struct { } func (e *ErrInvalidOpenAIServiceTier) Error() string { - return fmt.Sprintf("invalid service_tier %q: must be one of auto, default, fast, flex, priority, scale", e.Value) + return fmt.Sprintf("invalid service_tier %q: must be one of auto, default, fast, flex, priority, scale, ultrafast", e.Value) } const invalidOpenAIServiceTierValueMaxLen = 64 @@ -1475,7 +1475,7 @@ func boundInvalidOpenAIServiceTierValue(raw string) string { // - absent / null → valid, returns "" (field omitted keeps current behavior) // - "fast" → normalized to "priority" (the two are equivalent; the canonical // value is what reaches the OpenAI upstream) -// - "priority" / "flex" / "auto" / "default" / "scale" → valid, returned as-is +// - "priority" / "flex" / "auto" / "default" / "scale" / "ultrafast" → valid, returned as-is // - an explicitly present non-string value, an empty string, or any other // unknown value → *ErrInvalidOpenAIServiceTier (handler maps to HTTP 400), // matching OpenAI's enum validation semantics diff --git a/backend/internal/service/openai_routing_hint.go b/backend/internal/service/openai_routing_hint.go index 85fd93314d..c66b941149 100644 --- a/backend/internal/service/openai_routing_hint.go +++ b/backend/internal/service/openai_routing_hint.go @@ -37,14 +37,14 @@ func setOpenAICodexRoutingHint(headers http.Header, account *Account, model stri // Codex treats "default" as an explicit standard-routing sentinel, not as a // service tier sent to the backend. Fast follows the gateway's existing - // canonicalization and therefore becomes "priority"; flex stays "flex". + // canonicalization and therefore becomes "priority"; flex and ultrafast + // stay as themselves. canonicalTier := normalizedOpenAIServiceTierValue(serviceTier) - // This backport has no Codex model-catalog snapshot with which to validate - // arbitrary tier ids. Keep the hint to the two effective tiers Codex itself - // selects; default, missing, and other gateway-compatible API values remain - // model-only rather than expanding the ChatGPT routing protocol here. + // Keep the hint to the effective tiers Codex itself selects. default, + // missing, auto, and scale remain model-only rather than expanding the + // ChatGPT routing protocol here. switch canonicalTier { - case OpenAIFastTierPriority, OpenAIFastTierFlex: + case OpenAIFastTierPriority, OpenAIFastTierFlex, OpenAIFastTierUltrafast: default: canonicalTier = "" } diff --git a/backend/internal/service/openai_routing_hint_test.go b/backend/internal/service/openai_routing_hint_test.go index 5801b15fa4..343d227357 100644 --- a/backend/internal/service/openai_routing_hint_test.go +++ b/backend/internal/service/openai_routing_hint_test.go @@ -24,6 +24,7 @@ func TestSetOpenAICodexRoutingHintCanonicalizesOfficialServiceTiers(t *testing.T {name: "fast alias", model: "gpt-5.6", serviceTier: " fast ", want: "model=gpt-5.6;tier=priority"}, {name: "priority", model: "gpt-5.6", serviceTier: "priority", want: "model=gpt-5.6;tier=priority"}, {name: "flex", model: "gpt-5.6", serviceTier: "flex", want: "model=gpt-5.6;tier=flex"}, + {name: "ultrafast", model: "gpt-5.6", serviceTier: "ultrafast", want: "model=gpt-5.6;tier=ultrafast"}, {name: "explicit default sentinel", model: "gpt-5.6", serviceTier: "default", want: "model=gpt-5.6"}, {name: "omitted tier", model: "gpt-5.6", want: "model=gpt-5.6"}, {name: "auto is not expanded without catalog support", model: "gpt-5.6", serviceTier: "auto", want: "model=gpt-5.6"}, @@ -90,6 +91,7 @@ func TestOpenAIOAuthHTTPBuildersSendRoutingHintFromFinalBody(t *testing.T) { }{ {name: "fast", body: []byte(`{"model":"gpt-5.6-codex","service_tier":"fast"}`), want: "model=gpt-5.6-codex;tier=priority"}, {name: "flex", body: []byte(`{"model":"gpt-5.6-codex","service_tier":"flex"}`), want: "model=gpt-5.6-codex;tier=flex"}, + {name: "ultrafast", body: []byte(`{"model":"gpt-5.6-codex","service_tier":"ultrafast"}`), want: "model=gpt-5.6-codex;tier=ultrafast"}, {name: "default", body: []byte(`{"model":"gpt-5.6-codex","service_tier":"default"}`), want: "model=gpt-5.6-codex"}, {name: "omitted", body: []byte(`{"model":"gpt-5.6-codex"}`), want: "model=gpt-5.6-codex"}, } @@ -216,6 +218,7 @@ func TestBuildOpenAIWSHeadersSendsOAuthRoutingHintOnly(t *testing.T) { }, } require.Equal(t, "model=gpt-5.6-codex;tier=priority", build(t, oauthAccount, "fast").Get(openAICodexRoutingHintHeader)) + require.Equal(t, "model=gpt-5.6-codex;tier=ultrafast", build(t, oauthAccount, "ultrafast").Get(openAICodexRoutingHintHeader)) require.Equal(t, "model=gpt-5.6-codex", build(t, oauthAccount, "default").Get(openAICodexRoutingHintHeader)) require.Empty(t, build(t, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, "priority").Get(openAICodexRoutingHintHeader)) } diff --git a/backend/internal/service/setting_features.go b/backend/internal/service/setting_features.go index 88b1ea4161..3827790fe8 100644 --- a/backend/internal/service/setting_features.go +++ b/backend/internal/service/setting_features.go @@ -1016,7 +1016,7 @@ func (s *SettingService) SetOpenAIFastPolicySettings(ctx context.Context, settin BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true, } validTiers := map[string]bool{ - OpenAIFastTierAny: true, OpenAIFastTierPriority: true, OpenAIFastTierFlex: true, + OpenAIFastTierAny: true, OpenAIFastTierPriority: true, OpenAIFastTierUltrafast: true, OpenAIFastTierFlex: true, } for i, rule := range settings.Rules { diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 002560e822..9b6932934d 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -669,15 +669,17 @@ func DefaultBetaPolicySettings() *BetaPolicySettings { // OpenAI Fast Policy 策略常量 // OpenAI 的 "fast 模式" 通过请求体中的 service_tier 字段识别: // - "priority"(客户端可传 "fast",归一化为 "priority"):fast 模式 +// - "ultrafast":Codex/API 的 Ultrafast 档位 // - "flex":低优先级模式 // - 省略:normal 默认 // // 本策略复用 BetaPolicyAction*/BetaPolicyScope* 常量语义,只是匹配键从 // anthropic-beta header 换成 body 的 service_tier 字段。 const ( - OpenAIFastTierAny = "all" // 匹配任意已识别的 service_tier - OpenAIFastTierPriority = "priority" // 仅匹配 fast(priority) - OpenAIFastTierFlex = "flex" // 仅匹配 flex + OpenAIFastTierAny = "all" // 匹配任意已识别的 service_tier + OpenAIFastTierPriority = "priority" // 仅匹配 fast(priority) + OpenAIFastTierUltrafast = "ultrafast" // 仅匹配 ultrafast + OpenAIFastTierFlex = "flex" // 仅匹配 flex // OpenAIFastPolicyActionForcePriority 会保留 service_tier 字段并强制写成 // priority,用于把 flex/auto/default/scale 等已识别 tier 收敛为 fast。 @@ -686,7 +688,7 @@ const ( // OpenAIFastPolicyRule 单条 OpenAI fast/flex 策略规则 type OpenAIFastPolicyRule struct { - ServiceTier string `json:"service_tier"` // "priority" | "flex" | "auto" | "default" | "scale" | "all" + ServiceTier string `json:"service_tier"` // "priority" | "ultrafast" | "flex" | "auto" | "default" | "scale" | "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 所属用户 diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 29e1550c3b..3c8d608637 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -1429,7 +1429,7 @@ export async function updateRectifierSettings( * Matches backend dto.OpenAIFastPolicyRule. */ export interface OpenAIFastPolicyRule { - service_tier: "all" | "priority" | "flex"; + service_tier: "all" | "priority" | "flex" | "ultrafast"; action: "pass" | "filter" | "block" | "force_priority"; scope: "all" | "oauth" | "apikey" | "bedrock"; user_ids?: number[]; diff --git a/frontend/src/components/admin/usage/__tests__/UsageTable.spec.ts b/frontend/src/components/admin/usage/__tests__/UsageTable.spec.ts index 2bbbef6147..5f920b493d 100644 --- a/frontend/src/components/admin/usage/__tests__/UsageTable.spec.ts +++ b/frontend/src/components/admin/usage/__tests__/UsageTable.spec.ts @@ -30,6 +30,7 @@ const messages: Record = { 'usage.perMillionTokens': '/ 1M tokens', 'usage.serviceTier': 'Service tier', 'usage.serviceTierPriority': 'Fast', + 'usage.serviceTierUltrafast': 'Ultrafast', 'usage.serviceTierFlex': 'Flex', 'usage.serviceTierStandard': 'Standard', 'usage.rate': 'Rate', diff --git a/frontend/src/i18n/__tests__/openaiFastPolicyLocales.spec.ts b/frontend/src/i18n/__tests__/openaiFastPolicyLocales.spec.ts index 805d356c9f..422db45776 100644 --- a/frontend/src/i18n/__tests__/openaiFastPolicyLocales.spec.ts +++ b/frontend/src/i18n/__tests__/openaiFastPolicyLocales.spec.ts @@ -31,6 +31,7 @@ describe('OpenAI Fast/Flex policy locale keys', () => { it('describes target and other-model actions without whitelist terminology', () => { expect(zh.admin.settings.openaiFastPolicy).toMatchObject({ tierAll: '全部 tier 值', + tierUltrafast: 'ultrafast', modelWhitelist: '目标模型', fallbackAction: '其他模型处理方式', summaryTargetModels: '目标模型', @@ -43,6 +44,7 @@ describe('OpenAI Fast/Flex policy locale keys', () => { expect(en.admin.settings.openaiFastPolicy).toMatchObject({ tierAll: 'All tier values', + tierUltrafast: 'ultrafast', modelWhitelist: 'Target models', fallbackAction: 'Other models action', summaryTargetModels: 'Target models', diff --git a/frontend/src/i18n/__tests__/usageServiceTierLocales.spec.ts b/frontend/src/i18n/__tests__/usageServiceTierLocales.spec.ts index ecd191f214..e0dcc6db51 100644 --- a/frontend/src/i18n/__tests__/usageServiceTierLocales.spec.ts +++ b/frontend/src/i18n/__tests__/usageServiceTierLocales.spec.ts @@ -7,6 +7,7 @@ describe('usage service tier locale keys', () => { it('contains zh labels for service tier tooltip', () => { expect(zh.usage.serviceTier).toBe('服务档位') expect(zh.usage.serviceTierPriority).toBe('Fast') + expect(zh.usage.serviceTierUltrafast).toBe('Ultrafast') expect(zh.usage.serviceTierFlex).toBe('Flex') expect(zh.usage.serviceTierStandard).toBe('Standard') }) @@ -14,6 +15,7 @@ describe('usage service tier locale keys', () => { it('contains en labels for service tier tooltip', () => { expect(en.usage.serviceTier).toBe('Service tier') expect(en.usage.serviceTierPriority).toBe('Fast') + expect(en.usage.serviceTierUltrafast).toBe('Ultrafast') expect(en.usage.serviceTierFlex).toBe('Flex') expect(en.usage.serviceTierStandard).toBe('Standard') }) diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index 79125850c4..a40e9f962b 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -1081,7 +1081,7 @@ export default { }, openaiFastPolicy: { title: 'OpenAI Fast/Flex Policy', - description: 'Intercept, filter, or pass OpenAI fast(priority) / 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.', empty: 'No rules configured. Click the button below to add one.', ruleHeader: 'Rule #{index}', removeRule: 'Remove rule', @@ -1090,6 +1090,7 @@ export default { serviceTier: 'service_tier match', tierAll: 'All tier values', tierPriority: 'priority (fast)', + tierUltrafast: 'ultrafast', tierFlex: 'flex', action: 'Action', actionPass: 'Pass (keep service_tier)', diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts index 576067c6a3..9b56d13d5a 100644 --- a/frontend/src/i18n/locales/en/dashboard.ts +++ b/frontend/src/i18n/locales/en/dashboard.ts @@ -398,6 +398,7 @@ export default { cacheWrite: 'Write', serviceTier: 'Service tier', serviceTierPriority: 'Fast', + serviceTierUltrafast: 'Ultrafast', serviceTierFlex: 'Flex', serviceTierStandard: 'Standard', rate: 'Rate', diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index 07898e7a94..78a76ff310 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -1075,7 +1075,7 @@ export default { }, openaiFastPolicy: { title: 'OpenAI Fast/Flex 策略', - description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority) 与 flex 请求;仅作用于 OpenAI 网关。', + description: '基于请求体 service_tier 字段拦截/过滤/透传 OpenAI fast(priority)、ultrafast 与 flex 请求;仅作用于 OpenAI 网关。', empty: '尚未配置任何规则。点击下方按钮新增。', ruleHeader: '规则 #{index}', removeRule: '删除规则', @@ -1084,6 +1084,7 @@ export default { serviceTier: 'service_tier 匹配', tierAll: '全部 tier 值', tierPriority: 'priority(fast)', + tierUltrafast: 'ultrafast', tierFlex: 'flex', action: '处理方式', actionPass: '透传(保留 service_tier)', diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts index 60117abbbc..8b67863441 100644 --- a/frontend/src/i18n/locales/zh/dashboard.ts +++ b/frontend/src/i18n/locales/zh/dashboard.ts @@ -403,6 +403,7 @@ export default { cacheWrite: '写入', serviceTier: '服务档位', serviceTierPriority: 'Fast', + serviceTierUltrafast: 'Ultrafast', serviceTierFlex: 'Flex', serviceTierStandard: 'Standard', rate: '倍率', diff --git a/frontend/src/utils/__tests__/usageServiceTier.spec.ts b/frontend/src/utils/__tests__/usageServiceTier.spec.ts index e8a039f10d..a2bd9b7a7f 100644 --- a/frontend/src/utils/__tests__/usageServiceTier.spec.ts +++ b/frontend/src/utils/__tests__/usageServiceTier.spec.ts @@ -12,6 +12,7 @@ describe('usageServiceTier utils', () => { it('preserves supported tiers', () => { expect(normalizeUsageServiceTier('priority')).toBe('priority') expect(normalizeUsageServiceTier('flex')).toBe('flex') + expect(normalizeUsageServiceTier('ultrafast')).toBe('ultrafast') }) it('formats empty values as standard', () => { @@ -27,11 +28,13 @@ describe('usageServiceTier utils', () => { it('maps tiers to translated labels', () => { const translate = (key: string) => ({ 'usage.serviceTierPriority': 'Fast', + 'usage.serviceTierUltrafast': 'Ultrafast', 'usage.serviceTierFlex': 'Flex', 'usage.serviceTierStandard': 'Standard', })[key] ?? key expect(getUsageServiceTierLabel('fast', translate)).toBe('Fast') + expect(getUsageServiceTierLabel('ultrafast', translate)).toBe('Ultrafast') expect(getUsageServiceTierLabel('flex', translate)).toBe('Flex') expect(getUsageServiceTierLabel(undefined, translate)).toBe('Standard') expect(getUsageServiceTierLabel('custom-tier', translate)).toBe('custom-tier') diff --git a/frontend/src/utils/usageServiceTier.ts b/frontend/src/utils/usageServiceTier.ts index eefce2dd22..bca2398148 100644 --- a/frontend/src/utils/usageServiceTier.ts +++ b/frontend/src/utils/usageServiceTier.ts @@ -3,7 +3,7 @@ export function normalizeUsageServiceTier(serviceTier?: string | null): string | if (!value) return null if (value === 'fast') return 'priority' if (value === 'default' || value === 'standard') return 'standard' - if (value === 'priority' || value === 'flex') return value + if (value === 'priority' || value === 'flex' || value === 'ultrafast') return value return value } @@ -19,6 +19,7 @@ export function getUsageServiceTierLabel( ): string { const tier = formatUsageServiceTier(serviceTier) if (tier === 'priority') return translate('usage.serviceTierPriority') + if (tier === 'ultrafast') return translate('usage.serviceTierUltrafast') if (tier === 'flex') return translate('usage.serviceTierFlex') if (tier === 'standard') return translate('usage.serviceTierStandard') return tier diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 8f6848af96..be9742fc11 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -12084,6 +12084,10 @@ const openaiFastPolicyTierOptions = computed(() => [ value: "priority", label: t("admin.settings.openaiFastPolicy.tierPriority"), }, + { + value: "ultrafast", + label: t("admin.settings.openaiFastPolicy.tierUltrafast"), + }, { value: "flex", label: t("admin.settings.openaiFastPolicy.tierFlex") }, ]);