From 50a66bb6f14b65a9165b44a3e7a1a550e7b1ad54 Mon Sep 17 00:00:00 2001 From: Iuliia Ivashko Date: Fri, 20 Feb 2026 00:34:34 +0200 Subject: [PATCH] fix(auth): show actionable provider re-auth errors (#450) --- .../ui/src/components/chat/ChatMessage.tsx | 4 +++ .../ui/src/lib/messages/providerAuthError.ts | 34 +++++++++++++++++++ packages/ui/src/stores/messageStore.ts | 5 +++ .../web/server/lib/quota/providers/codex.js | 4 ++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/lib/messages/providerAuthError.ts diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index adfbe781..32db1ac8 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -22,6 +22,7 @@ import type { StreamPhase, ToolPopupContent } from './message/types'; import { deriveMessageRole } from './message/messageRole'; import { filterVisibleParts } from './message/partUtils'; import { flattenAssistantTextParts } from '@/lib/messages/messageText'; +import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError'; import { FadeInOnReveal } from './message/FadeInOnReveal'; import type { TurnGroupingContext } from './hooks/useTurnGrouping'; @@ -646,6 +647,9 @@ const ChatMessage: React.FC = ({ if (errorName === 'SessionRetry') { return `Opencode failed to send a message. Retry attempt info: \n\`${detail}\``; } + if (isLikelyProviderAuthFailure(detail)) { + return PROVIDER_AUTH_FAILURE_MESSAGE; + } return `Opencode failed to send message with error:\n\`${detail}\``; }, [isUser, message.info]); diff --git a/packages/ui/src/lib/messages/providerAuthError.ts b/packages/ui/src/lib/messages/providerAuthError.ts new file mode 100644 index 00000000..1a49a14e --- /dev/null +++ b/packages/ui/src/lib/messages/providerAuthError.ts @@ -0,0 +1,34 @@ +export const PROVIDER_AUTH_FAILURE_MESSAGE = "Authentication failed for this provider. Please re-authenticate and retry."; + +export const isLikelyProviderAuthFailure = (value: unknown): boolean => { + if (typeof value !== "string") { + return false; + } + + const detail = value.toLowerCase().trim(); + if (!detail) { + return false; + } + + if ( + detail.includes("token refresh failed") || + detail.includes("unauthorized") || + detail.includes("invalid token") || + detail.includes("expired token") + ) { + return true; + } + + const hasOauth = detail.includes("oauth"); + const hasOauthFailure = + detail.includes("failed") || detail.includes("invalid") || detail.includes("expired"); + if (hasOauth && hasOauthFailure) { + return true; + } + + const has401 = /\b401\b/.test(detail); + const hasAuthContext = + detail.includes("auth") || detail.includes("token") || detail.includes("unauthorized"); + + return has401 && hasAuthContext; +}; diff --git a/packages/ui/src/stores/messageStore.ts b/packages/ui/src/stores/messageStore.ts index b50a36af..fc17a9d4 100644 --- a/packages/ui/src/stores/messageStore.ts +++ b/packages/ui/src/stores/messageStore.ts @@ -4,6 +4,7 @@ import { devtools, persist, createJSONStorage } from "zustand/middleware"; import type { Message, Part } from "@opencode-ai/sdk/v2"; import { opencodeClient } from "@/lib/opencode/client"; import { isExecutionForkMetaText } from "@/lib/messages/executionMeta"; +import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from "@/lib/messages/providerAuthError"; import type { SessionMemoryState, MessageStreamLifecycle, AttachedFile } from "./types/sessionTypes"; import { MEMORY_LIMITS, getMemoryLimits, getBackgroundTrimLimit } from "./types/sessionTypes"; import { @@ -787,6 +788,8 @@ export const useMessageStore = create()( return { abortControllers: nextControllers }; }); return; + } else if (isLikelyProviderAuthFailure(error.message)) { + errorMessage = PROVIDER_AUTH_FAILURE_MESSAGE; } else if (error.message) { errorMessage = error.message; } @@ -814,6 +817,8 @@ export const useMessageStore = create()( errorMessage = "OpenCode is restarting. Please wait a moment and try again."; } else if (error.message?.includes("504") || error.message?.includes("Gateway")) { errorMessage = "Gateway timeout - your message is being processed. Please wait for response."; + } else if (isLikelyProviderAuthFailure(error.message)) { + errorMessage = PROVIDER_AUTH_FAILURE_MESSAGE; } else if (error.message) { errorMessage = error.message; } diff --git a/packages/web/server/lib/quota/providers/codex.js b/packages/web/server/lib/quota/providers/codex.js index 4aec2ca5..a323d3f6 100644 --- a/packages/web/server/lib/quota/providers/codex.js +++ b/packages/web/server/lib/quota/providers/codex.js @@ -52,7 +52,9 @@ export const fetchQuota = async () => { providerName, ok: false, configured: true, - error: `API error: ${response.status}` + error: response.status === 401 + ? 'Session expired \u2014 please re-authenticate with OpenAI' + : `API error: ${response.status}` }); }