mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
🐛 fix: preserve model fetch error messages
This commit is contained in:
@@ -119,6 +119,33 @@ describe('GET handler', () => {
|
||||
|
||||
expect(responseBody.body.error.code).toBe('PROVIDER_ERROR');
|
||||
expect(responseBody.body.error.details).toBe('API limit exceeded');
|
||||
expect(responseBody.body.message).toBe('API limit exceeded');
|
||||
});
|
||||
|
||||
it('should prefer nested provider messages over wrapper messages', async () => {
|
||||
const mockParams = Promise.resolve({ provider: 'cloudflare' });
|
||||
|
||||
const structuredError = {
|
||||
errorType: AgentRuntimeErrorType.ProviderBizError,
|
||||
message: 'Provider request failed',
|
||||
error: {
|
||||
errors: [{ message: 'Cloudflare authentication error' }],
|
||||
message: 'Request failed',
|
||||
result: null,
|
||||
},
|
||||
};
|
||||
|
||||
const mockRuntime: LobeRuntimeAI = {
|
||||
baseURL: 'abc',
|
||||
chat: vi.fn(),
|
||||
models: vi.fn().mockRejectedValue(structuredError),
|
||||
};
|
||||
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
|
||||
|
||||
const response = await GET(request, { params: mockParams });
|
||||
const responseBody = await response.json();
|
||||
|
||||
expect(responseBody.body.message).toBe('Cloudflare authentication error');
|
||||
});
|
||||
|
||||
it('should return provider biz error for unstructured errors', async () => {
|
||||
|
||||
@@ -140,6 +140,42 @@ const normalizeModelFetchError = (error: unknown): Record<string, unknown> => {
|
||||
return { message: safeError === undefined ? 'Unknown error' : String(safeError) };
|
||||
};
|
||||
|
||||
const extractModelFetchErrorMessage = (
|
||||
error: unknown,
|
||||
seen = new WeakSet<object>(),
|
||||
depth = 0,
|
||||
): string | undefined => {
|
||||
if (error === null || error === undefined) return;
|
||||
if (typeof error === 'string') return error || undefined;
|
||||
if (typeof error === 'number' || typeof error === 'boolean' || typeof error === 'bigint') {
|
||||
return String(error);
|
||||
}
|
||||
if (!isRecord(error)) return;
|
||||
if (seen.has(error) || depth >= MAX_ERROR_DEPTH) return;
|
||||
|
||||
seen.add(error);
|
||||
|
||||
const record = error as Record<string, unknown>;
|
||||
const nestedErrorKeys = ['error', 'body', 'cause', 'response', 'detail', 'details', 'reason'];
|
||||
|
||||
for (const key of nestedErrorKeys) {
|
||||
const message = extractModelFetchErrorMessage(record[key], seen, depth + 1);
|
||||
if (message) return message;
|
||||
}
|
||||
|
||||
if (Array.isArray(record.errors)) {
|
||||
for (const item of record.errors) {
|
||||
const message = extractModelFetchErrorMessage(item, seen, depth + 1);
|
||||
if (message) return message;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
if (typeof record.message === 'string' && record.message) return record.message;
|
||||
if (typeof record.status === 'number') return `HTTP ${record.status}`;
|
||||
if (typeof record.statusCode === 'number') return `HTTP ${record.statusCode}`;
|
||||
};
|
||||
|
||||
const normalizeModelListResponse = (list: unknown) => toJsonSafeValue(list);
|
||||
|
||||
export const GET = checkAuth(async (req, { params, userId, serverDB }) => {
|
||||
@@ -158,9 +194,9 @@ export const GET = checkAuth(async (req, { params, userId, serverDB }) => {
|
||||
const errorPayload = isRecord(e) ? (e as Partial<ChatCompletionErrorPayload>) : undefined;
|
||||
const errorType = errorPayload?.errorType || AgentRuntimeErrorType.ProviderBizError;
|
||||
const errorContent = errorPayload?.error;
|
||||
const message = errorPayload?.message;
|
||||
|
||||
const error = errorContent || e;
|
||||
const message = extractModelFetchErrorMessage(error) || errorPayload?.message;
|
||||
// track the error at server side
|
||||
console.error(`Route: [${provider}] ${errorType}:`, error);
|
||||
|
||||
|
||||
@@ -169,10 +169,14 @@ const ModelFetchErrorAlert = memo<ModelFetchErrorAlertProps>(({ error, provider
|
||||
|
||||
if (!error) return null;
|
||||
|
||||
const title = getRuntimeErrorMessage(t, error.type, { provider: providerName });
|
||||
const description = error.message && error.message !== title ? error.message : undefined;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
showIcon
|
||||
title={getRuntimeErrorMessage(t, error.type, { provider: providerName })}
|
||||
description={description}
|
||||
title={title}
|
||||
type={'error'}
|
||||
extra={
|
||||
<Flexbox paddingBlock={8} paddingInline={16}>
|
||||
|
||||
@@ -130,6 +130,62 @@ describe('ModelsService', () => {
|
||||
error: { message: 'Provider failed' },
|
||||
provider: 'openai',
|
||||
},
|
||||
message: 'Provider failed',
|
||||
type: AgentRuntimeErrorType.ProviderBizError,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract nested provider message from parsed server errors', async () => {
|
||||
(fetch as Mock).mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
body: {
|
||||
error: {
|
||||
errors: [{ message: 'Cloudflare authentication error' }],
|
||||
result: null,
|
||||
},
|
||||
provider: 'cloudflare',
|
||||
},
|
||||
errorType: AgentRuntimeErrorType.ProviderBizError,
|
||||
}),
|
||||
{ status: 471 },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(modelsService.getModels('cloudflare')).rejects.toMatchObject({
|
||||
body: {
|
||||
error: {
|
||||
errors: [{ message: 'Cloudflare authentication error' }],
|
||||
result: null,
|
||||
},
|
||||
provider: 'cloudflare',
|
||||
},
|
||||
message: 'Cloudflare authentication error',
|
||||
type: AgentRuntimeErrorType.ProviderBizError,
|
||||
});
|
||||
});
|
||||
|
||||
it('should prefer nested provider messages over parsed wrapper messages', async () => {
|
||||
(fetch as Mock).mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
body: {
|
||||
error: {
|
||||
errors: [{ message: 'Cloudflare authentication error' }],
|
||||
message: 'Request failed',
|
||||
result: null,
|
||||
},
|
||||
provider: 'cloudflare',
|
||||
},
|
||||
errorType: AgentRuntimeErrorType.ProviderBizError,
|
||||
message: 'Provider request failed',
|
||||
}),
|
||||
{ status: 471 },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(modelsService.getModels('cloudflare')).rejects.toMatchObject({
|
||||
message: 'Cloudflare authentication error',
|
||||
type: AgentRuntimeErrorType.ProviderBizError,
|
||||
});
|
||||
});
|
||||
|
||||
+51
-4
@@ -20,8 +20,50 @@ const isChatMessageError = (error: unknown): error is ChatMessageError => {
|
||||
return 'type' in error && 'message' in error;
|
||||
};
|
||||
|
||||
const MAX_ERROR_DEPTH = 4;
|
||||
|
||||
const extractModelFetchErrorMessage = (
|
||||
error: unknown,
|
||||
seen = new WeakSet<object>(),
|
||||
depth = 0,
|
||||
): string | undefined => {
|
||||
if (error === null || error === undefined) return;
|
||||
if (typeof error === 'string') return error || undefined;
|
||||
if (typeof error === 'number' || typeof error === 'boolean' || typeof error === 'bigint') {
|
||||
return String(error);
|
||||
}
|
||||
if (!isRecord(error) || depth >= MAX_ERROR_DEPTH) return;
|
||||
if (seen.has(error)) return;
|
||||
|
||||
seen.add(error);
|
||||
|
||||
const nestedErrorKeys = ['error', 'body', 'cause', 'response', 'detail', 'details', 'reason'];
|
||||
|
||||
for (const key of nestedErrorKeys) {
|
||||
const message = extractModelFetchErrorMessage(error[key], seen, depth + 1);
|
||||
if (message) return message;
|
||||
}
|
||||
|
||||
if (Array.isArray(error.errors)) {
|
||||
for (const item of error.errors) {
|
||||
const message = extractModelFetchErrorMessage(item, seen, depth + 1);
|
||||
if (message) return message;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
if (typeof error.message === 'string' && error.message) return error.message;
|
||||
if (typeof error.status === 'number') return `HTTP ${error.status}`;
|
||||
if (typeof error.statusCode === 'number') return `HTTP ${error.statusCode}`;
|
||||
};
|
||||
|
||||
export const normalizeModelFetchError = (error: unknown, provider: string): ChatMessageError => {
|
||||
if (isChatMessageError(error)) return error;
|
||||
if (isChatMessageError(error)) {
|
||||
return {
|
||||
...error,
|
||||
message: extractModelFetchErrorMessage(error.body) || error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (isRecord(error) && 'errorType' in error) {
|
||||
const errorType = error.errorType as ChatMessageError['type'];
|
||||
@@ -29,22 +71,27 @@ export const normalizeModelFetchError = (error: unknown, provider: string): Chat
|
||||
|
||||
return {
|
||||
body: { error: errorContent, provider },
|
||||
message: typeof error.message === 'string' ? error.message : String(errorType),
|
||||
message:
|
||||
extractModelFetchErrorMessage(errorContent) ||
|
||||
(typeof error.message === 'string' ? error.message : undefined) ||
|
||||
String(errorType),
|
||||
type: errorType,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const message = extractModelFetchErrorMessage(error) || error.message;
|
||||
|
||||
return {
|
||||
body: { error: { message: error.message, name: error.name }, provider },
|
||||
message: error.message,
|
||||
message,
|
||||
type: AgentRuntimeErrorType.ProviderBizError,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: { error, provider },
|
||||
message: String(error),
|
||||
message: error === undefined ? 'Unknown error' : String(error),
|
||||
type: AgentRuntimeErrorType.ProviderBizError,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user