Skip to content

Commit 98e8bea

Browse files
committed
fix: treat prolite plan as pro (closes #111)
OpenAI reports the $100 Pro tier as "prolite" — functionally a Pro plan with a smaller usage cap, but the exact-match plan gating meant these accounts fell into the default branch for premium 5h rate limit handling, scheduler score bias, 429 cooldown window detection, and the Spark model's pro-only account filter, so gpt-5.3-codex-spark requests could not be served by prolite accounts. Fold prolite/pro_lite/pro-lite into "pro" via a single NormalizePlanType helper and route existing callers through it. Raw PlanType is preserved on the account for UI display and downstream diagnostics. The frontend usage/filter decisions are normalized in parallel so prolite accounts render Pro-style 5h+7d usage bars and match the "pro" plan filter, while the table still shows the raw label.
1 parent bb6173e commit 98e8bea

6 files changed

Lines changed: 68 additions & 8 deletions

File tree

auth/premium_rate_limit.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,24 @@ import (
1010

1111
const premium5hFallbackWindow = 5 * time.Hour
1212

13+
// NormalizePlanType canonicalizes a plan string for behavior-level comparisons.
14+
// OpenAI reports the $100 Pro tier as "prolite"; functionally it is a Pro plan
15+
// with a smaller usage cap, so we fold it into "pro" so that downstream plan
16+
// gating (premium 5h rate-limit, Spark routing, scheduler bias, 429 cooldown
17+
// window) treats it identically. The raw value is kept in Account.PlanType so
18+
// the UI can still render "prolite" for operator visibility.
19+
func NormalizePlanType(plan string) string {
20+
normalized := strings.ToLower(strings.TrimSpace(plan))
21+
switch normalized {
22+
case "prolite", "pro_lite", "pro-lite":
23+
return "pro"
24+
default:
25+
return normalized
26+
}
27+
}
28+
1329
func normalizePlanType(plan string) string {
14-
return strings.ToLower(strings.TrimSpace(plan))
30+
return NormalizePlanType(plan)
1531
}
1632

1733
func isPremium5hPlan(plan string) bool {

auth/premium_rate_limit_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,38 @@ func TestPremium5hRateLimitedSkipsUsageProbeBeforeReset(t *testing.T) {
7070
}
7171
}
7272

73+
func TestNormalizePlanTypeFoldsProliteIntoPro(t *testing.T) {
74+
cases := map[string]string{
75+
"prolite": "pro",
76+
"ProLite": "pro",
77+
" prolite ": "pro",
78+
"pro_lite": "pro",
79+
"pro-lite": "pro",
80+
"pro": "pro",
81+
"plus": "plus",
82+
"free": "free",
83+
"": "",
84+
}
85+
for input, want := range cases {
86+
if got := NormalizePlanType(input); got != want {
87+
t.Errorf("NormalizePlanType(%q) = %q, want %q", input, got, want)
88+
}
89+
}
90+
}
91+
92+
func TestProliteIsTreatedAsPremium5hPlan(t *testing.T) {
93+
acc := newPremium5hTestAccount("prolite", time.Now().Add(30*time.Minute))
94+
if !acc.IsPremium5hPlan() {
95+
t.Fatal("prolite should be recognized as a premium 5h plan")
96+
}
97+
if !IsPlusOrHigherPlan("prolite") {
98+
t.Fatal("prolite should qualify as plus-or-higher for image routing")
99+
}
100+
if got := defaultScoreBiasForPlan("prolite"); got != 50 {
101+
t.Fatalf("defaultScoreBiasForPlan(prolite) = %d, want 50", got)
102+
}
103+
}
104+
73105
func TestCleanByRuntimeStatusSkipsPremium5hRateLimitedAccount(t *testing.T) {
74106
acc := newPremium5hTestAccount("plus", time.Now().Add(20*time.Minute))
75107
store := &Store{

auth/store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func concurrencyLimitForTier(baseLimit int64, tier AccountHealthTier) int64 {
308308
}
309309

310310
func defaultScoreBiasForPlan(planType string) int64 {
311-
switch strings.ToLower(strings.TrimSpace(planType)) {
311+
switch NormalizePlanType(planType) {
312312
case "pro", "plus", "team":
313313
return 50
314314
default:

frontend/src/pages/Accounts.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default function Accounts() {
119119
return false
120120
}
121121

122-
const plan = (account.plan_type || '').toLowerCase()
122+
const plan = normalizePlanType(account.plan_type)
123123
const has7d = account.usage_percent_7d !== null && account.usage_percent_7d !== undefined
124124
const has5h = account.usage_percent_5h !== null && account.usage_percent_5h !== undefined
125125

@@ -191,7 +191,7 @@ export default function Accounts() {
191191
}
192192
// 套餐过滤
193193
if (planFilter !== 'all') {
194-
const plan = (account.plan_type || '').toLowerCase()
194+
const plan = normalizePlanType(account.plan_type)
195195
if (plan !== planFilter) return false
196196
}
197197
// 搜索过滤
@@ -2243,8 +2243,17 @@ function getDispatchScore(account: AccountRow): number {
22432243
return account.dispatch_score ?? account.scheduler_score ?? 0
22442244
}
22452245

2246+
// OpenAI reports the $100 Pro tier as "prolite" — functionally a Pro plan with
2247+
// a smaller usage cap. Keep behavioral comparisons (usage windows, plan filter,
2248+
// scheduler bias) aligned with the Go side by folding it into "pro".
2249+
function normalizePlanType(planType?: string): string {
2250+
const raw = (planType || '').toLowerCase().trim()
2251+
if (raw === 'prolite' || raw === 'pro_lite' || raw === 'pro-lite') return 'pro'
2252+
return raw
2253+
}
2254+
22462255
function getDefaultScoreBias(planType?: string): number {
2247-
switch ((planType || '').toLowerCase()) {
2256+
switch (normalizePlanType(planType)) {
22482257
case 'pro':
22492258
case 'plus':
22502259
case 'team':
@@ -2817,7 +2826,7 @@ function UsageWindowStat({ label, detail }: { label: string; detail?: AccountRow
28172826

28182827
// 用量列组件
28192828
function UsageCell({ account }: { account: AccountRow }) {
2820-
const plan = (account.plan_type || '').toLowerCase()
2829+
const plan = normalizePlanType(account.plan_type)
28212830
const has7d = account.usage_percent_7d !== null && account.usage_percent_7d !== undefined
28222831
const has5h = account.usage_percent_5h !== null && account.usage_percent_5h !== undefined
28232832
const has7dDetail = hasUsageWindowDetail(account.usage_7d_detail)

proxy/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func accountFilterForModel(model string) auth.AccountFilter {
158158
return false
159159
}
160160
if isProOnlyModel(model) {
161-
return strings.EqualFold(strings.TrimSpace(account.GetPlanType()), "pro")
161+
return auth.NormalizePlanType(account.GetPlanType()) == "pro"
162162
}
163163
return true
164164
}
@@ -2194,7 +2194,7 @@ func compute429Cooldown(account *auth.Account, body []byte, resp *http.Response)
21942194
}
21952195

21962196
// 2. 没有精确重置时间,根据套餐类型 + 用量窗口推断
2197-
planType := strings.ToLower(account.GetPlanType())
2197+
planType := auth.NormalizePlanType(account.GetPlanType())
21982198

21992199
switch planType {
22002200
case "free":

proxy/handler_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ func TestAccountFilterForSparkRequiresPro(t *testing.T) {
349349
if !filter(&auth.Account{PlanType: "pro"}) {
350350
t.Fatal("spark filter should allow pro accounts")
351351
}
352+
if !filter(&auth.Account{PlanType: "prolite"}) {
353+
t.Fatal("spark filter should treat prolite as pro")
354+
}
352355
if filter(&auth.Account{PlanType: "plus"}) {
353356
t.Fatal("spark filter should reject non-pro accounts")
354357
}

0 commit comments

Comments
 (0)