@@ -10,6 +10,7 @@ import (
11 "log/slog"
11 "log/slog"
@@ -130,32 +131,42 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
131 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
132 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
133− if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
134+ run := func() (*fantasy.AgentResult, error) {
135+ return c.currentAgent.Run(ctx, SessionAgentCall{
136+ SessionID: sessionID,
138+ Attachments: attachments,
139+ MaxOutputTokens: maxTokens,
140+ ProviderOptions: mergedOptions,
144+ FrequencyPenalty: freqPenalty,
145+ PresencePenalty: presPenalty,
148+ result, originalErr := run()
150+ if c.isUnauthorized(originalErr) {
152+ case providerCfg.OAuthToken != nil:
134− slog.Info("Detected expired OAuth token, attempting refresh", "provider", providerCfg.ID)
153+ slog.Info("Received 401. Refreshing token and retrying", "provider", providerCfg.ID)
135− if refreshErr := c.cfg.RefreshOAuthToken(ctx, providerCfg.ID); refreshErr != nil {
154+ if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
155+ return nil, originalErr
136− slog.Error("Failed to refresh OAuth token", "provider", providerCfg.ID, "error", refreshErr)
157+ slog.Info("Retrying request with refreshed OAuth token", "provider", providerCfg.ID)
137− return nil, refreshErr
158+ return run()
138− }
159+ case strings.Contains(providerCfg.APIKeyTemplate, "$"):
139−
160+ slog.Info("Received 401. Refreshing API Key template and retrying", "provider", providerCfg.ID)
140− // Rebuild models with refreshed token
161+ if err := c.refreshApiKeyTemplate(ctx, providerCfg); err != nil {
141− if updateErr := c.UpdateModels(ctx); updateErr != nil {
162+ return nil, originalErr
142− slog.Error("Failed to update models after token refresh", "error", updateErr)
163+ }
164+ slog.Info("Retrying request with refreshed API key", "provider", providerCfg.ID)
143− return nil, updateErr
165+ return run()
146− result, err := c.currentAgent.Run(ctx, SessionAgentCall{
168+
147− SessionID: sessionID,
149− Attachments: attachments,
150− MaxOutputTokens: maxTokens,
151− ProviderOptions: mergedOptions,
155− FrequencyPenalty: freqPenalty,
156− PresencePenalty: presPenalty,
158− return result, err
169+ return result, originalErr
161func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
172func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
@@ -773,3 +784,35 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error {
774 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
785 return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg))
788+func (c *coordinator) isUnauthorized(err error) bool {
789+ var providerErr *fantasy.ProviderError
790+ return errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized
793+func (c *coordinator) refreshOAuth2Token(ctx context.Context, providerCfg config.ProviderConfig) error {
794+ if err := c.cfg.RefreshOAuthToken(ctx, providerCfg.ID); err != nil {
795+ slog.Error("Failed to refresh OAuth token after 401 error", "provider", providerCfg.ID, "error", err)
798+ if err := c.UpdateModels(ctx); err != nil {
804+func (c *coordinator) refreshApiKeyTemplate(ctx context.Context, providerCfg config.ProviderConfig) error {
805+ newAPIKey, err := c.cfg.Resolve(providerCfg.APIKeyTemplate)
807+ slog.Error("Failed to re-resolve API key after 401 error", "provider", providerCfg.ID, "error", err)
811+ providerCfg.APIKey = newAPIKey
812+ c.cfg.Providers.Set(providerCfg.ID, providerCfg)
814+ if err := c.UpdateModels(ctx); err != nil {