Skip to content

Commit

Permalink
[BYOM] Improves alerts for certain API response codes (#27194)
Browse files Browse the repository at this point in the history
* improves insights for some api codes

Introduces support for response codes 401 (incorrect api key), 429 (rate limit reached, or out of credits), and 529 (temporary server overload). In these scenarios, the user will be shown a new (or modified) alert for the specific scenario encountered.

---------

Co-authored-by: Jay Harris <[email protected]>
  • Loading branch information
jonathansampson and fallaciousreasoning authored Jan 13, 2025
1 parent 379a96e commit f0cd735
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 20 deletions.
3 changes: 3 additions & 0 deletions components/ai_chat/core/browser/constants.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ base::span<const webui::LocalizedString> GetLocalizedStrings() {
IDS_CHAT_UI_MODIFY_CONFIGURATION_LABEL},
{"errorNetworkLabel", IDS_CHAT_UI_ERROR_NETWORK},
{"errorRateLimit", IDS_CHAT_UI_ERROR_RATE_LIMIT},
{"errorInvalidAPIKey", IDS_CHAT_UI_ERROR_INVALID_API_KEY},
{"errorOAIRateLimit", IDS_CHAT_UI_ERROR_OAI_RATE_LIMIT},
{"errorServiceOverloaded", IDS_CHAT_UI_ERROR_SERVICE_OVERLOADED},
{"retryButtonLabel", IDS_CHAT_UI_RETRY_BUTTON_LABEL},
{"introMessage-chat-basic", IDS_CHAT_UI_INTRO_MESSAGE_CHAT_BASIC},
{"introMessage-chat-leo-expanded",
Expand Down
22 changes: 21 additions & 1 deletion components/ai_chat/core/browser/engine/oai_api_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,28 @@ void OAIAPIClient::OnQueryCompleted(GenerationCompletedCallback callback,
return;
}

// Determine which type of error occurred.
// https://platform.openai.com/docs/guides/error-codes
// https://docs.anthropic.com/en/api/errors
mojom::APIError error;

switch (result.response_code()) {
case 401: // Incorrect API key provided
error = mojom::APIError::InvalidAPIKey;
break;
case 429: // Rate limit reached or out of credits
error = mojom::APIError::RateLimitReached;
break;
case 529: // Temporary server overload
error = mojom::APIError::ServiceOverloaded;
break;
default:
error = mojom::APIError::ConnectionIssue;
break;
}

// Handle error
std::move(callback).Run(base::unexpected(mojom::APIError::ConnectionIssue));
std::move(callback).Run(base::unexpected(error));
}

void OAIAPIClient::OnQueryDataReceived(
Expand Down
4 changes: 3 additions & 1 deletion components/ai_chat/core/common/mojom/ai_chat.mojom
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ enum APIError {
ConnectionIssue,
RateLimitReached,
ContextLimitReached,
InvalidEndpointURL
InvalidEndpointURL,
InvalidAPIKey,
ServiceOverloaded
};

enum ModelEngineType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* Copyright (c) 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

import * as React from 'react'
import Alert from '@brave/leo/react/alert'
import Button from '@brave/leo/react/button'
import { getLocale } from '$web-common/locale'
import { useAIChat } from '../../state/ai_chat_context'
import styles from './alerts.module.scss'

interface ElementProps {
onRetry?: () => void
}

export default function ErrorInvalidAPIKey(props: ElementProps) {
const aiChatContext = useAIChat()

const handleConfigureClick = () => {
aiChatContext.uiHandler?.openAIChatSettings()
}

return (
<div className={styles.alert}>
<Alert type='warning'>
{getLocale('errorInvalidAPIKey')}
<Button
slot='actions'
kind='filled'
onClick={handleConfigureClick}
>
{getLocale('customModelModifyConfigurationLabel')}
</Button>
<Button
slot='actions'
kind='filled'
onClick={props.onRetry}
>
{getLocale('retryButtonLabel')}
</Button>
</Alert>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

import * as React from 'react'
import Alert from '@brave/leo/react/alert'
import Button from '@brave/leo/react/button'
import { getLocale } from '$web-common/locale'
import { useAIChat } from '../../state/ai_chat_context'
import styles from './alerts.module.scss'

export default function ErrorInvalidEndpointURL () {
import * as React from 'react'
import Alert from '@brave/leo/react/alert'
import Button from '@brave/leo/react/button'
import { getLocale } from '$web-common/locale'
import { useAIChat } from '../../state/ai_chat_context'
import styles from './alerts.module.scss'

export default function ErrorInvalidEndpointURL() {
const aiChatContext = useAIChat()

const handleConfigureClick = () => {
Expand All @@ -20,16 +19,14 @@ export default function ErrorInvalidEndpointURL () {

return (
<div className={styles.alert}>
<Alert
type='error'
>
<Alert type='error'>
{getLocale('customModelInvalidEndpoint')}
<Button
slot='actions'
kind='filled'
onClick={handleConfigureClick}
>
{getLocale('customModelModifyConfigurationLabel')}
{getLocale('customModelModifyConfigurationLabel')}
</Button>
</Alert>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,46 @@ import { useConversation } from '../../state/conversation_context'
import PremiumSuggestion from '../premium_suggestion'
import styles from './alerts.module.scss'

function ErrorRateLimit() {
interface Props {
_testIsCurrentModelLeo?: boolean
}

function ErrorRateLimit(props: Props) {
const aiChatContext = useAIChat()
const conversationContext = useConversation()

// Respond to BYOM scenarios
if (
!conversationContext.isCurrentModelLeo ||
props._testIsCurrentModelLeo === false
) {
return (
<div className={styles.alert}>
<Alert type='warning'>
{getLocale('errorOAIRateLimit')}
<Button
slot='actions'
kind='filled'
onClick={conversationContext.retryAPIRequest}
>
{getLocale('retryButtonLabel')}
</Button>
</Alert>
</div>
)
}

// Respond to Leo (i.e., non-BYOM) scenarios
if (!aiChatContext.isPremiumUser) {
return (
<PremiumSuggestion
title={getLocale('rateLimitReachedTitle')}
description={getLocale('rateLimitReachedDesc')}
secondaryActionButton={
<Button kind='plain-faint' onClick={conversationContext.handleResetError}>
<Button
kind='plain-faint'
onClick={conversationContext.handleResetError}
>
{getLocale('maybeLaterLabel')}
</Button>
}
Expand All @@ -32,16 +61,14 @@ function ErrorRateLimit() {

return (
<div className={styles.alert}>
<Alert
type='warning'
>
<Alert type='warning'>
{getLocale('errorRateLimit')}
<Button
slot='actions'
kind='filled'
onClick={conversationContext.retryAPIRequest}
>
{getLocale('retryButtonLabel')}
{getLocale('retryButtonLabel')}
</Button>
</Alert>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* Copyright (c) 2023 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */

import * as React from 'react'
import Alert from '@brave/leo/react/alert'
import Button from '@brave/leo/react/button'
import { getLocale } from '$web-common/locale'
import styles from './alerts.module.scss'

interface ElementProps {
onRetry?: () => void
}

export default function ErrorServiceOverloaded(props: ElementProps) {
return (
<div className={styles.alert}>
<Alert type='error'>
{getLocale('errorServiceOverloaded')}
<Button
slot='actions'
kind='filled'
onClick={props.onRetry}
>
{getLocale('retryButtonLabel')}
</Button>
</Alert>
</div>
)
}
10 changes: 10 additions & 0 deletions components/ai_chat/resources/page/components/main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { useAIChat } from '../../state/ai_chat_context'
import { isLeoModel } from '../../model_utils'
import ErrorConnection from '../alerts/error_connection'
import ErrorConversationEnd from '../alerts/error_conversation_end'
import ErrorInvalidAPIKey from '../alerts/error_invalid_api_key'
import ErrorInvalidEndpointURL from '../alerts/error_invalid_endpoint_url'
import ErrorRateLimit from '../alerts/error_rate_limit'
import ErrorServiceOverloaded from '../alerts/error_service_overloaded'
import LongConversationInfo from '../alerts/long_conversation_info'
import NoticeConversationStorage from '../notices/notice_conversation_storage'
import WarningPremiumDisconnected from '../alerts/warning_premium_disconnected'
Expand Down Expand Up @@ -93,6 +95,14 @@ function Main() {
currentErrorElement = <ErrorConnection
onRetry={conversationContext.retryAPIRequest} />
break
case Mojom.APIError.InvalidAPIKey:
currentErrorElement = <ErrorInvalidAPIKey
onRetry={conversationContext.retryAPIRequest} />
break
case Mojom.APIError.ServiceOverloaded:
currentErrorElement = <ErrorServiceOverloaded
onRetry={conversationContext.retryAPIRequest} />
break
case Mojom.APIError.RateLimitReached:
currentErrorElement = <ErrorRateLimit />
break
Expand Down
26 changes: 26 additions & 0 deletions components/ai_chat/resources/page/stories/components_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import ACTIONS_LIST from './story_utils/actions'
import styles from './style.module.scss'
import StorybookConversationEntries from './story_utils/ConversationEntries'
import { UntrustedConversationContext, UntrustedConversationReactContext } from '../../untrusted_conversation_frame/untrusted_conversation_context'
import ErrorConnection from '../components/alerts/error_connection'
import ErrorConversationEnd from '../components/alerts/error_conversation_end'
import ErrorInvalidAPIKey from '../components/alerts/error_invalid_api_key'
import ErrorInvalidEndpointURL from '../components/alerts/error_invalid_endpoint_url'
import ErrorRateLimit from '../components/alerts/error_rate_limit'
import ErrorServiceOverloaded from '../components/alerts/error_service_overloaded'
import LongConversationInfo from '../components/alerts/long_conversation_info'
import WarningPremiumDisconnected from '../components/alerts/warning_premium_disconnected'

function getCompletionEvent(text: string): Mojom.ConversationEntryEvent {
return {
Expand Down Expand Up @@ -635,6 +643,24 @@ export const _Panel: Story = {
}
}

export const _Alerts = {
render: () => {
return (
<div className={`${styles.container} ${styles.containerAlerts}`}>
<ErrorConnection />
<ErrorConversationEnd />
<ErrorInvalidAPIKey />
<ErrorInvalidEndpointURL />
<ErrorRateLimit />
<ErrorRateLimit _testIsCurrentModelLeo={false} />
<ErrorServiceOverloaded />
<LongConversationInfo />
<WarningPremiumDisconnected />
</div>
)
}
}

export const _FeedbackForm = {
render: () => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ provideStrings({
aboutDescription_3: 'Leo does not collect or otherwise process identifiers such as IP Address that can be linked to you. No personal data is retained by the AI model or any 3rd-party model providers.',
acceptButtonLabel: 'Accept and begin',
pageContentWarning: 'Disconnect to stop sending this page content to Leo, and start a new conversation',
customModelInvalidEndpoint: 'This model has an invalid endpoint. Please check your configuration and try again.',
customModelModifyConfigurationLabel: 'Configure',
errorNetworkLabel: 'There was a network issue connecting to Leo, check your connection and try again.',
errorOAIRateLimit: 'You\'ve reached the rate limit for this model or are out of credits. Please try again later.',
errorInvalidAPIKey: 'The API key configured for this model is invalid. Please check your configuration and try again.',
errorRateLimit: 'You\'ve reached the premium rate limit. Please try again in a few hours.',
errorServiceOverloaded: 'The endpoint is currently overloaded. Please try again later.',
braveLeoAssistantEndpointInvalidError: 'The endpoint URL is invalid. Please check the URL and try again.',
braveLeoAssistantEndpointValidAsPrivateIp: 'If you would like to use a private IP address, you must first enable "Private IP Addresses for Custom Model Enpoints" via brave://flags/#brave-ai-chat-allow-private-ips',
retryButtonLabel: 'Retry',
Expand Down
7 changes: 7 additions & 0 deletions components/ai_chat/resources/page/stories/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
overflow: hidden;
}

.containerAlerts {
display: flex;
flex-direction: column;
gap: 1em;
padding: 1em;
}

.containerFull {
width: calc(100vw - 30px);
height: 95vh;
Expand Down
9 changes: 9 additions & 0 deletions components/resources/ai_chat_ui_strings.grdp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
<message name="IDS_CHAT_UI_ERROR_RATE_LIMIT" desc="An error presented when the user has exhausted the API request limit">
You've reached the premium rate limit. Please try again in a few hours.
</message>
<message name="IDS_CHAT_UI_ERROR_OAI_RATE_LIMIT" desc="An error presented when the BYOM user has exhausted the API request limit">
You've reached the rate limit for this model or are out of credits. Please try again later.
</message>
<message name="IDS_CHAT_UI_ERROR_INVALID_API_KEY" desc="An error presented when the configured API key is invalid">
The API key configured for this model is invalid. Please check your configuration and try again.
</message>
<message name="IDS_CHAT_UI_ERROR_SERVICE_OVERLOADED" desc="An error presented when the API service is overloaded">
The endpoint is currently overloaded. Please try again later.
</message>
<message name="IDS_CHAT_UI_RETRY_BUTTON_LABEL" desc="A button label to retry API again">
Retry
</message>
Expand Down

0 comments on commit f0cd735

Please sign in to comment.