feat: add initial implementation of Simple AI Provider package
This commit is contained in:
264
src/providers/base.ts
Normal file
264
src/providers/base.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Abstract base class for all AI providers
|
||||
* Provides common functionality and enforces a consistent interface
|
||||
*/
|
||||
|
||||
import type {
|
||||
AIProviderConfig,
|
||||
CompletionParams,
|
||||
CompletionResponse,
|
||||
CompletionChunk,
|
||||
ProviderInfo
|
||||
} from '../types/index.js';
|
||||
import { AIProviderError, AIErrorType } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Abstract base class that all AI providers must extend
|
||||
*/
|
||||
export abstract class BaseAIProvider {
|
||||
protected config: AIProviderConfig;
|
||||
protected initialized: boolean = false;
|
||||
|
||||
constructor(config: AIProviderConfig) {
|
||||
this.config = this.validateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provider configuration
|
||||
* @param config - Configuration object to validate
|
||||
* @returns Validated configuration
|
||||
* @throws AIProviderError if configuration is invalid
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the provider (setup connections, validate credentials, etc.)
|
||||
* Must be called before using the provider
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.doInitialize();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
throw this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provider is initialized
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public async complete(params: CompletionParams): Promise<CompletionResponse> {
|
||||
this.ensureInitialized();
|
||||
this.validateCompletionParams(params);
|
||||
|
||||
try {
|
||||
return await this.doComplete(params);
|
||||
} catch (error) {
|
||||
throw this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a streaming completion
|
||||
* @param params - Parameters for the completion request
|
||||
* @returns AsyncIterable of completion chunks
|
||||
*/
|
||||
public async *stream(params: CompletionParams): AsyncIterable<CompletionChunk> {
|
||||
this.ensureInitialized();
|
||||
this.validateCompletionParams(params);
|
||||
|
||||
try {
|
||||
yield* this.doStream(params);
|
||||
} catch (error) {
|
||||
throw this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about this provider
|
||||
* @returns Provider information and capabilities
|
||||
*/
|
||||
public abstract getInfo(): ProviderInfo;
|
||||
|
||||
/**
|
||||
* Provider-specific initialization logic
|
||||
* Override this method in concrete implementations
|
||||
*/
|
||||
protected abstract doInitialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Provider-specific completion logic
|
||||
* Override this method in concrete implementations
|
||||
*/
|
||||
protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse>;
|
||||
|
||||
/**
|
||||
* Provider-specific streaming logic
|
||||
* Override this method in concrete implementations
|
||||
*/
|
||||
protected abstract doStream(params: CompletionParams): AsyncIterable<CompletionChunk>;
|
||||
|
||||
/**
|
||||
* Ensures the provider is initialized before use
|
||||
* @throws AIProviderError if not initialized
|
||||
*/
|
||||
protected ensureInitialized(): void {
|
||||
if (!this.initialized) {
|
||||
throw new AIProviderError(
|
||||
'Provider must be initialized before use. Call initialize() first.',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates completion parameters
|
||||
* @param params - Parameters to validate
|
||||
* @throws AIProviderError if parameters are invalid
|
||||
*/
|
||||
protected validateCompletionParams(params: CompletionParams): void {
|
||||
if (!params.messages || !Array.isArray(params.messages) || params.messages.length === 0) {
|
||||
throw new AIProviderError(
|
||||
'Messages array is required and must not be empty',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
for (const message of params.messages) {
|
||||
if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) {
|
||||
throw new AIProviderError(
|
||||
'Each message must have a valid role (system, user, or assistant)',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (!message.content || typeof message.content !== 'string') {
|
||||
throw new AIProviderError(
|
||||
'Each message must have non-empty string content',
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles and transforms errors into AIProviderError instances
|
||||
* @param error - The original error
|
||||
* @returns AIProviderError with appropriate type and context
|
||||
*/
|
||||
protected handleError(error: Error): AIProviderError {
|
||||
if (error instanceof AIProviderError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Handle common HTTP status codes
|
||||
if ('status' in error) {
|
||||
const status = (error as any).status;
|
||||
switch (status) {
|
||||
case 401:
|
||||
case 403:
|
||||
return new AIProviderError(
|
||||
'Authentication failed. Please check your API key.',
|
||||
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.',
|
||||
AIErrorType.MODEL_NOT_FOUND,
|
||||
status,
|
||||
error
|
||||
);
|
||||
case 400:
|
||||
return new AIProviderError(
|
||||
'Invalid request parameters.',
|
||||
AIErrorType.INVALID_REQUEST,
|
||||
status,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle timeout errors
|
||||
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
|
||||
return new AIProviderError(
|
||||
'Request timed out. Please try again.',
|
||||
AIErrorType.TIMEOUT,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Handle network errors
|
||||
if (error.message.includes('network') || error.message.includes('ENOTFOUND')) {
|
||||
return new AIProviderError(
|
||||
'Network error occurred. Please check your connection.',
|
||||
AIErrorType.NETWORK,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Default to unknown error
|
||||
return new AIProviderError(
|
||||
`Unknown error: ${error.message}`,
|
||||
AIErrorType.UNKNOWN,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user