From 7f2e38790305d1d30c52121eb7f3943d485d90e5 Mon Sep 17 00:00:00 2001 From: Felarof Date: Thu, 16 Apr 2026 16:14:45 -0700 Subject: [PATCH] fix(agent): clarify upstream provider rate-limit errors (#734) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(agent): clarify upstream provider rate-limit errors When a non-BrowserOS provider (OpenAI, Anthropic, OpenRouter, etc.) returned a 429, ChatError rendered the retry-wrapped message "Failed after 3 attempts. Last error: The usage limit has been reached" with a generic "Something went wrong" title, leading users to blame BrowserOS for throttling imposed by their configured upstream. Detect upstream 429s in parseErrorMessage, show the provider name in the title ("OpenAI rate limit reached"), strip the retry prefix, render the raw upstream message, and add clarifying subtext that names the provider and explicitly excludes BrowserOS. Skip the BrowserOS-specific ShareForCredits / survey / upgrade affordances on this path — they do not apply. Co-Authored-By: Claude Opus 4.7 (1M context) * fix: address Greptile review comments - Tighten 429 pattern to \b429\b so it only matches the standalone status code, not incidental substrings (model IDs, paths, etc.). - Unwrap JSON-encoded provider error bodies on the upstream-rate-limit path so users see the human-readable message instead of raw JSON. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../entrypoints/sidepanel/index/ChatError.tsx | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) 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 && (