{"record":{"id":"b1111dac8f380fa4","repo":"charmbracelet/crush","slug":"oauth-refresh-not-supported-for-provider-s","errorCode":null,"errorMessage":"OAuth refresh not supported for provider %s","messagePattern":"OAuth refresh not supported for provider (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":873,"sourceCode":"\t\treturn nil\n\t}\n\treturn diskToken\n}\n\n// exchange performs the provider-specific OAuth token exchange. Tests may\n// override it via the exchangeToken field; production uses the real\n// provider clients.\nfunc (s *ConfigStore) exchange(ctx context.Context, providerID, refreshToken string) (*oauth.Token, error) {\n\tif s.exchangeToken != nil {\n\t\treturn s.exchangeToken(ctx, providerID, refreshToken)\n\t}\n\tswitch providerID {\n\tcase string(catwalk.InferenceProviderCopilot):\n\t\treturn copilot.RefreshToken(ctx, refreshToken)\n\tcase hyperp.Name:\n\t\treturn hyper.ExchangeToken(ctx, refreshToken)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"OAuth refresh not supported for provider %s\", providerID)\n\t}\n}\n\n// withRefreshLock runs fn while holding the per-provider cross-process\n// refresh lock, so a credential write cannot interleave with a peer's\n// token exchange. Acquisition is best effort: when the lock cannot be\n// taken in time, fn runs anyway rather than blocking a write the user is\n// waiting on.\nfunc (s *ConfigStore) withRefreshLock(providerID string, fn func() error) error {\n\tctx, cancel := context.WithTimeout(context.Background(), credentialWriteLockDeadline)\n\tdefer cancel()\n\trelease, err := lock.File(ctx, s.refreshLockPath(providerID))\n\tif err != nil {\n\t\tslog.Warn(\"Writing credentials without the refresh lock\", \"provider\", providerID, \"error\", err)\n\t\treturn fn()\n\t}\n\tdefer release()\n\treturn fn()","sourceCodeStart":855,"sourceCodeEnd":891,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L855-L891","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a supported provider (Copilot or Hyper) for OAuth refresh, or supply a static API key for the unsupported provider.","Remove the stale oauth entry for the unsupported provider so the store stops trying to refresh it.","Update Crush / catwalk provider definitions to a version that supports the provider's refresh flow."],"exampleFix":"// before: oauth block on unsupported provider\nproviders.monster.oauth = { ... }\n// after: static key instead\nproviders.monster.api_key = \"sk-...\"","handlingStrategy":"validation","validationCode":"supported := map[string]bool{\"copilot\": true, \"hyper\": true}\nif !supported[providerID] && hasOAuthBlock(providerID) { removeOAuthBlock(providerID) }","typeGuard":null,"tryCatchPattern":"if err := refresh(); err != nil && strings.Contains(err.Error(), \"not supported\") {\n    fallbackToAPIKey(providerID)\n}","preventionTips":["Only configure oauth blocks for providers with refresh support","Use static api_key for other providers","Keep provider definitions updated with the app version"],"tags":["oauth","unsupported-provider","configuration"],"backgroundTag":"oauth-provider-unsupported","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}