Skip to content

Commit 01d65a0

Browse files
committed
fix(auth): clear stale runtime block after refresh
1 parent bb0efc8 commit 01d65a0

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

sdk/cliproxy/auth/conductor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3480,8 +3480,8 @@ func (m *Manager) refreshAuth(ctx context.Context, id string) {
34803480
}
34813481
updated.LastRefreshedAt = now
34823482
updated.NextRefreshAfter = time.Time{}
3483-
updated.LastError = nil
3484-
updated.UpdatedAt = now
3483+
updated.ModelStates = nil
3484+
clearAuthStateOnSuccess(updated, now)
34853485
if m.shouldRefresh(updated, now) {
34863486
updated.NextRefreshAfter = now.Add(refreshIneffectiveBackoff)
34873487
}

sdk/cliproxy/auth/conductor_update_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,36 @@ func (e ineffectiveRefreshExecutor) HttpRequest(context.Context, *Auth, *http.Re
4646
return nil, nil
4747
}
4848

49+
type successfulRefreshExecutor struct {
50+
provider string
51+
}
52+
53+
func (e successfulRefreshExecutor) Identifier() string { return e.provider }
54+
55+
func (e successfulRefreshExecutor) Execute(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
56+
return cliproxyexecutor.Response{}, nil
57+
}
58+
59+
func (e successfulRefreshExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
60+
return nil, nil
61+
}
62+
63+
func (e successfulRefreshExecutor) Refresh(_ context.Context, auth *Auth) (*Auth, error) {
64+
if auth.Metadata == nil {
65+
auth.Metadata = make(map[string]any)
66+
}
67+
auth.Metadata["access_token"] = "new-access-token"
68+
return auth, nil
69+
}
70+
71+
func (e successfulRefreshExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
72+
return cliproxyexecutor.Response{}, nil
73+
}
74+
75+
func (e successfulRefreshExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) {
76+
return nil, nil
77+
}
78+
4979
type metadataMutatingExecutor struct {
5080
provider string
5181
}
@@ -339,6 +369,49 @@ func TestManager_Update_ActiveInheritsModelStates(t *testing.T) {
339369
}
340370
}
341371

372+
func TestManager_RefreshAuth_ClearsInheritedRuntimeBlockAfterSuccessfulRefresh(t *testing.T) {
373+
m := NewManager(nil, nil, nil)
374+
executor := successfulRefreshExecutor{provider: "claude"}
375+
m.RegisterExecutor(executor)
376+
377+
now := time.Now().UTC()
378+
if _, errRegister := m.Register(context.Background(), &Auth{
379+
ID: "auth-refresh-success",
380+
Provider: "claude",
381+
Status: StatusError,
382+
Metadata: map[string]any{
383+
"access_token": "old-access-token",
384+
},
385+
ModelStates: map[string]*ModelState{
386+
"claude-sonnet": {
387+
Status: StatusError,
388+
Unavailable: true,
389+
NextRetryAfter: now.Add(30 * time.Minute),
390+
LastError: &Error{Message: "unauthorized", HTTPStatus: http.StatusUnauthorized},
391+
UpdatedAt: now,
392+
},
393+
},
394+
}); errRegister != nil {
395+
t.Fatalf("register auth: %v", errRegister)
396+
}
397+
398+
m.refreshAuth(context.Background(), "auth-refresh-success")
399+
400+
updated, ok := m.GetByID("auth-refresh-success")
401+
if !ok || updated == nil {
402+
t.Fatalf("expected auth to be present after refresh")
403+
}
404+
if got := updated.Metadata["access_token"]; got != "new-access-token" {
405+
t.Fatalf("access_token = %v, want new-access-token", got)
406+
}
407+
if updated.Unavailable || !updated.NextRetryAfter.IsZero() || len(updated.ModelStates) != 0 {
408+
t.Fatalf("expected successful refresh to clear runtime block, got unavailable=%v next_retry=%s model_states=%d", updated.Unavailable, updated.NextRetryAfter, len(updated.ModelStates))
409+
}
410+
if updated.LastRefreshedAt.IsZero() {
411+
t.Fatal("expected LastRefreshedAt to be set")
412+
}
413+
}
414+
342415
func TestManager_RefreshAuth_SetsBackoffWhenRefreshIsIneffective(t *testing.T) {
343416
m := NewManager(nil, nil, nil)
344417
executor := ineffectiveRefreshExecutor{provider: "claude"}

sdk/cliproxy/auth/runtime_persistence.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ func preserveRuntimeSnapshotState(auth *Auth, existing *Auth, now time.Time) {
277277
if auth.Disabled || auth.Status == StatusDisabled || existing.Disabled || existing.Status == StatusDisabled {
278278
return
279279
}
280+
if authRefreshTimestampAdvanced(auth, existing) {
281+
return
282+
}
280283
candidate := auth.Clone()
281284
sanitizeRuntimeSnapshotAuth(candidate, now)
282285
if hasRuntimeSnapshotState(candidate) {
@@ -289,6 +292,13 @@ func preserveRuntimeSnapshotState(auth *Auth, existing *Auth, now time.Time) {
289292
_ = applyRuntimeSnapshotState(auth, state, now)
290293
}
291294

295+
func authRefreshTimestampAdvanced(auth *Auth, existing *Auth) bool {
296+
if auth == nil || existing == nil || auth.LastRefreshedAt.IsZero() {
297+
return false
298+
}
299+
return existing.LastRefreshedAt.IsZero() || auth.LastRefreshedAt.After(existing.LastRefreshedAt)
300+
}
301+
292302
func sanitizeRuntimeSnapshotAuth(auth *Auth, now time.Time) {
293303
if auth == nil {
294304
return

0 commit comments

Comments
 (0)