feat: add initial implementation of Simple AI Provider package

This commit is contained in:
2025-05-28 11:54:24 +02:00
commit 42902445fb
15 changed files with 1616 additions and 0 deletions

264
src/providers/base.ts Normal file
View 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
);
}
}