on main help sign in
fix: refresh oauth token in the background

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>

commitb621f9312f4e7677062e3dec2028091279ba4b2d authorRaphael Amorim <rapha850@gmail.com> dateSat 29 Nov 2025, 16:51 (1 year ago) parentb66c49f
2 files changed+53−1
internal/agent/coordinator.go+15−1 internal/config/config.go+38
internal/agent/coordinator.go+15−1
@@ -130,7 +130,20 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
130130
131131 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
132132
133− return c.currentAgent.Run(ctx, SessionAgentCall{
133+ if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
134+ slog.Info("Detected expired OAuth token, attempting refresh", "provider", providerCfg.ID)
135+ if refreshErr := c.cfg.RefreshOAuthToken(ctx, providerCfg.ID); refreshErr != nil {
136+ slog.Error("Failed to refresh OAuth token", "provider", providerCfg.ID, "error", refreshErr)
137+ return nil, refreshErr
138+ }
139+
140+ // Rebuild models with refreshed token
141+ if updateErr := c.UpdateModels(ctx); updateErr != nil {
142+ slog.Error("Failed to update models after token refresh", "error", updateErr)
143+ return nil, updateErr
144+ }
145+ }
146+ result, err := c.currentAgent.Run(ctx, SessionAgentCall{
134147 SessionID: sessionID,
135148 Prompt: prompt,
136149 Attachments: attachments,
@@ -142,6 +155,7 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
142155 FrequencyPenalty: freqPenalty,
143156 PresencePenalty: presPenalty,
144157 })
158+ return result, err
145159}
146160
147161func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
internal/config/config.go+38
@@ -16,6 +16,7 @@ import (
1616 "github.com/charmbracelet/crush/internal/csync"
1717 "github.com/charmbracelet/crush/internal/env"
1818 "github.com/charmbracelet/crush/internal/oauth"
19+ "github.com/charmbracelet/crush/internal/oauth/claude"
1920 "github.com/invopop/jsonschema"
2021 "github.com/tidwall/sjson"
2122)
@@ -468,6 +469,43 @@ func (c *Config) SetConfigField(key string, value any) error {
468469 return nil
469470}
470471
472+func (c *Config) RefreshOAuthToken(ctx context.Context, providerID string) error {
473+ providerConfig, exists := c.Providers.Get(providerID)
474+ if !exists {
475+ return fmt.Errorf("provider %s not found", providerID)
476+ }
477+
478+ if providerConfig.OAuthToken == nil {
479+ return fmt.Errorf("provider %s does not have an OAuth token", providerID)
480+ }
481+
482+ // Only Anthropic provider uses OAuth for now
483+ if providerID != string(catwalk.InferenceProviderAnthropic) {
484+ return fmt.Errorf("OAuth refresh not supported for provider %s", providerID)
485+ }
486+
487+ newToken, err := claude.RefreshToken(ctx, providerConfig.OAuthToken.RefreshToken)
488+ if err != nil {
489+ return fmt.Errorf("failed to refresh OAuth token for provider %s: %w", providerID, err)
490+ }
491+
492+ slog.Info("Successfully refreshed OAuth token in background", "provider", providerID)
493+ providerConfig.OAuthToken = newToken
494+ providerConfig.APIKey = fmt.Sprintf("Bearer %s", newToken.AccessToken)
495+ providerConfig.SetupClaudeCode()
496+
497+ c.Providers.Set(providerID, providerConfig)
498+
499+ if err := cmp.Or(
500+ c.SetConfigField(fmt.Sprintf("providers.%s.api_key", providerID), newToken.AccessToken),
501+ c.SetConfigField(fmt.Sprintf("providers.%s.oauth", providerID), newToken),
502+ ); err != nil {
503+ return fmt.Errorf("failed to persist refreshed token: %w", err)
504+ }
505+
506+ return nil
507+}
508+
471509func (c *Config) SetProviderAPIKey(providerID string, apiKey any) error {
472510 var providerConfig ProviderConfig
473511 var exists bool