diff --git a/backend/internal/service/claude_reset_credits.go b/backend/internal/service/claude_reset_credits.go index 8c4aee7da0..059aaa4b5e 100644 --- a/backend/internal/service/claude_reset_credits.go +++ b/backend/internal/service/claude_reset_credits.go @@ -2,8 +2,6 @@ package service import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" "io" @@ -22,7 +20,6 @@ const claudeResetUsageURL = "https://api.anthropic.com/api/oauth/usage?cedar_emb // ClaudeResetCredit is deliberately free of upstream grant and organization IDs. type ClaudeResetCredit struct { - SelectionToken string `json:"selection_token"` Label string `json:"label"` ResetsLeft int `json:"resets_left"` StartsAt *time.Time `json:"starts_at,omitempty"` @@ -137,50 +134,43 @@ func (s *ClaudeResetCreditService) headers(ctx context.Context, req *http.Reques req.Header.Set("User-Agent", "claude-cli/"+s.settings.GetClaudeCodeClientVersion(ctx)+" (external, cli)") } -func (s *ClaudeResetCreditService) query(ctx context.Context, id int64) (*ClaudeResetCredits, *claudeResetBlock, error) { +func (s *ClaudeResetCreditService) query(ctx context.Context, id int64) (*ClaudeResetCredits, error) { _, token, proxy, err := s.account(ctx, id) if err != nil { - return nil, nil, err + return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodGet, claudeResetUsageURL, nil) if err != nil { - return nil, nil, err + return nil, err } s.headers(ctx, req, token) resp, err := s.do(req, proxy) if err != nil { - return nil, nil, infraerrors.ServiceUnavailable("CLAUDE_RESET_QUERY_FAILED", "reset status request failed") + return nil, infraerrors.ServiceUnavailable("CLAUDE_RESET_QUERY_FAILED", "reset status request failed") } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return nil, nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_QUERY_FAILED", fmt.Sprintf("reset status upstream HTTP %d", resp.StatusCode)) + return nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_QUERY_FAILED", fmt.Sprintf("reset status upstream HTTP %d", resp.StatusCode)) } var envelope map[string]json.RawMessage if err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&envelope); err != nil || envelope == nil { - return nil, nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset status") + return nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset status") } if _, ok := envelope["error"]; ok { - return nil, nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset status") + return nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset status") } var block *claudeResetBlock raw, present := envelope["cedar_ember"] if present && string(raw) != "null" { if err = json.Unmarshal(raw, &block); err != nil || block == nil || block.Grants == nil { - return nil, nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset grants") + return nil, infraerrors.New(http.StatusBadGateway, "CLAUDE_RESET_STATUS_INVALID", "invalid reset grants") } } - result := projectClaudeResetCredits(block, s.now()) - return result, block, nil + return projectClaudeResetCredits(block, s.now()), nil } func (s *ClaudeResetCreditService) Query(ctx context.Context, id int64) (*ClaudeResetCredits, error) { - r, _, e := s.query(ctx, id) - return r, e -} - -func claudeGrantSelection(g claudeResetGrant) string { - h := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", g.ID, g.ResetsLeft))) - return hex.EncodeToString(h[:]) + return s.query(ctx, id) } func projectClaudeResetCredits(b *claudeResetBlock, now time.Time) *ClaudeResetCredits { @@ -189,7 +179,9 @@ func projectClaudeResetCredits(b *claudeResetBlock, now time.Time) *ClaudeResetC return r } r.Eligible = b.Eligible - r.CooldownUntil = b.CooldownUntil + if b.CooldownUntil != nil && now.Before(*b.CooldownUntil) { + r.CooldownUntil = b.CooldownUntil + } r.WeeklyResetsAt = b.WeeklyResetsAt for _, g := range b.Grants { if !claudeResetGrantIDPattern.MatchString(g.ID) || len(g.Clears) == 0 || g.ResetsLeft <= 0 || g.Paused || (g.StartsAt != nil && now.Before(*g.StartsAt)) || (g.EndsAt != nil && !now.Before(*g.EndsAt)) { @@ -203,7 +195,7 @@ func projectClaudeResetCredits(b *claudeResetBlock, now time.Time) *ClaudeResetC used[k] = v } } - r.Credits = append(r.Credits, ClaudeResetCredit{SelectionToken: claudeGrantSelection(g), Label: g.Label, ResetsLeft: g.ResetsLeft, StartsAt: g.StartsAt, ExpiresAt: g.EndsAt, Clears: g.Clears, PercentUsed: used, Blocking: g.Blocking, UseRequiresLimit: requires, Redeemable: usable}) + r.Credits = append(r.Credits, ClaudeResetCredit{Label: g.Label, ResetsLeft: g.ResetsLeft, StartsAt: g.StartsAt, ExpiresAt: g.EndsAt, Clears: g.Clears, PercentUsed: used, Blocking: g.Blocking, UseRequiresLimit: requires, Redeemable: usable}) if usable { r.AvailableCount += g.ResetsLeft } diff --git a/backend/internal/service/claude_reset_credits_test.go b/backend/internal/service/claude_reset_credits_test.go index e2f4b99a4a..fc11f2a5ef 100644 --- a/backend/internal/service/claude_reset_credits_test.go +++ b/backend/internal/service/claude_reset_credits_test.go @@ -40,6 +40,19 @@ func TestClaudeResetStatusNativeContract(t *testing.T) { require.NotContains(t, string(b), `"id"`) require.NotContains(t, string(b), "launch") require.NotContains(t, string(b), "synthetic-token") + require.NotContains(t, string(b), "selection_token") +} +func TestClaudeResetPastCooldownIsCleared(t *testing.T) { + now := time.Now() + past := now.Add(-time.Minute) + no := false + g := claudeResetGrant{ID: "grant", ResetsLeft: 1, UsableNow: true, UseRequiresLimit: &no, Clears: []string{"five_hour"}} + r := projectClaudeResetCredits(&claudeResetBlock{Eligible: true, NextGrantID: g.ID, CooldownUntil: &past, Grants: []claudeResetGrant{g}}, now) + require.Nil(t, r.CooldownUntil) + require.Equal(t, 1, r.AvailableCount) + future := now.Add(time.Hour) + r = projectClaudeResetCredits(&claudeResetBlock{Eligible: true, NextGrantID: g.ID, CooldownUntil: &future, Grants: []claudeResetGrant{g}}, now) + require.Equal(t, &future, r.CooldownUntil) } func TestClaudeResetEligibilityFailClosed(t *testing.T) { now := time.Now() diff --git a/frontend/src/api/admin/claudeResetCredits.ts b/frontend/src/api/admin/claudeResetCredits.ts index 68edb5afb9..f7011d05cc 100644 --- a/frontend/src/api/admin/claudeResetCredits.ts +++ b/frontend/src/api/admin/claudeResetCredits.ts @@ -1,7 +1,6 @@ import { apiClient } from '../client' export interface ClaudeResetCredit { - selection_token: string label: string resets_left: number starts_at?: string diff --git a/frontend/src/components/account/ClaudeResetCreditsCell.vue b/frontend/src/components/account/ClaudeResetCreditsCell.vue index 1fc6a4df85..4a139b1665 100644 --- a/frontend/src/components/account/ClaudeResetCreditsCell.vue +++ b/frontend/src/components/account/ClaudeResetCreditsCell.vue @@ -36,7 +36,7 @@ -