Skip to content

fix(auth): resolve oauth aliases before suspension checks#2441

Merged
luispater merged 2 commits intorouter-for-me:devfrom
MonsterQiu:issue-2421-alias-before-suspension
Apr 2, 2026
Merged

fix(auth): resolve oauth aliases before suspension checks#2441
luispater merged 2 commits intorouter-for-me:devfrom
MonsterQiu:issue-2421-alias-before-suspension

Conversation

@MonsterQiu
Copy link
Copy Markdown
Contributor

Summary:

  • resolve oauth model aliases before auth availability checks when selecting credentials
  • use the resolved upstream model for execution-state checks when oauth alias routing changes the model
  • add a regression test covering a blocked route model with a healthy alias target

Why:

  • requests for aliased oauth models could return auth_unavailable: no auth available when the client-visible route model was suspended, even though the alias target model was healthy
  • the routing code selected auths and checked model suspension against the unresolved route model instead of the alias-resolved upstream model

Verification:

  • go test ./sdk/cliproxy/auth -run 'TestManagerExecute_OAuthAliasBypassesBlockedRouteModel' -count=1
  • go test ./sdk/cliproxy/auth -count=1
  • go test ./...

Closes #2421

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07b7c1a1e0

ℹ️ 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".

Comment thread sdk/cliproxy/auth/conductor.go Outdated
m.mu.RUnlock()
return nil, nil, "", errAvailable
}
selected, errPick := m.selector.Pick(ctx, "mixed", "", opts, available)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward route model to selector in legacy pick paths

pickNextLegacy/pickNextMixedLegacy now call selector.Pick with an empty model string after availableAuthsForRouteModel prefilters by alias-resolved model. For built-in selectors, this triggers a second availability check via getAvailableAuths(..., model=""), which falls back to aggregate auth-level blocking (isAuthBlockedForModel with empty model) and can reject credentials that were just deemed usable for the resolved alias model. A concrete case is an auth with auth.Unavailable=true from a cooled-down route model state but a healthy alias target: prefilter admits it, then selector drops it and still returns auth_unavailable, so the alias-suspension fix is bypassed in real cooldown states.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces route-aware model selection and aliasing logic to the Manager. Key changes include new methods for resolving model aliases, determining execution state models, and filtering authentication candidates by priority and blocking status. The scheduler's picking logic is updated to handle these aliases, with a fallback to legacy paths when route-aware selection is required. Review feedback suggests simplifying the alias resolution logic, adopting idiomatic Go loops, and deduplicating model support checks into a helper method to improve maintainability.

Comment on lines +446 to +450
resolvedModel := m.applyOAuthModelAlias(auth, requestedModel)
if strings.TrimSpace(resolvedModel) == "" {
resolvedModel = requestedModel
}
return resolvedModel
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block can be simplified for better readability and conciseness, while maintaining the same logic.

Suggested change
resolvedModel := m.applyOAuthModelAlias(auth, requestedModel)
if strings.TrimSpace(resolvedModel) == "" {
resolvedModel = requestedModel
}
return resolvedModel
if resolvedModel := m.applyOAuthModelAlias(auth, requestedModel); strings.TrimSpace(resolvedModel) != "" {
return resolvedModel
}
return requestedModel

