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
This commit is contained in:
SuYao 2025-10-15 16:47:45 +08:00 committed by GitHub
parent 4028b26c1d
commit f27a481c3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 77 additions and 4 deletions

View File

@ -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 { ClaudeCodeRawValue } from '@shared/agents/claudecode/types'
import type { TextStreamPart, ToolSet } from 'ai'
@ -355,7 +357,11 @@ export class AiSdkToChunkAdapter {
case 'error':
this.onChunk({
type: ChunkType.ERROR,
error: chunk.error as Record<string, any>
error: new ProviderSpecificError({
message: formatErrorMessage(chunk.error),
provider: 'unknown',
cause: chunk.error
})
})
break

View File

@ -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 (
<>
<BuiltinError error={error} />
{cause && (
<ErrorDetailItem>
<ErrorDetailLabel>{t('error.cause')}:</ErrorDetailLabel>
<ErrorDetailValue>{error.cause}</ErrorDetailValue>
<ErrorDetailValue>
<div
className="markdown [&_pre]:!bg-transparent [&_pre_span]:whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: highlightedString }}
/>
</ErrorDetailValue>
</ErrorDetailItem>
)}
</>

View File

@ -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) ||

View File

@ -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)
}
}

View File

@ -199,6 +199,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
}