diff --git a/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatError.tsx b/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatError.tsx index 511e692ff..03d92929d 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatError.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/ChatError.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import { useMemo } from 'react' import { ShareForCredits } from '@/components/referral/ShareForCredits' import { Button } from '@/components/ui/button' +import type { ProviderType } from '@/lib/llm-providers/types' const SURVEY_DIRECTIONS = [ 'competitor', @@ -15,6 +16,44 @@ function pickRandomDirection(): string { return SURVEY_DIRECTIONS[Math.floor(Math.random() * SURVEY_DIRECTIONS.length)] } +const PROVIDER_DISPLAY_NAMES: Record = { + anthropic: 'Anthropic', + openai: 'OpenAI', + 'openai-compatible': 'OpenAI-compatible', + google: 'Google', + openrouter: 'OpenRouter', + azure: 'Azure OpenAI', + ollama: 'Ollama', + lmstudio: 'LM Studio', + bedrock: 'AWS Bedrock', + browseros: 'BrowserOS', + moonshot: 'Moonshot', + 'chatgpt-pro': 'ChatGPT Pro', + 'github-copilot': 'GitHub Copilot', + 'qwen-code': 'Qwen Code', +} + +const UPSTREAM_RATE_LIMIT_PATTERNS: Array = [ + 'usage limit', + 'rate limit', + 'rate-limit', + 'quota', + /\b429\b/, + 'too many requests', + 'insufficient_quota', +] + +function getProviderDisplayName(providerType?: string): string { + if (providerType && providerType in PROVIDER_DISPLAY_NAMES) { + return PROVIDER_DISPLAY_NAMES[providerType as ProviderType] + } + return 'your provider' +} + +function stripRetryPrefix(message: string): string { + return message.replace(/^Failed after \d+ attempts?\.\s*Last error:\s*/i, '') +} + interface ChatErrorProps { error: Error onRetry?: () => void @@ -30,6 +69,8 @@ function parseErrorMessage( isRateLimit?: boolean isCreditsExhausted?: boolean isConnectionError?: boolean + isUpstreamRateLimit?: boolean + providerName?: string } { const isBrowserosProvider = providerType === 'browseros' @@ -70,6 +111,28 @@ function parseErrorMessage( } } + // Detect rate limits from non-BrowserOS upstream providers. Users were + // confused that a quota/429 from OpenAI/Anthropic/etc. looked like a + // BrowserOS-imposed limit. + if (!isBrowserosProvider && providerType) { + const lower = message.toLowerCase() + const matchesRateLimit = UPSTREAM_RATE_LIMIT_PATTERNS.some((p) => + typeof p === 'string' ? lower.includes(p) : p.test(lower), + ) + if (matchesRateLimit) { + let stripped = stripRetryPrefix(message).trim() + try { + const parsed = JSON.parse(stripped) + if (parsed?.error?.message) stripped = parsed.error.message + } catch {} + return { + text: stripped || message, + isUpstreamRateLimit: true, + providerName: getProviderDisplayName(providerType), + } + } + } + let text = message try { const parsed = JSON.parse(message) @@ -91,8 +154,15 @@ export const ChatError: FC = ({ onRetry, providerType, }) => { - const { text, url, isRateLimit, isCreditsExhausted, isConnectionError } = - parseErrorMessage(error.message, providerType) + const { + text, + url, + isRateLimit, + isCreditsExhausted, + isConnectionError, + isUpstreamRateLimit, + providerName, + } = parseErrorMessage(error.message, providerType) const surveyUrl = useMemo( () => @@ -101,6 +171,11 @@ export const ChatError: FC = ({ ) const getTitle = () => { + if (isUpstreamRateLimit) { + return providerName && providerName !== 'your provider' + ? `${providerName} rate limit reached` + : 'Upstream rate limit reached' + } if (isRateLimit) return 'Daily limit reached' if (isConnectionError) return 'Connection failed' return 'Something went wrong' @@ -113,6 +188,14 @@ export const ChatError: FC = ({ {getTitle()}

{text}

+ {isUpstreamRateLimit && ( +

+ This is a limit from{' '} + {providerName} + {' — your configured model provider — not BrowserOS. Check your '} + provider's dashboard for quota, usage, or billing details. +

+ )} {isConnectionError && url && (