@@ -184,13 +184,10 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
184184
185185 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
186186
187โ if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
188โ slog.Debug("Token needs to be refreshed", "provider", providerCfg.ID)
189โ if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
190โ // NOTE(@andreynering): We don't return here because the event handling to ask the user to reauthenticate
191โ // depends on the flow below. If refresh fails, proceed with the token we have.
192โ slog.Error("Failed to refresh OAuth2 token. Proceeding with existing token.", "error", err)
193โ }
187+ if err := c.refreshTokenIfExpired(ctx, providerCfg); err != nil {
188+ // NOTE(@andreynering): We don't return here because the event handling to ask the user to reauthenticate
189+ // depends on the flow below. If refresh fails, proceed with the token we have.
190+ slog.Error("Failed to refresh OAuth2 token. Proceeding with existing token.", "error", err)
194191 }
195192
196193 run := func() (*fantasy.AgentResult, error) {
@@ -212,20 +209,7 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
212209 logTurnSkillUsage(sessionID, prompt, c.activeSkills, c.skillTracker, beforeLoaded)
213210
214211 if c.isUnauthorized(originalErr) {
215โ switch {
216โ case providerCfg.OAuthToken != nil:
217โ slog.Debug("Received 401. Refreshing token and retrying", "provider", providerCfg.ID)
218โ if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
219โ return nil, originalErr
220โ }
221โ slog.Debug("Retrying request with refreshed OAuth token", "provider", providerCfg.ID)
222โ return run()
223โ case strings.Contains(providerCfg.APIKeyTemplate, "$"):
224โ slog.Debug("Received 401. Refreshing API Key template and retrying", "provider", providerCfg.ID)
225โ if err := c.refreshApiKeyTemplate(ctx, providerCfg); err != nil {
226โ return nil, originalErr
227โ }
228โ slog.Debug("Retrying request with refreshed API key", "provider", providerCfg.ID)
212+ if err := c.retryAfterUnauthorized(ctx, providerCfg); err == nil {
229213 return run()
230214 }
231215 }
@@ -962,12 +946,8 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
962946 return errModelProviderNotConfigured
963947 }
964948
965โ // Proactively refresh OAuth token if expired, same as Run().
966โ if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
967โ slog.Debug("Token needs to be refreshed before summarize", "provider", providerCfg.ID)
968โ if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
969โ slog.Error("Failed to refresh OAuth2 token before summarize. Proceeding with existing token.", "error", err)
970โ }
949+ if err := c.refreshTokenIfExpired(ctx, providerCfg); err != nil {
950+ slog.Error("Failed to refresh OAuth2 token before summarize. Proceeding with existing token.", "error", err)
971951 }
972952
973953 summarize := func() error {
@@ -976,18 +956,7 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
976956
977957 err := summarize()
978958 if err != nil && c.isUnauthorized(err) {
979โ switch {
980โ case providerCfg.OAuthToken != nil:
981โ slog.Debug("Received 401 during summarize. Refreshing token and retrying", "provider", providerCfg.ID)
982โ if refreshErr := c.refreshOAuth2Token(ctx, providerCfg); refreshErr != nil {
983โ return err
984โ }
985โ return summarize()
986โ case strings.Contains(providerCfg.APIKeyTemplate, "$"):
987โ slog.Debug("Received 401 during summarize. Refreshing API Key template and retrying", "provider", providerCfg.ID)
988โ if refreshErr := c.refreshApiKeyTemplate(ctx, providerCfg); refreshErr != nil {
989โ return err
990โ }
959+ if retryErr := c.retryAfterUnauthorized(ctx, providerCfg); retryErr == nil {
991960 return summarize()
992961 }
993962 }
@@ -995,6 +964,30 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
995964 return err
996965}
997966
967+// refreshTokenIfExpired proactively refreshes the OAuth token if it has expired.
968+func (c *coordinator) refreshTokenIfExpired(ctx context.Context, providerCfg config.ProviderConfig) error {
969+ if providerCfg.OAuthToken == nil || !providerCfg.OAuthToken.IsExpired() {
970+ return nil
971+ }
972+ slog.Debug("Token needs to be refreshed", "provider", providerCfg.ID)
973+ return c.refreshOAuth2Token(ctx, providerCfg)
974+}
975+
976+// retryAfterUnauthorized attempts to refresh credentials after receiving a 401
977+// and returns nil if retry should be attempted.
978+func (c *coordinator) retryAfterUnauthorized(ctx context.Context, providerCfg config.ProviderConfig) error {
979+ switch {
980+ case providerCfg.OAuthToken != nil:
981+ slog.Debug("Received 401. Refreshing token and retrying", "provider", providerCfg.ID)
982+ return c.refreshOAuth2Token(ctx, providerCfg)
983+ case strings.Contains(providerCfg.APIKeyTemplate, "$"):
984+ slog.Debug("Received 401. Refreshing API Key template and retrying", "provider", providerCfg.ID)
985+ return c.refreshApiKeyTemplate(ctx, providerCfg)
986+ default:
987+ return nil
988+ }
989+}
990+
998991func (c *coordinator) isUnauthorized(err error) bool {
999992 var providerErr *fantasy.ProviderError
1000993 return errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized