From 726b2570e2d1f3cd86b1243631dc5514f7b695f8 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 15 Oct 2025 16:47:45 +0800 Subject: [PATCH] Fix/aisdk error (#10563) * Add syntax highlighting to AI SDK error cause display - Parse and format error cause as JSON with syntax highlighting - Use CodeStyleProvider context for consistent code styling - Maintain plain text fallback for non-JSON content * fix patch * chore: yarn lock * feat: provider-specific-error * chore * chore * fix: handle JSON parsing errors in AiSdkErrorBase component * fix: improve error message formatting in AiSdkToChunkAdapter * fix: remove unused MarkdownContainer and update AiSdkErrorBase to use styled div --- .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 8 ++++- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 27 +++++++++++++++-- src/renderer/src/types/error.ts | 16 +++++++++- .../src/types/provider-specific-error.ts | 29 +++++++++++++++++++ src/renderer/src/utils/error.ts | 1 + 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/types/provider-specific-error.ts diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 6d7070ce85..8627b9f010 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -6,6 +6,8 @@ import { loggerService } from '@logger' import { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types' import { Chunk, ChunkType } from '@renderer/types/chunk' +import { ProviderSpecificError } from '@renderer/types/provider-specific-error' +import { formatErrorMessage } from '@renderer/utils/error' import { convertLinks, flushLinkConverterBuffer } from '@renderer/utils/linkConverter' import type { TextStreamPart, ToolSet } from 'ai' @@ -340,7 +342,11 @@ export class AiSdkToChunkAdapter { case 'error': this.onChunk({ type: ChunkType.ERROR, - error: chunk.error as Record + error: new ProviderSpecificError({ + message: formatErrorMessage(chunk.error), + provider: 'unknown', + cause: chunk.error + }) }) break diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index 2cf85a3d63..243bec8ccc 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -1,5 +1,6 @@ import { Button } from '@heroui/button' import CodeViewer from '@renderer/components/CodeViewer' +import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useTimer } from '@renderer/hooks/useTimer' import { getHttpMessageLabel, getProviderLabel } from '@renderer/i18n/label' import { getProviderById } from '@renderer/services/ProviderService' @@ -35,7 +36,7 @@ import { import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage' import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error' import { Alert as AntdAlert, Modal } from 'antd' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import styled from 'styled-components' @@ -305,14 +306,36 @@ const BuiltinError = ({ error }: { error: SerializedError }) => { // 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染 const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => { const { t } = useTranslation() + const { highlightCode } = useCodeStyle() + const [highlightedString, setHighlightedString] = useState('') const cause = error.cause + + useEffect(() => { + const highlight = async () => { + try { + const result = await highlightCode(JSON.stringify(JSON.parse(cause || '{}'), null, 2), 'json') + setHighlightedString(result) + } catch { + setHighlightedString(cause || '') + } + } + const timer = setTimeout(highlight, 0) + + return () => clearTimeout(timer) + }, [highlightCode, cause]) + return ( <> {cause && ( {t('error.cause')}: - {error.cause} + +
+ )} diff --git a/src/renderer/src/types/error.ts b/src/renderer/src/types/error.ts index c260c9c09f..78bfe0a526 100644 --- a/src/renderer/src/types/error.ts +++ b/src/renderer/src/types/error.ts @@ -20,6 +20,7 @@ import { UnsupportedFunctionalityError } from 'ai' +import { ProviderSpecificError } from './provider-specific-error' import { Serializable } from './serialize' export interface SerializedError { @@ -80,7 +81,7 @@ export interface SerializedAiSdkInvalidArgumentError extends SerializedAiSdkErro export const isSerializedAiSdkInvalidArgumentError = ( error: SerializedError ): error is SerializedAiSdkInvalidArgumentError => { - return isSerializedAiSdkError(error) && 'parameter' in error && 'value' in error + return isSerializedAiSdkError(error) && 'message' in error && error.name === 'AI_InvalidArgumentError' } export interface SerializedAiSdkInvalidDataContentError extends SerializedAiSdkError { @@ -198,10 +199,20 @@ export interface SerializedAiSdkNoSuchToolError extends SerializedAiSdkError { readonly availableTools: string[] | null } +export interface SerializedAiSdkProviderSpecificError extends SerializedAiSdkError { + readonly provider: string +} + export const isSerializedAiSdkNoSuchToolError = (error: SerializedError): error is SerializedAiSdkNoSuchToolError => { return isSerializedAiSdkError(error) && 'toolName' in error && 'availableTools' in error } +export const isSerializedAiSdkProviderSpecificError = ( + error: SerializedError +): error is SerializedAiSdkProviderSpecificError => { + return isSerializedAiSdkError(error) && 'provider' in error +} + export interface SerializedAiSdkRetryError extends SerializedAiSdkError { readonly reason: string readonly lastError: Serializable @@ -277,6 +288,7 @@ export type AiSdkErrorUnion = | NoSuchModelError | NoSuchProviderError | NoSuchToolError + | ProviderSpecificError | RetryError | ToolCallRepairError | TypeValidationError @@ -297,6 +309,7 @@ export type SerializedAiSdkErrorUnion = | SerializedAiSdkNoSuchModelError | SerializedAiSdkNoSuchProviderError | SerializedAiSdkNoSuchToolError + | SerializedAiSdkProviderSpecificError | SerializedAiSdkRetryError | SerializedAiSdkToolCallRepairError | SerializedAiSdkTypeValidationError @@ -317,6 +330,7 @@ export const isSerializedAiSdkErrorUnion = (error: SerializedError): error is Se isSerializedAiSdkNoSuchModelError(error) || isSerializedAiSdkNoSuchProviderError(error) || isSerializedAiSdkNoSuchToolError(error) || + isSerializedAiSdkProviderSpecificError(error) || isSerializedAiSdkRetryError(error) || isSerializedAiSdkToolCallRepairError(error) || isSerializedAiSdkTypeValidationError(error) || diff --git a/src/renderer/src/types/provider-specific-error.ts b/src/renderer/src/types/provider-specific-error.ts new file mode 100644 index 0000000000..7d624b3ebe --- /dev/null +++ b/src/renderer/src/types/provider-specific-error.ts @@ -0,0 +1,29 @@ +import { AISDKError } from 'ai' + +const name = 'AI_ProviderSpecificError' +const marker = `vercel.ai.error.${name}` +const symbol = Symbol.for(marker) + +export class ProviderSpecificError extends AISDKError { + // @ts-ignore + private readonly [symbol] = true // used in isInstance + + readonly provider: string + + constructor({ + message, + provider, + cause + }: { + message: string + provider: string + cause?: unknown + }) { + super({ name, message, cause }) + this.provider = provider + } + + static isInstance(error: unknown): error is ProviderSpecificError { + return AISDKError.hasMarker(error, marker) + } +} diff --git a/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts index b69073e406..7b5667ef15 100644 --- a/src/renderer/src/utils/error.ts +++ b/src/renderer/src/utils/error.ts @@ -173,6 +173,7 @@ export const serializeError = (error: AiSdkErrorUnion): SerializedError => { ? serializeInvalidToolInputError(error.originalError) : serializeNoSuchToolError(error.originalError) if ('functionality' in error) serializedError.functionality = error.functionality + if ('provider' in error) serializedError.provider = error.provider return serializedError }