UNPKG

@llumiverse/drivers

Version:

LLM driver implementations. Currently supported are: openai, huggingface, bedrock, replicate.

433 lines (364 loc) 17.4 kB
import { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, BadRequestError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, UnprocessableEntityError, } from '@anthropic-ai/sdk/error'; import { LlumiverseError } from '@llumiverse/core'; import { beforeEach, describe, expect, it } from 'vitest'; import { VertexAIDriver } from '../index.js'; import { ClaudeModelDefinition } from './claude.js'; describe('ClaudeModelDefinition Error Handling', () => { let modelDef: ClaudeModelDefinition; let driver: VertexAIDriver; beforeEach(() => { modelDef = new ClaudeModelDefinition('claude-haiku-4-5'); driver = { provider: 'vertexai', logger: { warn: () => { }, info: () => { }, error: () => { } }, } as any; }); describe('formatLlumiverseError', () => { it('should handle BadRequestError with status code in message', () => { const headers = new Headers(); headers.set('request-id', 'req_test_123'); const anthropicError = new BadRequestError( 400, { type: 'error', error: { type: 'invalid_request_error', message: 'temperature: range: 0..1' } }, '400 {"type":"error","error":{"type":"invalid_request_error","message":"temperature: range: 0..1"},"request_id":"req_test_123"}', headers ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error).toBeInstanceOf(LlumiverseError); expect(error.code).toBe(400); expect(error.message).toContain('[400]'); expect(error.message).toContain('temperature: range: 0..1'); expect(error.message).toContain('invalid_request_error'); expect(error.message).toContain('req_test_123'); expect(error.name).toBe('BadRequestError'); expect(error.retryable).toBe(false); }); it('should handle RateLimitError as retryable', () => { const anthropicError = new RateLimitError( 429, { type: 'error', error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } }, 'Rate limit exceeded', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(429); expect(error.retryable).toBe(true); expect(error.name).toBe('RateLimitError'); }); it('should handle InternalServerError as retryable', () => { const anthropicError = new InternalServerError( 500, { type: 'error', error: { type: 'internal_error', message: 'Internal server error' } }, 'Internal server error', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(500); expect(error.retryable).toBe(true); expect(error.name).toBe('InternalServerError'); }); it('should handle AuthenticationError as not retryable', () => { const anthropicError = new AuthenticationError( 401, { type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } }, 'Invalid API key', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(401); expect(error.retryable).toBe(false); expect(error.name).toBe('AuthenticationError'); }); it('should handle PermissionDeniedError as not retryable', () => { const anthropicError = new PermissionDeniedError( 403, { type: 'error', error: { type: 'permission_error', message: 'Insufficient permissions' } }, 'Insufficient permissions', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(403); expect(error.retryable).toBe(false); expect(error.name).toBe('PermissionDeniedError'); }); it('should handle NotFoundError as not retryable', () => { const anthropicError = new NotFoundError( 404, { type: 'error', error: { type: 'not_found_error', message: 'Model not found' } }, 'Model not found', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(404); expect(error.retryable).toBe(false); expect(error.name).toBe('NotFoundError'); }); it('should handle ConflictError as retryable (lock timeout)', () => { const anthropicError = new ConflictError( 409, { type: 'error', error: { type: 'conflict_error', message: 'Resource conflict' } }, 'Resource conflict', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(409); expect(error.retryable).toBe(true); expect(error.name).toBe('ConflictError'); }); it('should handle UnprocessableEntityError as not retryable', () => { const anthropicError = new UnprocessableEntityError( 422, { type: 'error', error: { type: 'validation_error', message: 'Validation failed' } }, 'Validation failed', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBe(422); expect(error.retryable).toBe(false); expect(error.name).toBe('UnprocessableEntityError'); }); it('should handle APIConnectionTimeoutError as retryable', () => { const anthropicError = new APIConnectionTimeoutError({ message: 'Request timed out' }); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBeUndefined(); expect(error.retryable).toBe(true); expect(error.name).toBe('APIConnectionTimeoutError'); }); it('should handle APIConnectionError as retryable', () => { const anthropicError = new APIConnectionError({ message: 'Connection failed' }); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.code).toBeUndefined(); expect(error.retryable).toBe(true); expect(error.name).toBe('APIConnectionError'); }); it('should extract error type from nested error object', () => { const anthropicError = new BadRequestError( 400, { type: 'error', error: { type: 'invalid_request_error', message: 'Missing required field' } }, 'Missing required field', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.message).toContain('invalid_request_error'); expect(error.message).toContain('Missing required field'); }); it('should include request ID in message when available', () => { const headers = new Headers(); headers.set('request-id', 'req_vrtx_test123'); const anthropicError = new BadRequestError( 400, { type: 'error', error: { type: 'invalid_request_error', message: 'Bad request' } }, 'Bad request', headers ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.message).toContain('req_vrtx_test123'); }); it('should throw for non-Anthropic errors', () => { const regularError = new Error('Regular error'); expect(() => { modelDef.formatLlumiverseError(driver, regularError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); }).toThrow('Regular error'); }); it('should preserve original error for debugging', () => { const anthropicError = new BadRequestError( 400, { type: 'error', error: { type: 'invalid_request_error', message: 'Test error' } }, 'Test error', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error.originalError).toBe(anthropicError); }); }); describe('isClaudeErrorRetryable', () => { it('should classify retryable error types correctly', () => { const retryableErrors = [ new RateLimitError(429, {}, 'Rate limit', new Headers()), new InternalServerError(500, {}, 'Server error', new Headers()), new APIConnectionTimeoutError({ message: 'Timeout' }), new ConflictError(409, {}, 'Conflict / lock timeout', new Headers()), ]; for (const error of retryableErrors) { const result = (modelDef as any).isClaudeErrorRetryable(error, error.status, undefined); expect(result, `${error.constructor.name} should be retryable`).toBe(true); } }); it('should classify non-retryable error types correctly', () => { const nonRetryableErrors = [ new BadRequestError(400, {}, 'Bad request', new Headers()), new AuthenticationError(401, {}, 'Auth error', new Headers()), new PermissionDeniedError(403, {}, 'Permission denied', new Headers()), new NotFoundError(404, {}, 'Not found', new Headers()), new UnprocessableEntityError(422, {}, 'Validation error', new Headers()), ]; for (const error of nonRetryableErrors) { const result = (modelDef as any).isClaudeErrorRetryable(error, error.status, undefined); expect(result, `${error.constructor.name} should not be retryable`).toBe(false); } }); it('should use HTTP status codes when available', () => { const apiError = new APIError(429, {}, 'Too many requests', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError, 429, undefined)).toBe(true); const apiError2 = new APIError(408, {}, 'Request timeout', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError2, 408, undefined)).toBe(true); const apiError3 = new APIError(529, {}, 'Overloaded', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError3, 529, undefined)).toBe(true); const apiError4 = new APIError(503, {}, 'Service unavailable', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError4, 503, undefined)).toBe(true); }); it('should classify 4xx as non-retryable', () => { const apiError = new APIError(400, {}, 'Bad request', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError, 400, undefined)).toBe(false); const apiError2 = new APIError(403, {}, 'Forbidden', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError2, 403, undefined)).toBe(false); }); it('should classify 5xx as retryable', () => { const apiError = new APIError(500, {}, 'Internal error', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError, 500, undefined)).toBe(true); const apiError2 = new APIError(502, {}, 'Bad gateway', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError2, 502, undefined)).toBe(true); }); it('should classify invalid_request_error as non-retryable', () => { const apiError = new APIError(400, {}, 'Invalid request', new Headers()); expect((modelDef as any).isClaudeErrorRetryable(apiError, 400, 'invalid_request_error')).toBe(false); }); it('should classify APIConnectionError (non-timeout) as retryable', () => { const connectionError = new APIConnectionError({ message: 'Network failure' }); expect((modelDef as any).isClaudeErrorRetryable(connectionError, undefined, undefined)).toBe(true); }); it('should return undefined for unknown errors', () => { const apiError = new APIError(undefined, {}, 'Unknown error', undefined as any); expect((modelDef as any).isClaudeErrorRetryable(apiError, undefined, undefined)).toBeUndefined(); }); }); describe('VertexAIDriver error routing', () => { it('should route to Claude-specific error handler', () => { const headers = new Headers(); headers.set('request-id', 'req_test_routing'); const anthropicError = new RateLimitError( 429, { type: 'error', error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } }, 'Rate limit exceeded', headers ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model: 'claude-haiku-4-5', operation: 'execute', }); expect(error).toBeInstanceOf(LlumiverseError); expect(error.code).toBe(429); expect(error.retryable).toBe(true); expect(error.message).toContain('rate_limit_error'); expect(error.message).toContain('req_test_routing'); expect(error.name).toBe('RateLimitError'); }); it('should work with different Claude model versions', () => { const models = ['claude-haiku-4-5', 'claude-3-7-sonnet-20250219', 'claude-opus-4-5']; models.forEach((model) => { const modelDef = new ClaudeModelDefinition(model); const anthropicError = new BadRequestError( 400, { type: 'error', error: { type: 'invalid_request_error', message: 'Invalid parameter' } }, 'Invalid parameter', new Headers() ); const error = modelDef.formatLlumiverseError(driver, anthropicError, { provider: 'vertexai', model, operation: 'execute', }); expect(error.code).toBe(400); expect(error.retryable).toBe(false); expect(error.name).toBe('BadRequestError'); }); }); }); });