88 "io"
99 "net/http"
1010 "path/filepath"
11+ "reflect"
1112 "sort"
1213 "strconv"
1314 "strings"
@@ -1425,6 +1426,136 @@ func (m *Manager) Register(ctx context.Context, auth *Auth) (*Auth, error) {
14251426 return auth .Clone (), nil
14261427}
14271428
1429+ // ShouldInheritModelStates reports whether an active auth update should keep
1430+ // previous per-model runtime availability. Capacity identity changes, such as a
1431+ // Codex plan upgrade, must clear stale quota cooldowns so the account can be
1432+ // retried immediately with its new limits.
1433+ func ShouldInheritModelStates (existing , incoming * Auth ) bool {
1434+ if existing == nil || incoming == nil {
1435+ return false
1436+ }
1437+ if existing .Disabled || existing .Status == StatusDisabled || incoming .Disabled || incoming .Status == StatusDisabled {
1438+ return false
1439+ }
1440+ if len (incoming .ModelStates ) > 0 || len (existing .ModelStates ) == 0 {
1441+ return false
1442+ }
1443+ return capacityIdentitySignatureEqual (authCapacityIdentitySignature (existing ), authCapacityIdentitySignature (incoming ))
1444+ }
1445+
1446+ func shouldClearModelStates (existing , incoming * Auth ) bool {
1447+ if existing == nil || incoming == nil || len (existing .ModelStates ) == 0 || len (incoming .ModelStates ) == 0 {
1448+ return false
1449+ }
1450+ if existing .Disabled || existing .Status == StatusDisabled || incoming .Disabled || incoming .Status == StatusDisabled {
1451+ return true
1452+ }
1453+ return ! capacityIdentitySignatureEqual (authCapacityIdentitySignature (existing ), authCapacityIdentitySignature (incoming ))
1454+ }
1455+
1456+ var capacityIdentityKeys = map [string ]struct {}{
1457+ "account_id" : {},
1458+ "account_uuid" : {},
1459+ "accountid" : {},
1460+ "balance" : {},
1461+ "chatgpt_account_id" : {},
1462+ "chatgpt_plan_type" : {},
1463+ "chatgpt_subscription_active_start" : {},
1464+ "chatgpt_subscription_active_until" : {},
1465+ "credit" : {},
1466+ "credits" : {},
1467+ "organization_id" : {},
1468+ "org_id" : {},
1469+ "plan" : {},
1470+ "plan_type" : {},
1471+ "project_id" : {},
1472+ "quota" : {},
1473+ "quota_limit" : {},
1474+ "service_tier" : {},
1475+ "subscription" : {},
1476+ "subscription_plan" : {},
1477+ "subscription_status" : {},
1478+ "team_id" : {},
1479+ "tier" : {},
1480+ }
1481+
1482+ func authCapacityIdentitySignature (auth * Auth ) map [string ]string {
1483+ if auth == nil {
1484+ return nil
1485+ }
1486+ signature := make (map [string ]string )
1487+ for key , value := range auth .Attributes {
1488+ normalized := strings .ToLower (strings .TrimSpace (key ))
1489+ if _ , ok := capacityIdentityKeys [normalized ]; ! ok {
1490+ continue
1491+ }
1492+ val := strings .TrimSpace (value )
1493+ if val == "" {
1494+ continue
1495+ }
1496+ signature ["attr:" + normalized ] = val
1497+ }
1498+ for key , value := range auth .Metadata {
1499+ normalized := strings .ToLower (strings .TrimSpace (key ))
1500+ if _ , ok := capacityIdentityKeys [normalized ]; ! ok {
1501+ continue
1502+ }
1503+ val := capacityIdentityValue (value )
1504+ if val == "" {
1505+ continue
1506+ }
1507+ signature ["meta:" + normalized ] = val
1508+ }
1509+ return signature
1510+ }
1511+
1512+ func capacityIdentityValue (value any ) string {
1513+ if value == nil {
1514+ return ""
1515+ }
1516+ val := reflect .ValueOf (value )
1517+ for val .Kind () == reflect .Ptr {
1518+ if val .IsNil () {
1519+ return ""
1520+ }
1521+ val = val .Elem ()
1522+ }
1523+ underlying := val .Interface ()
1524+ switch typed := underlying .(type ) {
1525+ case string :
1526+ return strings .TrimSpace (typed )
1527+ case []byte :
1528+ return strings .TrimSpace (string (typed ))
1529+ case json.Number :
1530+ return strings .TrimSpace (typed .String ())
1531+ case bool :
1532+ if typed {
1533+ return "true"
1534+ }
1535+ return "false"
1536+ case time.Time :
1537+ return typed .Format (time .RFC3339 )
1538+ default :
1539+ raw , err := json .Marshal (typed )
1540+ if err != nil {
1541+ return ""
1542+ }
1543+ return strings .TrimSpace (string (raw ))
1544+ }
1545+ }
1546+
1547+ func capacityIdentitySignatureEqual (left , right map [string ]string ) bool {
1548+ if len (left ) != len (right ) {
1549+ return false
1550+ }
1551+ for key , leftValue := range left {
1552+ if right [key ] != leftValue {
1553+ return false
1554+ }
1555+ }
1556+ return true
1557+ }
1558+
14281559// Update replaces an existing auth entry and notifies hooks.
14291560func (m * Manager ) Update (ctx context.Context , auth * Auth ) (* Auth , error ) {
14301561 if auth == nil || auth .ID == "" {
@@ -1443,10 +1574,10 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) {
14431574 auth .Success = existing .Success
14441575 auth .Failed = existing .Failed
14451576 auth .recentRequests = existing .recentRequests
1446- if ! existing . Disabled && existing . Status != StatusDisabled && ! auth . Disabled && auth . Status != StatusDisabled {
1447- if len ( auth .ModelStates ) == 0 && len ( existing .ModelStates ) > 0 {
1448- auth . ModelStates = existing . ModelStates
1449- }
1577+ if ShouldInheritModelStates ( existing , auth ) {
1578+ auth .ModelStates = existing .ModelStates
1579+ } else if shouldClearModelStates ( existing , auth ) {
1580+ auth . ModelStates = nil
14501581 }
14511582 auth .EnsureIndex ()
14521583 authClone := auth .Clone ()
0 commit comments