kimi-code/packages/kosong/test/anthropic-errors.test.ts
liruifengv df7a9cab60
fix: prevent streaming tool argument CPU spikes (#29)
* fix: prevent streaming tool argument CPU spikes

* test: update Anthropic streaming mock
2026-05-25 19:49:11 +08:00

356 lines
11 KiB
TypeScript

import {
APIConnectionError,
APIContextOverflowError,
APIStatusError,
APITimeoutError,
ChatProviderError,
} from '#/errors';
import { convertAnthropicError, AnthropicChatProvider } from '#/providers/anthropic';
import {
APIConnectionError as AnthropicConnectionError,
APIConnectionTimeoutError as AnthropicTimeoutError,
APIError as AnthropicAPIError,
AnthropicError,
AuthenticationError as AnthropicAuthenticationError,
RateLimitError as AnthropicRateLimitError,
} from '@anthropic-ai/sdk';
import { describe, it, expect, vi } from 'vitest';
describe('convertAnthropicError', () => {
it('APIConnectionTimeoutError -> APITimeoutError (not misclassified as connection)', () => {
const err = new AnthropicTimeoutError({ message: 'timed out' });
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APITimeoutError);
// Must NOT be a plain APIConnectionError
expect(result.constructor).toBe(APITimeoutError);
});
it('APIConnectionError -> APIConnectionError', () => {
const err = new AnthropicConnectionError({ message: 'connection refused' });
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APIConnectionError);
});
it('APIError with status -> APIStatusError', () => {
const err = AnthropicAPIError.generate(
502,
{ type: 'error', error: { type: 'api_error', message: 'bad gateway' } },
'bad gateway',
new Headers(),
);
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APIStatusError);
expect((result as APIStatusError).statusCode).toBe(502);
});
it('context overflow APIError -> APIContextOverflowError', () => {
const err = AnthropicAPIError.generate(
422,
{
type: 'error',
error: {
type: 'invalid_request_error',
message: 'prompt is too long: 210000 tokens exceeds the maximum',
},
},
'prompt is too long: 210000 tokens exceeds the maximum',
new Headers(),
);
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APIContextOverflowError);
expect((result as APIContextOverflowError).statusCode).toBe(422);
});
it('AuthenticationError -> APIStatusError with 401', () => {
const err = new AnthropicAuthenticationError(
401,
{ type: 'error', error: { type: 'authentication_error', message: 'invalid key' } },
'invalid key',
new Headers(),
);
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APIStatusError);
expect((result as APIStatusError).statusCode).toBe(401);
});
it('RateLimitError -> APIStatusError with 429', () => {
const err = new AnthropicRateLimitError(
429,
{ type: 'error', error: { type: 'rate_limit_error', message: 'rate limited' } },
'rate limited',
new Headers(),
);
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(APIStatusError);
expect((result as APIStatusError).statusCode).toBe(429);
});
it('generic AnthropicError -> ChatProviderError', () => {
const err = new AnthropicError('something went wrong');
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(ChatProviderError);
expect(result.message).toContain('something went wrong');
});
it('plain Error -> ChatProviderError', () => {
const err = new Error('unexpected');
const result = convertAnthropicError(err);
expect(result).toBeInstanceOf(ChatProviderError);
expect(result.message).toContain('unexpected');
});
it('non-Error value -> ChatProviderError', () => {
const result = convertAnthropicError('string error');
expect(result).toBeInstanceOf(ChatProviderError);
expect(result.message).toContain('string error');
});
});
describe('non-stream error propagation', () => {
function createNonStreamProvider(): AnthropicChatProvider {
return new AnthropicChatProvider({
model: 'k25',
apiKey: 'test-key',
defaultMaxTokens: 1024,
stream: false,
});
}
it('APIConnectionTimeoutError during generate is converted', async () => {
const provider = createNonStreamProvider();
const sdkError = new AnthropicTimeoutError({ message: 'stream timed out' });
(provider as any)._client.messages.create = vi.fn().mockRejectedValue(sdkError);
await expect(
provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
),
).rejects.toThrow(APITimeoutError);
});
it('APIConnectionError during generate is converted', async () => {
const provider = createNonStreamProvider();
const sdkError = new AnthropicConnectionError({ message: 'connection reset' });
(provider as any)._client.messages.create = vi.fn().mockRejectedValue(sdkError);
await expect(
provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
),
).rejects.toThrow(APIConnectionError);
});
it('APIError with status during generate is converted to APIStatusError', async () => {
const provider = createNonStreamProvider();
const sdkError = AnthropicAPIError.generate(
500,
{ type: 'error', error: { type: 'api_error', message: 'internal error' } },
'internal error',
new Headers(),
);
(provider as any)._client.messages.create = vi.fn().mockRejectedValue(sdkError);
await expect(
provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
),
).rejects.toThrow(APIStatusError);
});
it('RateLimitError during generate is converted to APIStatusError(429)', async () => {
const provider = createNonStreamProvider();
const sdkError = new AnthropicRateLimitError(
429,
{ type: 'error', error: { type: 'rate_limit_error', message: 'too many requests' } },
'too many requests',
new Headers(),
);
(provider as any)._client.messages.create = vi.fn().mockRejectedValue(sdkError);
try {
await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
expect.unreachable('Should have thrown');
} catch (error) {
expect(error).toBeInstanceOf(APIStatusError);
expect((error as APIStatusError).statusCode).toBe(429);
}
});
it('AuthenticationError during generate is converted to APIStatusError(401)', async () => {
const provider = createNonStreamProvider();
const sdkError = new AnthropicAuthenticationError(
401,
{ type: 'error', error: { type: 'authentication_error', message: 'invalid' } },
'invalid',
new Headers(),
);
(provider as any)._client.messages.create = vi.fn().mockRejectedValue(sdkError);
try {
await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
expect.unreachable('Should have thrown');
} catch (error) {
expect(error).toBeInstanceOf(APIStatusError);
expect((error as APIStatusError).statusCode).toBe(401);
}
});
});
describe('stream error propagation', () => {
function createStreamProvider(): AnthropicChatProvider {
return new AnthropicChatProvider({
model: 'k25',
apiKey: 'test-key',
defaultMaxTokens: 1024,
stream: true,
});
}
function makeErrorStream(error: Error) {
return {
async *[Symbol.asyncIterator]() {
yield {
type: 'message_start',
message: { id: 'msg_err', usage: { input_tokens: 0 } },
};
throw error;
},
};
}
it('APIConnectionTimeoutError during stream iteration is converted', async () => {
const provider = createStreamProvider();
const sdkError = new AnthropicTimeoutError({ message: 'stream timed out' });
(provider as any)._client.messages.create = vi
.fn()
.mockResolvedValue(makeErrorStream(sdkError)) as never;
const result = await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
const parts: unknown[] = [];
await expect(
(async () => {
for await (const part of result) {
parts.push(part);
}
})(),
).rejects.toThrow(APITimeoutError);
});
it('APIConnectionError during stream iteration is converted', async () => {
const provider = createStreamProvider();
const sdkError = new AnthropicConnectionError({ message: 'connection reset' });
(provider as any)._client.messages.create = vi
.fn()
.mockResolvedValue(makeErrorStream(sdkError)) as never;
const result = await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
await expect(
(async () => {
for await (const _ of result) {
void _;
}
})(),
).rejects.toThrow(APIConnectionError);
});
it('APIError with status during stream iteration is converted to APIStatusError', async () => {
const provider = createStreamProvider();
const sdkError = AnthropicAPIError.generate(
500,
{ type: 'error', error: { type: 'api_error', message: 'internal error' } },
'internal error',
new Headers(),
);
(provider as any)._client.messages.create = vi
.fn()
.mockResolvedValue(makeErrorStream(sdkError)) as never;
const result = await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
await expect(
(async () => {
for await (const _ of result) {
void _;
}
})(),
).rejects.toThrow(APIStatusError);
});
it('RateLimitError during stream iteration is converted to APIStatusError(429)', async () => {
const provider = createStreamProvider();
const sdkError = new AnthropicRateLimitError(
429,
{ type: 'error', error: { type: 'rate_limit_error', message: 'too many requests' } },
'too many requests',
new Headers(),
);
(provider as any)._client.messages.create = vi
.fn()
.mockResolvedValue(makeErrorStream(sdkError)) as never;
const result = await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
try {
for await (const _ of result) {
void _;
}
expect.unreachable('Should have thrown');
} catch (error) {
expect(error).toBeInstanceOf(APIStatusError);
expect((error as APIStatusError).statusCode).toBe(429);
}
});
it('AuthenticationError during stream iteration is converted to APIStatusError(401)', async () => {
const provider = createStreamProvider();
const sdkError = new AnthropicAuthenticationError(
401,
{ type: 'error', error: { type: 'authentication_error', message: 'invalid' } },
'invalid',
new Headers(),
);
(provider as any)._client.messages.create = vi
.fn()
.mockResolvedValue(makeErrorStream(sdkError)) as never;
const result = await provider.generate(
'',
[],
[{ role: 'user', content: [{ type: 'text', text: 'Hi' }], toolCalls: [] }],
);
try {
for await (const _ of result) {
void _;
}
expect.unreachable('Should have thrown');
} catch (error) {
expect(error).toBeInstanceOf(APIStatusError);
expect((error as APIStatusError).statusCode).toBe(401);
}
});
});