Comment thread sdk/cliproxy/auth/conductor.go Outdated
Comment on lines +510 to +525
for i := 0; i < len(auths); i++ {
candidate := auths[i]
checkModel := m.selectionModelForAuth(candidate, routeModel)
blocked, reason, next := isAuthBlockedForModel(candidate, checkModel, now)
if !blocked {
priority := authPriority(candidate)
availableByPriority[priority] = append(availableByPriority[priority], candidate)
continue
}
if reason == blockReasonCooldown {
cooldownCount++
if !next.IsZero() && (earliest.IsZero() || next.Before(earliest)) {
earliest = next
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This for loop can be converted to a for...range loop, which is more idiomatic in Go and improves readability.

    for _, candidate := range auths {
        checkModel := m.selectionModelForAuth(candidate, routeModel)
        blocked, reason, next := isAuthBlockedForModel(candidate, checkModel, now)
        if !blocked {
            priority := authPriority(candidate)
            availableByPriority[priority] = append(availableByPriority[priority], candidate)
            continue
        }
        if reason == blockReasonCooldown {
            cooldownCount++
            if !next.IsZero() && (earliest.IsZero() || next.Before(earliest)) {
                earliest = next
            }
        }
    }

Comment thread sdk/cliproxy/auth/conductor.go Outdated
Comment on lines 2388 to 2399
if modelKey != "" && registryRef != nil {
supportsModel := registryRef.ClientSupportsModel(candidate.ID, modelKey)
if !supportsModel {
selectionKey := canonicalModelKey(m.selectionModelForAuth(candidate, model))
if selectionKey != "" && selectionKey != modelKey {
supportsModel = registryRef.ClientSupportsModel(candidate.ID, selectionKey)
}
}
if !supportsModel {
continue
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of logic for checking model support with aliases is duplicated in pickNextMixedLegacy (lines 2526-2537). To improve maintainability and reduce code duplication, consider extracting this logic into a helper method. For example:

func (m *Manager) clientSupportsModelWithAlias(candidate *Auth, model, modelKey string, registryRef *registry.ModelRegistry) bool {
	if modelKey == "" || registryRef == nil {
		return true // No model to check, or no registry to check against
	}
	if registryRef.ClientSupportsModel(candidate.ID, modelKey) {
		return true
	}
	selectionKey := canonicalModelKey(m.selectionModelForAuth(candidate, model))
	if selectionKey != "" && selectionKey != modelKey {
		return registryRef.ClientSupportsModel(candidate.ID, selectionKey)
	}
	return false
}

You could then replace this block with a call to the new helper:
if !m.clientSupportsModelWithAlias(candidate, model, modelKey, registryRef) { continue }

@MonsterQiu
Copy link
Copy Markdown
Contributor Author

Follow-up pushed in f611dd6e.

This keeps the same fix but tightens the implementation in two places:

  • deduplicates route-aware model support checks behind a helper
  • preserves the original model argument for non-built-in selectors instead of always blanking it in the legacy path

Verification rerun after the refactor:

  • go test ./sdk/cliproxy/auth -run 'TestManagerExecute_OAuthAliasBypassesBlockedRouteModel' -count=1
  • go test ./sdk/cliproxy/auth -count=1
  • go test ./...

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f611dd6e96

ℹ️ 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".

Comment on lines +459 to +461
selectionModel := m.selectionModelForAuth(auth, routeModel)
if canonicalModelKey(selectionModel) == canonicalModelKey(upstreamModel) && strings.TrimSpace(selectionModel) != "" {
return strings.TrimSpace(upstreamModel)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep prefixed route model when recording execution state

stateModelForExecution now derives the state key from selectionModelForAuth, which calls rewriteModelForAuth and strips auth.Prefix; for requests like team-a/model-x, this makes result.Model become model-x instead of the routed team-a/model-x. MarkResult uses result.Model to call registry suspension/quota APIs, so prefixed registrations (added via applyModelPrefixes in sdk/cliproxy/service.go) are no longer updated, and registry consumers can keep seeing the prefixed route model as available even while that route is actually in cooldown/error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Fixes OAuth alias routing so auth availability / suspension checks and execution-state handling use the alias-resolved upstream model (avoids auth_unavailable when the route model is blocked but the alias target is healthy).
  • Adds a focused regression test for the reported scenario.
  • Refactors/dedupes route-aware model support checks.

What I liked

  • availableAuthsForRouteModel checks selectionModelForAuth(...) before isAuthBlockedForModel(...), which directly addresses the bug.
  • stateModelForExecution(...) keeps model-state tracking aligned with the actual upstream model when alias routing changes the model.
  • Regression test TestManagerExecute_OAuthAliasBypassesBlockedRouteModel is deterministic and validates both payload and executor model.

Non-blocking notes

  • Please confirm the intent of passing "" as the selector model arg for built-in selectors in legacy fallback (cursor becomes provider-scoped).
  • The scheduler fast-path fallback adds an O(n) scan over m.auths per request when model != ""; might be worth caching if n is large.

Test plan

  • go test ./sdk/cliproxy/auth -run TestManagerExecute_OAuthAliasBypassesBlockedRouteModel -count=1
  • go test ./...

This is an automated Codex review result and still requires manual verification by a human reviewer.

@luispater luispater added the codex label Apr 1, 2026
@luispater luispater merged commit e783d0a into router-for-me:dev Apr 2, 2026
2 checks passed
@MonsterQiu MonsterQiu deleted the issue-2421-alias-before-suspension branch April 3, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants