From 8db2e34ab5db150e9fe83ae0ff67839eae25b30e Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Mon, 27 Jul 2026 02:26:33 +0530 Subject: [PATCH] show specific AI provider errors instead of a generic message --- cmd/ai.go | 6 +++--- i18n/en-US.json | 7 ++++++- internal/ai/ai.go | 20 +++++++++++++++++--- internal/ai/openai.go | 6 +++--- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/cmd/ai.go b/cmd/ai.go index b8bb0245..73c98fcf 100644 --- a/cmd/ai.go +++ b/cmd/ai.go @@ -293,7 +293,7 @@ func handleAIGenerateReply(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } if strings.TrimSpace(resp) == "" { - return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.somethingWentWrong"), nil)) + return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil)) } return r.SendEnvelope(resp) } @@ -322,7 +322,7 @@ func handleAISummarizeConversation(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } if strings.TrimSpace(summary) == "" { - return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.somethingWentWrong"), nil)) + return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil)) } auser := r.RequestCtx.UserValue("user").(amodels.User) note := "**" + app.i18n.T("ai.summaryNoteTitle") + "**\n\n" + summary @@ -421,7 +421,7 @@ func handleAICopilot(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } if strings.TrimSpace(resp) == "" { - return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("globals.messages.somethingWentWrong"), nil)) + return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("ai.emptyResponse"), nil)) } // Copilot answers in markdown; store and return HTML so the panel, reply editor and private notes consume it as is. resp = stringutil.Markdown2HTML(resp) diff --git a/i18n/en-US.json b/i18n/en-US.json index f69190b1..a36bd353 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -552,7 +552,12 @@ "agent.generateApiKey": "Generate API key", "agent.new": "New agent", "agent.selectAvailabilityStatus": "Select availability status", - "ai.apiKeyNotSet": "The AI provider API key is not set. Please ask your administrator to set it up", + "ai.apiKeyNotSet": "The AI provider API key is not set. Ask your administrator to add it in AI settings.", + "ai.invalidAPIKey": "The AI provider rejected the API key. AI features won't work until your administrator updates it in AI settings.", + "ai.rateLimited": "The AI provider is rate limiting requests. Wait a minute and try again.", + "ai.providerUnavailable": "The AI provider could not be reached. Try again in a few minutes, or ask your administrator to check the AI settings.", + "ai.providerTimeout": "The AI provider took too long to respond. Try again, or with a shorter request.", + "ai.emptyResponse": "The AI returned an empty response. Nothing was generated. Try again, or rephrase your request.", "ai.faqAlreadyReviewed": "This suggestion has already been reviewed.", "ai.noAnswerWithinSteps": "The AI hit its step limit before answering. Try a simpler request, or ask an admin to raise the limit.", "ai.agent.handoffDefault": "The AI assistant handed this conversation off to a human.", diff --git a/internal/ai/ai.go b/internal/ai/ai.go index f39ad633..ddbbc3db 100644 --- a/internal/ai/ai.go +++ b/internal/ai/ai.go @@ -34,8 +34,10 @@ var ( //go:embed queries.sql efs embed.FS - ErrInvalidAPIKey = errors.New("invalid API Key") - ErrApiKeyNotSet = errors.New("api Key not set") + ErrInvalidAPIKey = errors.New("invalid API Key") + ErrApiKeyNotSet = errors.New("api Key not set") + ErrRateLimited = errors.New("rate limited by AI provider") + ErrProviderUnavailable = errors.New("AI provider unavailable") ) type Manager struct { @@ -382,11 +384,23 @@ func (m *Manager) getProviderClient(providerType string) (ProviderClient, error) func (m *Manager) providerError(err error) error { if errors.Is(err, ErrInvalidAPIKey) { m.lo.Error("invalid provider API key") - return envelope.NewError(envelope.InputError, m.i18n.T("globals.messages.somethingWentWrong"), nil) + return envelope.NewError(envelope.InputError, m.i18n.T("ai.invalidAPIKey"), nil) } if errors.Is(err, ErrApiKeyNotSet) { return envelope.NewError(envelope.InputError, m.i18n.T("ai.apiKeyNotSet"), nil) } + if errors.Is(err, ErrRateLimited) { + m.lo.Error("rate limited by AI provider", "error", err) + return envelope.NewError(envelope.RateLimitError, m.i18n.T("ai.rateLimited"), nil) + } + if errors.Is(err, context.DeadlineExceeded) { + m.lo.Error("AI provider request timed out", "error", err) + return envelope.NewError(envelope.GeneralError, m.i18n.T("ai.providerTimeout"), nil) + } + if errors.Is(err, ErrProviderUnavailable) { + m.lo.Error("AI provider unavailable", "error", err) + return envelope.NewError(envelope.GeneralError, m.i18n.T("ai.providerUnavailable"), nil) + } m.lo.Error("error from AI provider", "error", err) return envelope.NewError(envelope.GeneralError, capProviderErrorMessage(err), nil) } diff --git a/internal/ai/openai.go b/internal/ai/openai.go index 2d73a11d..42d5a757 100644 --- a/internal/ai/openai.go +++ b/internal/ai/openai.go @@ -351,7 +351,7 @@ func (o *OpenAIClient) doRequest(ctx context.Context, path string, bodyBytes []b resp, err := o.client.Do(req) if err != nil { o.lo.Error("error making request to AI provider", "error", err) - return nil, 0, true, fmt.Errorf("making HTTP request: %w", err) + return nil, 0, true, fmt.Errorf("%w: making HTTP request: %w", ErrProviderUnavailable, err) } defer resp.Body.Close() @@ -368,10 +368,10 @@ func (o *OpenAIClient) doRequest(ctx context.Context, path string, bodyBytes []b return nil, 0, false, ErrInvalidAPIKey case resp.StatusCode == http.StatusTooManyRequests: o.lo.Error("rate limited by AI provider (429)", "response", string(respBytes)) - return nil, parseRetryAfter(resp.Header.Get("Retry-After")), true, fmt.Errorf("provider API error: status %d: %s", resp.StatusCode, string(respBytes)) + return nil, parseRetryAfter(resp.Header.Get("Retry-After")), true, fmt.Errorf("%w: status %d: %s", ErrRateLimited, resp.StatusCode, string(respBytes)) case resp.StatusCode >= 500: o.lo.Error("server error from AI provider", "status", resp.StatusCode, "response", string(respBytes)) - return nil, 0, true, fmt.Errorf("provider API error: status %d: %s", resp.StatusCode, string(respBytes)) + return nil, 0, true, fmt.Errorf("%w: status %d: %s", ErrProviderUnavailable, resp.StatusCode, string(respBytes)) default: o.lo.Error("non-ok response from AI provider", "status", resp.StatusCode, "response", string(respBytes)) return respBytes, 0, false, fmt.Errorf("provider API error: status %d: %s", resp.StatusCode, string(respBytes))