charmbracelet/crush · error

OAuth refresh not supported for provider %s

Error message

OAuth refresh not supported for provider %s

What it means

exchange dispatches token refresh to a provider-specific implementation. Only Copilot (via copilot.RefreshToken) and Hyper (via hyper.ExchangeToken) are supported; any other providerID hits the default branch and returns this error. It means the config store cannot refresh OAuth credentials for that provider.

Source

Thrown at internal/config/store.go:873

		return nil
	}
	return diskToken
}

// exchange performs the provider-specific OAuth token exchange. Tests may
// override it via the exchangeToken field; production uses the real
// provider clients.
func (s *ConfigStore) exchange(ctx context.Context, providerID, refreshToken string) (*oauth.Token, error) {
	if s.exchangeToken != nil {
		return s.exchangeToken(ctx, providerID, refreshToken)
	}
	switch providerID {
	case string(catwalk.InferenceProviderCopilot):
		return copilot.RefreshToken(ctx, refreshToken)
	case hyperp.Name:
		return hyper.ExchangeToken(ctx, refreshToken)
	default:
		return nil, fmt.Errorf("OAuth refresh not supported for provider %s", providerID)
	}
}

// withRefreshLock runs fn while holding the per-provider cross-process
// refresh lock, so a credential write cannot interleave with a peer's
// token exchange. Acquisition is best effort: when the lock cannot be
// taken in time, fn runs anyway rather than blocking a write the user is
// waiting on.
func (s *ConfigStore) withRefreshLock(providerID string, fn func() error) error {
	ctx, cancel := context.WithTimeout(context.Background(), credentialWriteLockDeadline)
	defer cancel()
	release, err := lock.File(ctx, s.refreshLockPath(providerID))
	if err != nil {
		slog.Warn("Writing credentials without the refresh lock", "provider", providerID, "error", err)
		return fn()
	}
	defer release()
	return fn()

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Use a supported provider (Copilot or Hyper) for OAuth refresh, or supply a static API key for the unsupported provider.
  2. Remove the stale oauth entry for the unsupported provider so the store stops trying to refresh it.
  3. Update Crush / catwalk provider definitions to a version that supports the provider's refresh flow.

Example fix

// before: oauth block on unsupported provider
providers.monster.oauth = { ... }
// after: static key instead
providers.monster.api_key = "sk-..."
Defensive patterns

Strategy: validation

Validate before calling

supported := map[string]bool{"copilot": true, "hyper": true}
if !supported[providerID] && hasOAuthBlock(providerID) { removeOAuthBlock(providerID) }

Try / catch

if err := refresh(); err != nil && strings.Contains(err.Error(), "not supported") {
    fallbackToAPIKey(providerID)
}

Prevention

When it happens

Trigger: refreshOAuthTokenLocked calls s.exchange for a providerID other than catwalk.InferenceProviderCopilot or hyperp.Name — i.e. a provider configured with OAuth-style fields but lacking a refresh implementation.

Common situations: User hand-edited crush config adding an oauth block for an unsupported provider; provider support was removed/renamed in a version change; typo in provider ID.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/b1111dac8f380fa4. Report an issue: GitHub.