refactor: update provider documentation and improve comments

This commit is contained in:
2025-05-28 13:00:25 +02:00
parent 9bf8030ad5
commit 5da20536af
10 changed files with 2658 additions and 824 deletions

View File

@@ -1,6 +1,17 @@
/**
* Abstract base class for all AI providers
* Provides common functionality and enforces a consistent interface
* Abstract Base Provider for AI Services
*
* This module provides the foundation for all AI provider implementations,
* ensuring consistency, proper error handling, and maintainable code structure.
*
* Design Principles:
* - Template Method Pattern: Define algorithm structure, let subclasses implement specifics
* - Fail-Fast: Validate inputs early and provide clear error messages
* - Separation of Concerns: Configuration, validation, execution are clearly separated
* - Type Safety: Comprehensive TypeScript support for better DX
*
* @author Jan-Marlon Leibl
* @version 1.0.0
*/
import type {
@@ -12,120 +23,333 @@ import type {
} from '../types/index.js';
import { AIProviderError, AIErrorType } from '../types/index.js';
// ============================================================================
// ABSTRACT BASE PROVIDER CLASS
// ============================================================================
/**
* Abstract base class that all AI providers must extend
* Abstract base class that all AI providers must extend.
*
* This class implements the Template Method pattern, providing a consistent
* interface and workflow while allowing providers to implement their specific
* logic in protected abstract methods.
*
* Lifecycle:
* 1. Construction: validateConfig() called automatically
* 2. Initialization: initialize() must be called before use
* 3. Usage: complete() and stream() methods available
* 4. Error Handling: All errors are normalized to AIProviderError
*
* @example
* ```typescript
* class MyProvider extends BaseAIProvider {
* protected async doInitialize(): Promise<void> {
* // Initialize your client/connection here
* }
*
* protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
* // Implement completion logic here
* }
*
* // ... other required methods
* }
* ```
*/
export abstract class BaseAIProvider {
protected config: AIProviderConfig;
protected initialized: boolean = false;
// ========================================================================
// INSTANCE PROPERTIES
// ========================================================================
constructor(config: AIProviderConfig) {
this.config = this.validateConfig(config);
}
/** Validated configuration object for this provider instance */
protected readonly config: AIProviderConfig;
/** Internal flag tracking initialization state */
private initialized: boolean = false;
// ========================================================================
// CONSTRUCTOR & CONFIGURATION
// ========================================================================
/**
* Validates the provider configuration
* @param config - Configuration object to validate
* @returns Validated configuration
* @throws AIProviderError if configuration is invalid
* Constructs a new provider instance with validated configuration.
*
* @param config - Provider configuration object
* @throws {AIProviderError} If configuration is invalid
*
* @example
* ```typescript
* const provider = new MyProvider({
* apiKey: 'your-api-key',
* timeout: 30000,
* maxRetries: 3
* });
* ```
*/
protected validateConfig(config: AIProviderConfig): AIProviderConfig {
if (!config.apiKey || typeof config.apiKey !== 'string') {
throw new AIProviderError(
'API key is required and must be a string',
AIErrorType.INVALID_REQUEST
);
}
// Set default values
const validatedConfig = {
...config,
timeout: config.timeout ?? 30000,
maxRetries: config.maxRetries ?? 3
};
return validatedConfig;
constructor(config: AIProviderConfig) {
this.config = this.validateAndNormalizeConfig(config);
}
// ========================================================================
// PUBLIC API METHODS
// ========================================================================
/**
* Initialize the provider (setup connections, validate credentials, etc.)
* Must be called before using the provider
* Initializes the provider for use.
*
* This method must be called before any completion requests. It handles
* provider-specific setup such as client initialization and connection testing.
*
* @returns Promise that resolves when initialization is complete
* @throws {AIProviderError} If initialization fails
*
* @example
* ```typescript
* await provider.initialize();
* // Provider is now ready for use
* ```
*/
public async initialize(): Promise<void> {
try {
await this.doInitialize();
this.initialized = true;
} catch (error) {
throw this.handleError(error as Error);
// Normalize any error to our standard format
throw this.normalizeError(error as Error);
}
}
/**
* Check if the provider is initialized
* Checks if the provider has been initialized.
*
* @returns true if the provider is ready for use, false otherwise
*/
public isInitialized(): boolean {
return this.initialized;
}
/**
* Generate a completion based on the provided parameters
* @param params - Parameters for the completion request
* @returns Promise resolving to completion response
* Generates a text completion based on the provided parameters.
*
* This method handles validation, error normalization, and delegates to
* the provider-specific implementation via the Template Method pattern.
*
* @param params - Completion parameters including messages, model options, etc.
* @returns Promise resolving to the completion response
* @throws {AIProviderError} If the request fails or parameters are invalid
*
* @example
* ```typescript
* const response = await provider.complete({
* messages: [{ role: 'user', content: 'Hello!' }],
* maxTokens: 100,
* temperature: 0.7
* });
* console.log(response.content);
* ```
*/
public async complete(params: CompletionParams): Promise<CompletionResponse> {
// Ensure provider is ready for use
this.ensureInitialized();
// Validate input parameters early
this.validateCompletionParams(params);
try {
// Delegate to provider-specific implementation
return await this.doComplete(params);
} catch (error) {
throw this.handleError(error as Error);
// Normalize error to our standard format
throw this.normalizeError(error as Error);
}
}
/**
* Generate a streaming completion
* @param params - Parameters for the completion request
* @returns AsyncIterable of completion chunks
* Generates a streaming text completion.
*
* This method returns an async iterable that yields chunks of the response
* as they become available, enabling real-time UI updates.
*
* @param params - Completion parameters
* @returns AsyncIterable yielding completion chunks
* @throws {AIProviderError} If the request fails or parameters are invalid
*
* @example
* ```typescript
* for await (const chunk of provider.stream(params)) {
* if (!chunk.isComplete) {
* process.stdout.write(chunk.content);
* } else {
* console.log('\nDone! Usage:', chunk.usage);
* }
* }
* ```
*/
public async *stream(params: CompletionParams): AsyncIterable<CompletionChunk> {
// Ensure provider is ready for use
this.ensureInitialized();
// Validate input parameters early
this.validateCompletionParams(params);
try {
// Delegate to provider-specific implementation
yield* this.doStream(params);
} catch (error) {
throw this.handleError(error as Error);
// Normalize error to our standard format
throw this.normalizeError(error as Error);
}
}
/**
* Get information about this provider
* @returns Provider information and capabilities
* Returns information about this provider and its capabilities.
*
* This method provides metadata about the provider including supported
* models, context length, streaming support, and special capabilities.
*
* @returns Provider information object
*/
public abstract getInfo(): ProviderInfo;
// ========================================================================
// ABSTRACT METHODS (TEMPLATE METHOD PATTERN)
// ========================================================================
/**
* Provider-specific initialization logic
* Override this method in concrete implementations
* Provider-specific initialization logic.
*
* Subclasses should implement this method to handle their specific
* initialization requirements such as:
* - Creating API clients
* - Testing connections
* - Validating credentials
* - Setting up authentication
*
* @protected
* @returns Promise that resolves when initialization is complete
* @throws {Error} If initialization fails (will be normalized to AIProviderError)
*/
protected abstract doInitialize(): Promise<void>;
/**
* Provider-specific completion logic
* Override this method in concrete implementations
* Provider-specific completion implementation.
*
* Subclasses should implement this method to handle text completion
* using their specific API. The base class handles validation and
* error normalization.
*
* @protected
* @param params - Validated completion parameters
* @returns Promise resolving to completion response
* @throws {Error} If completion fails (will be normalized to AIProviderError)
*/
protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse>;
/**
* Provider-specific streaming logic
* Override this method in concrete implementations
* Provider-specific streaming implementation.
*
* Subclasses should implement this method to handle streaming completions
* using their specific API. The base class handles validation and
* error normalization.
*
* @protected
* @param params - Validated completion parameters
* @returns AsyncIterable yielding completion chunks
* @throws {Error} If streaming fails (will be normalized to AIProviderError)
*/
protected abstract doStream(params: CompletionParams): AsyncIterable<CompletionChunk>;
// ========================================================================
// PROTECTED UTILITY METHODS
// ========================================================================
/**
* Ensures the provider is initialized before use
* @throws AIProviderError if not initialized
* Validates and normalizes provider configuration.
*
* This method ensures all required fields are present and sets sensible
* defaults for optional fields. It follows the fail-fast principle.
*
* @protected
* @param config - Raw configuration object
* @returns Validated and normalized configuration
* @throws {AIProviderError} If configuration is invalid
*/
protected validateAndNormalizeConfig(config: AIProviderConfig): AIProviderConfig {
// Validate required fields
if (!config) {
throw new AIProviderError(
'Configuration object is required',
AIErrorType.INVALID_REQUEST
);
}
if (!config.apiKey || typeof config.apiKey !== 'string' || config.apiKey.trim() === '') {
throw new AIProviderError(
'API key is required and must be a non-empty string',
AIErrorType.INVALID_REQUEST
);
}
// Apply defaults and return normalized config
return {
...config,
timeout: this.validateTimeout(config.timeout),
maxRetries: this.validateMaxRetries(config.maxRetries)
};
}
/**
* Validates timeout configuration value.
*
* @private
* @param timeout - Timeout value to validate
* @returns Validated timeout value with default if needed
*/
private validateTimeout(timeout?: number): number {
const defaultTimeout = 30000; // 30 seconds
if (timeout === undefined) {
return defaultTimeout;
}
if (typeof timeout !== 'number' || timeout < 1000 || timeout > 300000) {
throw new AIProviderError(
'Timeout must be a number between 1000ms (1s) and 300000ms (5min)',
AIErrorType.INVALID_REQUEST
);
}
return timeout;
}
/**
* Validates max retries configuration value.
*
* @private
* @param maxRetries - Max retries value to validate
* @returns Validated max retries value with default if needed
*/
private validateMaxRetries(maxRetries?: number): number {
const defaultMaxRetries = 3;
if (maxRetries === undefined) {
return defaultMaxRetries;
}
if (typeof maxRetries !== 'number' || maxRetries < 0 || maxRetries > 10) {
throw new AIProviderError(
'Max retries must be a number between 0 and 10',
AIErrorType.INVALID_REQUEST
);
}
return maxRetries;
}
/**
* Ensures the provider has been initialized before use.
*
* @protected
* @throws {AIProviderError} If the provider is not initialized
*/
protected ensureInitialized(): void {
if (!this.initialized) {
@@ -137,127 +361,250 @@ export abstract class BaseAIProvider {
}
/**
* Validates completion parameters
* Validates completion parameters comprehensively.
*
* This method performs thorough validation of all completion parameters
* to ensure they meet the requirements and constraints.
*
* @protected
* @param params - Parameters to validate
* @throws AIProviderError if parameters are invalid
* @throws {AIProviderError} If any parameter is invalid
*/
protected validateCompletionParams(params: CompletionParams): void {
if (!params.messages || !Array.isArray(params.messages) || params.messages.length === 0) {
if (!params || typeof params !== 'object') {
throw new AIProviderError(
'Messages array is required and must not be empty',
'Completion parameters object is required',
AIErrorType.INVALID_REQUEST
);
}
for (const message of params.messages) {
if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) {
// Validate messages array
this.validateMessages(params.messages);
// Validate optional parameters
this.validateTemperature(params.temperature);
this.validateTopP(params.topP);
this.validateMaxTokens(params.maxTokens);
this.validateStopSequences(params.stopSequences);
}
/**
* Validates the messages array parameter.
*
* @private
* @param messages - Messages array to validate
* @throws {AIProviderError} If messages are invalid
*/
private validateMessages(messages: any): void {
if (!Array.isArray(messages) || messages.length === 0) {
throw new AIProviderError(
'Messages must be a non-empty array',
AIErrorType.INVALID_REQUEST
);
}
const validRoles = ['system', 'user', 'assistant'] as const;
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
if (!message || typeof message !== 'object') {
throw new AIProviderError(
'Each message must have a valid role (system, user, or assistant)',
`Message at index ${i} must be an object`,
AIErrorType.INVALID_REQUEST
);
}
if (!message.content || typeof message.content !== 'string') {
if (!validRoles.includes(message.role)) {
throw new AIProviderError(
'Each message must have non-empty string content',
`Message at index ${i} has invalid role '${message.role}'. Must be: ${validRoles.join(', ')}`,
AIErrorType.INVALID_REQUEST
);
}
}
if (params.temperature !== undefined && (params.temperature < 0 || params.temperature > 1)) {
throw new AIProviderError(
'Temperature must be between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
if (params.topP !== undefined && (params.topP < 0 || params.topP > 1)) {
throw new AIProviderError(
'Top-p must be between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
if (params.maxTokens !== undefined && params.maxTokens < 1) {
throw new AIProviderError(
'Max tokens must be a positive integer',
AIErrorType.INVALID_REQUEST
);
if (typeof message.content !== 'string' || message.content.trim() === '') {
throw new AIProviderError(
`Message at index ${i} must have non-empty string content`,
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Handles and transforms errors into AIProviderError instances
* @param error - The original error
* @returns AIProviderError with appropriate type and context
* Validates temperature parameter.
*
* @private
* @param temperature - Temperature value to validate
* @throws {AIProviderError} If temperature is invalid
*/
protected handleError(error: Error): AIProviderError {
private validateTemperature(temperature?: number): void {
if (temperature !== undefined) {
if (typeof temperature !== 'number' || temperature < 0 || temperature > 1) {
throw new AIProviderError(
'Temperature must be a number between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates top-p parameter.
*
* @private
* @param topP - Top-p value to validate
* @throws {AIProviderError} If top-p is invalid
*/
private validateTopP(topP?: number): void {
if (topP !== undefined) {
if (typeof topP !== 'number' || topP <= 0 || topP > 1) {
throw new AIProviderError(
'Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates max tokens parameter.
*
* @private
* @param maxTokens - Max tokens value to validate
* @throws {AIProviderError} If max tokens is invalid
*/
private validateMaxTokens(maxTokens?: number): void {
if (maxTokens !== undefined) {
if (typeof maxTokens !== 'number' || !Number.isInteger(maxTokens) || maxTokens < 1) {
throw new AIProviderError(
'Max tokens must be a positive integer',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates stop sequences parameter.
*
* @private
* @param stopSequences - Stop sequences to validate
* @throws {AIProviderError} If stop sequences are invalid
*/
private validateStopSequences(stopSequences?: string[]): void {
if (stopSequences !== undefined) {
if (!Array.isArray(stopSequences)) {
throw new AIProviderError(
'Stop sequences must be an array of strings',
AIErrorType.INVALID_REQUEST
);
}
for (let i = 0; i < stopSequences.length; i++) {
if (typeof stopSequences[i] !== 'string') {
throw new AIProviderError(
`Stop sequence at index ${i} must be a string`,
AIErrorType.INVALID_REQUEST
);
}
}
}
}
/**
* Normalizes any error into a standardized AIProviderError.
*
* This method provides consistent error handling across all providers,
* mapping various error types and status codes to appropriate categories.
*
* @protected
* @param error - The original error to normalize
* @returns Normalized AIProviderError with appropriate type and context
*/
protected normalizeError(error: Error): AIProviderError {
// If already normalized, return as-is
if (error instanceof AIProviderError) {
return error;
}
// Handle common HTTP status codes
if ('status' in error) {
const status = (error as any).status;
// Extract status code if available
const status = (error as any).status || (error as any).statusCode;
const message = error.message || 'Unknown error occurred';
// Map HTTP status codes to error types
if (status) {
switch (status) {
case 400:
return new AIProviderError(
`Bad request: ${message}`,
AIErrorType.INVALID_REQUEST,
status,
error
);
case 401:
case 403:
return new AIProviderError(
'Authentication failed. Please check your API key.',
'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
AIErrorType.AUTHENTICATION,
status,
error
);
case 429:
return new AIProviderError(
'Rate limit exceeded. Please try again later.',
AIErrorType.RATE_LIMIT,
status,
error
);
case 404:
return new AIProviderError(
'Model or endpoint not found.',
'The specified model or endpoint was not found. Please check the model name and availability.',
AIErrorType.MODEL_NOT_FOUND,
status,
error
);
case 400:
case 429:
return new AIProviderError(
'Invalid request parameters.',
AIErrorType.INVALID_REQUEST,
'Rate limit exceeded. Please reduce your request frequency and try again later.',
AIErrorType.RATE_LIMIT,
status,
error
);
case 500:
case 502:
case 503:
case 504:
return new AIProviderError(
'Service temporarily unavailable. Please try again in a few moments.',
AIErrorType.NETWORK,
status,
error
);
}
}
// Handle timeout errors
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
// Map common error patterns
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
return new AIProviderError(
'Request timed out. Please try again.',
'Request timed out. The operation took longer than expected.',
AIErrorType.TIMEOUT,
undefined,
error
);
}
// Handle network errors
if (error.message.includes('network') || error.message.includes('ENOTFOUND')) {
if (message.includes('network') || message.includes('ENOTFOUND') || message.includes('ECONNREFUSED')) {
return new AIProviderError(
'Network error occurred. Please check your connection.',
'Network error occurred. Please check your internet connection and try again.',
AIErrorType.NETWORK,
undefined,
error
);
}
// Default to unknown error
// Default to unknown error type
return new AIProviderError(
`Unknown error: ${error.message}`,
`Provider error: ${message}`,
AIErrorType.UNKNOWN,
undefined,
status,
error
);
}