UNPKG

@llumiverse/core

Version:

Provide an universal API to LLMs. Support for existing LLMs can be added by writing a driver.

262 lines (211 loc) 11.2 kB
import { AIModel, Completion, CompletionChunkObject, DriverOptions, EmbeddingsOptions, EmbeddingsResult, ExecutionOptions, LlumiverseErrorContext, ModelSearchPayload, LlumiverseError, } from '@llumiverse/common'; import { beforeEach, describe, expect, it } from 'vitest'; import { AbstractDriver } from './Driver.js'; // Simple test driver implementation class TestDriver extends AbstractDriver<DriverOptions, string> { provider = 'test-provider'; async requestTextCompletion(_prompt: string, _options: ExecutionOptions): Promise<Completion> { throw new Error('Not implemented'); } async requestTextCompletionStream(_prompt: string, _options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> { throw new Error('Not implemented'); } async listModels(_params?: ModelSearchPayload): Promise<AIModel[]> { return []; } async validateConnection(): Promise<boolean> { return true; } async generateEmbeddings(_options: EmbeddingsOptions): Promise<EmbeddingsResult> { throw new Error('Not implemented'); } } describe('AbstractDriver Error Formatting', () => { let driver: TestDriver; const mockContext: LlumiverseErrorContext = { provider: 'test-provider', model: 'test-model', operation: 'execute', }; beforeEach(() => { driver = new TestDriver({}); }); describe('isRetryableError', () => { describe('HTTP status codes', () => { it('should mark 429 as retryable (rate limit)', () => { expect(driver['isRetryableError'](429, 'Rate limit exceeded')).toBe(true); }); it('should mark 408 as retryable (timeout)', () => { expect(driver['isRetryableError'](408, 'Request timeout')).toBe(true); }); it('should mark 529 as retryable (overloaded)', () => { expect(driver['isRetryableError'](529, 'Service overloaded')).toBe(true); }); it('should mark 5xx as retryable (server errors)', () => { expect(driver['isRetryableError'](500, 'Internal server error')).toBe(true); expect(driver['isRetryableError'](502, 'Bad gateway')).toBe(true); expect(driver['isRetryableError'](503, 'Service unavailable')).toBe(true); expect(driver['isRetryableError'](504, 'Gateway timeout')).toBe(true); }); it('should mark 4xx as not retryable (except 429, 408)', () => { expect(driver['isRetryableError'](400, 'Bad request')).toBe(false); expect(driver['isRetryableError'](401, 'Unauthorized')).toBe(false); expect(driver['isRetryableError'](403, 'Forbidden')).toBe(false); expect(driver['isRetryableError'](404, 'Not found')).toBe(false); }); it('should mark 2xx and 3xx as not retryable', () => { expect(driver['isRetryableError'](200, 'OK')).toBe(false); expect(driver['isRetryableError'](301, 'Moved permanently')).toBe(false); }); }); describe('message-based detection', () => { it('should detect rate limit in message', () => { expect(driver['isRetryableError'](undefined, 'Rate limit exceeded')).toBe(true); expect(driver['isRetryableError'](undefined, 'You have hit the rate limit')).toBe(true); expect(driver['isRetryableError'](undefined, 'RATE_LIMIT_EXCEEDED')).toBe(true); }); it('should detect timeout in message', () => { expect(driver['isRetryableError'](undefined, 'Request timeout')).toBe(true); expect(driver['isRetryableError'](undefined, 'Connection timed out')).toBe(true); expect(driver['isRetryableError'](undefined, 'TIMEOUT_ERROR')).toBe(true); }); it('should detect retry in message', () => { expect(driver['isRetryableError'](undefined, 'Please retry later')).toBe(true); expect(driver['isRetryableError'](undefined, 'Retry the request')).toBe(true); }); it('should detect overload in message', () => { expect(driver['isRetryableError'](undefined, 'Service overloaded')).toBe(true); expect(driver['isRetryableError'](undefined, 'Server is overload')).toBe(true); expect(driver['isRetryableError'](undefined, 'System overloaded')).toBe(true); }); it('should detect resource exhausted in message', () => { expect(driver['isRetryableError'](undefined, 'Resource exhausted')).toBe(true); expect(driver['isRetryableError'](undefined, 'Resources exhausted')).toBe(true); }); it('should detect throttle in message', () => { expect(driver['isRetryableError'](undefined, 'Request throttled')).toBe(true); expect(driver['isRetryableError'](undefined, 'Throttling exception')).toBe(true); }); it('should detect status codes in message', () => { expect(driver['isRetryableError'](undefined, 'Error 429: Too many requests')).toBe(true); expect(driver['isRetryableError'](undefined, 'HTTP 529 error')).toBe(true); }); it('should mark unknown messages as undefined (let consumer decide)', () => { expect(driver['isRetryableError'](undefined, 'Invalid API key')).toBeUndefined(); expect(driver['isRetryableError'](undefined, 'Bad request')).toBeUndefined(); expect(driver['isRetryableError'](undefined, 'Model not found')).toBeUndefined(); }); }); }); describe('formatLlumiverseError', () => { it('should format error with status code', () => { const originalError = new Error('Rate limit exceeded'); (originalError as any).status = 429; const formatted = driver['formatLlumiverseError'](originalError, mockContext); expect(formatted).toBeInstanceOf(LlumiverseError); expect(formatted.code).toBe(429); expect(formatted.retryable).toBe(true); expect(formatted.message).toContain('[test-provider]'); expect(formatted.message).toContain('Rate limit exceeded'); expect(formatted.context).toEqual(mockContext); expect(formatted.originalError).toBe(originalError); }); it('should extract status from statusCode property', () => { const originalError = new Error('Server error'); (originalError as any).statusCode = 500; const formatted = driver['formatLlumiverseError'](originalError, mockContext); expect(formatted.code).toBe(500); expect(formatted.retryable).toBe(true); }); it('should extract status from code property', () => { const originalError = new Error('Timeout'); (originalError as any).code = 408; const formatted = driver['formatLlumiverseError'](originalError, mockContext); expect(formatted.code).toBe(408); expect(formatted.retryable).toBe(true); }); it('should use undefined when no status code found', () => { const originalError = new Error('Generic error'); const formatted = driver['formatLlumiverseError'](originalError, mockContext); expect(formatted.code).toBeUndefined(); expect(formatted.retryable).toBeUndefined(); // Unknown retryability }); it('should handle non-Error objects', () => { const originalError = 'String error message'; const formatted = driver['formatLlumiverseError'](originalError, mockContext); expect(formatted.message).toContain('String error message'); expect(formatted.originalError).toBe(originalError); }); it('should preserve provider in message', () => { const error = new Error('Test error'); const formatted = driver['formatLlumiverseError'](error, mockContext); expect(formatted.message).toMatch(/^\[test-provider\]/); }); it('should determine retryability based on status and message', () => { // Retryable by status const retryableError = new Error('Error'); (retryableError as any).status = 429; const formatted1 = driver['formatLlumiverseError'](retryableError, mockContext); expect(formatted1.retryable).toBe(true); // Not retryable by status const nonRetryableError = new Error('Error'); (nonRetryableError as any).status = 400; const formatted2 = driver['formatLlumiverseError'](nonRetryableError, mockContext); expect(formatted2.retryable).toBe(false); // Retryable by message const messageRetryable = new Error('Rate limit exceeded'); const formatted3 = driver['formatLlumiverseError'](messageRetryable, mockContext); expect(formatted3.retryable).toBe(true); }); }); describe('driver override capability', () => { class CustomDriver extends TestDriver { public formatLlumiverseError( error: unknown, context: LlumiverseErrorContext ): LlumiverseError { // Custom logic: check for specific error type if ((error as any).type === 'custom_retryable') { return new LlumiverseError( `[${this.provider}] Custom retryable error`, true, context, error, undefined, 'CUSTOM_ERROR' ); } // Fall back to default return super.formatLlumiverseError(error, context); } } it('should allow drivers to override error formatting', () => { const customDriver = new CustomDriver({}); const customError = { type: 'custom_retryable', message: 'Custom error' }; const formatted = customDriver['formatLlumiverseError'](customError, mockContext); expect(formatted.name).toBe('CUSTOM_ERROR'); expect(formatted.code).toBeUndefined(); expect(formatted.retryable).toBe(true); expect(formatted.message).toContain('Custom retryable error'); }); it('should fall back to default for non-custom errors', () => { const customDriver = new CustomDriver({}); const regularError = new Error('Regular error'); (regularError as any).status = 500; const formatted = customDriver['formatLlumiverseError'](regularError, mockContext); expect(formatted.code).toBe(500); expect(formatted.retryable).toBe(true); }); }); });