fix: clear quota cooldown after capacity changes#3809
Conversation
|
This pull request targeted The base branch has been automatically changed to |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 215879edcd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if len(incoming.ModelStates) > 0 || len(existing.ModelStates) == 0 { | ||
| return false | ||
| } | ||
| return capacityIdentitySignatureEqual(authCapacityIdentitySignature(existing), authCapacityIdentitySignature(incoming)) |
There was a problem hiding this comment.
Drop cloned model states when capacity changes
When the scheduler refreshes an auth, refreshAuth clones the current auth before calling the executor, so updated already contains the old ModelStates; Codex then only mutates capacity claims on that clone. Because this early return skips all signature comparison whenever incoming.ModelStates is non-empty, Update stores the stale cloned cooldowns even if plan_type or subscription metadata changed, so the main local token-refresh path still leaves upgraded Codex accounts blocked until the old quota retry expires.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to clear stale quota cooldowns and model states when an account's capacity identity changes (such as a Codex plan upgrade), allowing immediate retries with new limits. It adds token parsing to extract ChatGPT subscription details and updates the auth update flow to compare capacity signatures. The review feedback highlights two critical issues: a potential nil pointer dereference and stale claims retention in applyCodexCapacityClaims, and a type mismatch issue during token refresh in capacityIdentityValue when handling pointer types, which would prevent model states from being correctly inherited.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func applyCodexCapacityClaims(auth *cliproxyauth.Auth, idToken string) { | ||
| if auth == nil || strings.TrimSpace(idToken) == "" { | ||
| return | ||
| } | ||
| claims, errParse := codexauth.ParseJWTToken(idToken) | ||
| if errParse != nil || claims == nil { | ||
| if errParse != nil { | ||
| log.Warnf("codex executor: failed to parse refreshed capacity claims: %v", errParse) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| info := claims.CodexAuthInfo | ||
| if auth.Attributes == nil { | ||
| auth.Attributes = make(map[string]string) | ||
| } | ||
| if auth.Metadata == nil { | ||
| auth.Metadata = make(map[string]any) | ||
| } | ||
| if planType := strings.TrimSpace(info.ChatgptPlanType); planType != "" { | ||
| auth.Attributes["plan_type"] = planType | ||
| auth.Metadata["chatgpt_plan_type"] = planType | ||
| } | ||
| if accountID := strings.TrimSpace(info.ChatgptAccountID); accountID != "" { | ||
| auth.Metadata["chatgpt_account_id"] = accountID | ||
| } | ||
| if info.ChatgptSubscriptionActiveStart != nil { | ||
| auth.Metadata["chatgpt_subscription_active_start"] = info.ChatgptSubscriptionActiveStart | ||
| } | ||
| if info.ChatgptSubscriptionActiveUntil != nil { | ||
| auth.Metadata["chatgpt_subscription_active_until"] = info.ChatgptSubscriptionActiveUntil | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential Panic & Stale Capacity Claims Bug
- Potential Nil Pointer Dereference: If
claims.CodexAuthInfois a pointer type, it can benilif the corresponding claim is missing from the JWT. Accessing fields on it without a nil check would cause a panic. - Stale Capacity Claims: If a claim (like
ChatgptPlanTypeorChatgptSubscriptionActiveUntil) becomes empty ornil(e.g., subscription expired or downgraded), the current code does not clear the old value fromauth.Attributesorauth.Metadatabecause of theif ... != ""andif ... != nilguards. This keeps the expired subscription/plan active in the auth state, preventing the quota cooldown from being cleared when it should be.
We should add a nil check for info and explicitly clear/delete attributes and metadata keys if they are empty or nil in the new token.
func applyCodexCapacityClaims(auth *cliproxyauth.Auth, idToken string) {
if auth == nil || strings.TrimSpace(idToken) == "" {
return
}
claims, errParse := codexauth.ParseJWTToken(idToken)
if errParse != nil || claims == nil {
if errParse != nil {
log.Warnf("codex executor: failed to parse refreshed capacity claims: %v", errParse)
}
return
}
info := claims.CodexAuthInfo
if info == nil {
return
}
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
if planType := strings.TrimSpace(info.ChatgptPlanType); planType != "" {
auth.Attributes["plan_type"] = planType
auth.Metadata["chatgpt_plan_type"] = planType
} else {
delete(auth.Attributes, "plan_type")
delete(auth.Metadata, "chatgpt_plan_type")
}
if accountID := strings.TrimSpace(info.ChatgptAccountID); accountID != "" {
auth.Metadata["chatgpt_account_id"] = accountID
} else {
delete(auth.Metadata, "chatgpt_account_id")
}
if info.ChatgptSubscriptionActiveStart != nil {
auth.Metadata["chatgpt_subscription_active_start"] = info.ChatgptSubscriptionActiveStart
} else {
delete(auth.Metadata, "chatgpt_subscription_active_start")
}
if info.ChatgptSubscriptionActiveUntil != nil {
auth.Metadata["chatgpt_subscription_active_until"] = info.ChatgptSubscriptionActiveUntil
} else {
delete(auth.Metadata, "chatgpt_subscription_active_until")
}
}215879e to
ee1bb38
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee1bb38848
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| if planType := strings.TrimSpace(info.ChatgptPlanType); planType != "" { | ||
| auth.Attributes["plan_type"] = planType | ||
| auth.Metadata["chatgpt_plan_type"] = planType |
There was a problem hiding this comment.
Preserve cooldowns when refreshing the same Codex plan
When a Codex auth was created by the device flow, buildAuthRecord stores the plan only in Attributes["plan_type"] and the metadata initially contains just email. On the next successful refresh with the same plan, this line adds a new meta:chatgpt_plan_type entry to the capacity signature even though attr:plan_type is unchanged; because Update compares attr and meta keys separately, shouldClearModelStates treats this as a capacity change and drops any cloned per-model quota cooldown. In practice, an account that is still rate-limited can become immediately eligible again after its first token refresh purely because duplicate metadata was backfilled, not because the plan changed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee1bb38848
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } else if shouldClearModelStates(existing, auth) { | ||
| auth.ModelStates = nil |
There was a problem hiding this comment.
Clear aggregate cooldown when dropping model states
When a refreshed auth is a clone of a quota-cooled account and its capacity signature changes, this branch removes ModelStates but leaves the cloned auth-level fields (Unavailable, NextRetryAfter, Quota, and StatusMessage) untouched. For accounts where all model states had contributed to the aggregate cooldown, management output and any selection path that calls isAuthBlockedForModel with an empty model will still treat the account as cooling down until the old retry time, so the capacity-change refresh only partially clears the stale quota state. Clear/recompute the aggregate availability when dropping the per-model map.
Useful? React with 👍 / 👎.
ee1bb38 to
4674f09
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4674f097cb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| delete(auth.Attributes, "plan_type") | ||
| delete(auth.Metadata, "chatgpt_plan_type") |
There was a problem hiding this comment.
Preserve Codex plan when refresh omits the claim
When a valid refreshed Codex ID token omits chatgpt_plan_type, this branch deletes the existing Attributes["plan_type"]. I checked the downstream routing/model registration paths: sdk/cliproxy/service.go defaults an empty Codex plan to the Pro model set, and isFreeCodexAuth only blocks credentials whose plan is explicitly "free"; therefore a free account whose refresh response temporarily lacks this claim loses its free marker and can be registered/selected as an unrestricted Pro-capable account. Absence of the claim is not enough evidence to upgrade capacity, so keep the previous plan or mark it conservatively instead of deleting it.
Useful? React with 👍 / 👎.
4674f09 to
cdd691f
Compare
概述
修复账号容量相关信息变化后,旧的 quota 冷却状态仍然被继承的问题。
当 Codex 账号触发额度限制时,CPA 会保存按模型维度的运行时状态,例如
Quota.Exceeded和NextRetryAfter。如果后续账号因为订阅升级、订阅变更或容量变化而刷新,旧的ModelStates可能仍然被继承,导致账号明明已经有新额度,却依然被旧冷却状态阻塞。这个修改会在账号容量相关元数据没有变化时才继承旧的
ModelStates。修改内容
ShouldInheritModelStates,统一判断 auth 更新时是否应该保留旧的模型运行时状态。测试
go test ./...