Form Template Method / Pull Up Method from the Refactoring Guru catalog.
The three SDK-based providers (Claude, OpenAI, Gemini) each had their own
near-identical implementations of:
- validateConnection: try a tiny request, catch errors, map a handful
of patterns, warn on the rest
- validateModelName: iterate regex patterns, warn on no match
- handle{X}Error: switch on HTTP status, map to AIProviderError with
brand-flavored messages
These collapse into three protected hooks on BaseAIProvider:
- getModelNamePatterns(): RegExp[]
- sendValidationProbe(): Promise<void>
- mapProviderError(error): AIProviderError | null
- providerErrorMessages(): Partial<Record<number, string>>
The base owns the wrapping logic (warning, status -> error-type map,
common message patterns). Providers only contribute their unique parts.
Side effects:
- normalizeError now consults mapProviderError before generic mapping,
so provider-specific patterns work via the existing try/catch in
BaseAIProvider.complete/stream. The per-provider try/catch wrappers
around doComplete/doStream are removed.
- The unknown-error message becomes `${providerName} error: ${msg}`
instead of the hardcoded `Provider error:` / `Claude API error:` etc.
- OpenWebUI's HTTP-based validateConnection is preserved (override),
since its non-SDK shape doesn't fit the SDK probe pattern.
715 lines
23 KiB
TypeScript
715 lines
23 KiB
TypeScript
/**
|
|
* 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 {
|
|
AIProviderConfig,
|
|
CompletionParams,
|
|
CompletionResponse,
|
|
CompletionChunk,
|
|
ProviderInfo,
|
|
ResponseType
|
|
} from '../types/index.js';
|
|
import { AIProviderError, AIErrorType, generateResponseTypePrompt, parseAndValidateResponseType } from '../types/index.js';
|
|
import {
|
|
CONFIG_BOUNDS,
|
|
DEFAULT_MAX_RETRIES,
|
|
DEFAULT_TIMEOUT_MS
|
|
} from '../constants.js';
|
|
|
|
// ============================================================================
|
|
// ABSTRACT BASE PROVIDER CLASS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 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 {
|
|
// ========================================================================
|
|
// INSTANCE PROPERTIES
|
|
// ========================================================================
|
|
|
|
/** Validated configuration object for this provider instance */
|
|
protected readonly config: AIProviderConfig;
|
|
|
|
/** Internal flag tracking initialization state */
|
|
private initialized: boolean = false;
|
|
|
|
// ========================================================================
|
|
// CONSTRUCTOR & CONFIGURATION
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 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
|
|
* });
|
|
* ```
|
|
*/
|
|
constructor(config: AIProviderConfig) {
|
|
this.config = this.validateAndNormalizeConfig(config);
|
|
}
|
|
|
|
// ========================================================================
|
|
// PUBLIC API METHODS
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 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) {
|
|
// Normalize any error to our standard format
|
|
throw this.normalizeError(error as Error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the provider has been initialized.
|
|
*
|
|
* @returns true if the provider is ready for use, false otherwise
|
|
*/
|
|
public isInitialized(): boolean {
|
|
return this.initialized;
|
|
}
|
|
|
|
/**
|
|
* 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<T>(params: CompletionParams<T>): Promise<CompletionResponse<T>>;
|
|
public async complete(params: CompletionParams): Promise<CompletionResponse<string>>;
|
|
public async complete<T = any>(params: CompletionParams<T>): Promise<CompletionResponse<T | string>> {
|
|
// Ensure provider is ready for use
|
|
this.ensureInitialized();
|
|
|
|
// Validate input parameters early
|
|
this.validateCompletionParams(params);
|
|
|
|
try {
|
|
// Process response type instructions if specified
|
|
const processedParams = this.processResponseType(params);
|
|
|
|
// Delegate to provider-specific implementation
|
|
const response = await this.doComplete(processedParams);
|
|
|
|
// If a responseType is defined, parse and validate the response
|
|
if (params.responseType) {
|
|
const parsedData = parseAndValidateResponseType<T>(response.content, params.responseType);
|
|
return {
|
|
...response,
|
|
content: parsedData,
|
|
rawContent: response.content,
|
|
};
|
|
}
|
|
|
|
// Otherwise, return the raw string content
|
|
return {
|
|
...response,
|
|
content: response.content,
|
|
};
|
|
} catch (error) {
|
|
// Normalize error to our standard format
|
|
throw this.normalizeError(error as Error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
|
// Ensure provider is ready for use
|
|
this.ensureInitialized();
|
|
|
|
// Validate input parameters early
|
|
this.validateCompletionParams(params);
|
|
|
|
try {
|
|
// Process response type instructions if specified
|
|
const processedParams = this.processResponseType(params);
|
|
|
|
// Delegate to provider-specific implementation
|
|
yield* this.doStream(processedParams);
|
|
} catch (error) {
|
|
// Normalize error to our standard format
|
|
throw this.normalizeError(error as Error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* 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 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<string>>;
|
|
|
|
/**
|
|
* 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<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk>;
|
|
|
|
// ========================================================================
|
|
// PROTECTED HOOKS (subclass overrides; safe defaults provided)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* Regex patterns that valid model names should match.
|
|
* Empty array (the default) skips validation entirely.
|
|
*/
|
|
protected getModelNamePatterns(): RegExp[] {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Send a minimal request to verify the API key and connectivity.
|
|
* The default does nothing; SDK-based providers override this and the
|
|
* base `validateConnection` handles error mapping.
|
|
*/
|
|
protected async sendValidationProbe(): Promise<void> {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Map a provider-specific error to a normalized AIProviderError, or
|
|
* return null to let the base error mapping handle it generically.
|
|
* Override to recognize provider-specific message patterns (e.g. "API
|
|
* key", "quota", "model not found").
|
|
*/
|
|
protected mapProviderError(_error: any): AIProviderError | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Per-HTTP-status message overrides used by `normalizeError` when no
|
|
* provider-specific mapping applies. Lets providers add brand-flavored
|
|
* hints (console URLs, etc.) without re-implementing the status switch.
|
|
*/
|
|
protected providerErrorMessages(): Partial<Record<number, string>> {
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Human-readable provider name used in warning messages.
|
|
* Default derives from getInfo(); override if construction order makes
|
|
* getInfo() unsafe to call here.
|
|
*/
|
|
protected get providerName(): string {
|
|
try {
|
|
return this.getInfo().name;
|
|
} catch {
|
|
return 'Provider';
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// PROTECTED UTILITY METHODS
|
|
// ========================================================================
|
|
|
|
/**
|
|
* Validates a model name against the patterns returned by
|
|
* `getModelNamePatterns()`. Throws for invalid input, warns for
|
|
* non-matching names (since model lists change frequently).
|
|
*/
|
|
protected validateModelName(modelName: string): void {
|
|
if (!modelName || typeof modelName !== 'string') {
|
|
throw new AIProviderError(
|
|
'Model name must be a non-empty string',
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
const patterns = this.getModelNamePatterns();
|
|
if (patterns.length === 0) return;
|
|
|
|
const matches = patterns.some(pattern => pattern.test(modelName));
|
|
if (!matches) {
|
|
console.warn(
|
|
`Model name '${modelName}' doesn't match expected ${this.providerName} naming patterns. This may cause API errors.`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wraps `sendValidationProbe()` with consistent error handling: throws
|
|
* AIProviderError for provider-recognized failures, warns and continues
|
|
* for transient ones.
|
|
*/
|
|
protected async validateConnection(): Promise<void> {
|
|
try {
|
|
await this.sendValidationProbe();
|
|
} catch (error: any) {
|
|
const mapped = this.mapProviderError(error);
|
|
if (mapped) {
|
|
throw mapped;
|
|
}
|
|
console.warn(`${this.providerName} connection validation warning:`, error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
);
|
|
}
|
|
|
|
this.validateNumberInRange('timeout', config.timeout, {
|
|
...CONFIG_BOUNDS.timeoutMs,
|
|
label: `${CONFIG_BOUNDS.timeoutMs.min}ms and ${CONFIG_BOUNDS.timeoutMs.max}ms`
|
|
});
|
|
this.validateNumberInRange('maxRetries', config.maxRetries, {
|
|
...CONFIG_BOUNDS.maxRetries,
|
|
integer: true
|
|
});
|
|
|
|
return {
|
|
...config,
|
|
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validates an optional numeric parameter against a range.
|
|
* Skips validation when value is undefined so callers can apply defaults afterward.
|
|
*/
|
|
private validateNumberInRange(
|
|
name: string,
|
|
value: number | undefined,
|
|
bounds: {
|
|
min?: number;
|
|
max?: number;
|
|
exclusiveMin?: boolean;
|
|
integer?: boolean;
|
|
label?: string;
|
|
}
|
|
): void {
|
|
if (value === undefined) return;
|
|
|
|
const { min, max, exclusiveMin, integer, label } = bounds;
|
|
const range = label ?? this.describeRange(min, max, exclusiveMin);
|
|
|
|
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
throw new AIProviderError(
|
|
`${name} must be a number between ${range}`,
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
if (integer && !Number.isInteger(value)) {
|
|
throw new AIProviderError(
|
|
`${name} must be an integer`,
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
if (min !== undefined && (exclusiveMin ? value <= min : value < min)) {
|
|
throw new AIProviderError(
|
|
`${name} must be ${exclusiveMin ? 'greater than' : 'at least'} ${min}`,
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
if (max !== undefined && value > max) {
|
|
throw new AIProviderError(
|
|
`${name} must be at most ${max}`,
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
}
|
|
|
|
private describeRange(min?: number, max?: number, exclusiveMin?: boolean): string {
|
|
if (min !== undefined && max !== undefined) {
|
|
return `${exclusiveMin ? '>' : '>='}${min} and <=${max}`;
|
|
}
|
|
if (min !== undefined) return `${exclusiveMin ? '>' : '>='}${min}`;
|
|
if (max !== undefined) return `<=${max}`;
|
|
return 'a valid number';
|
|
}
|
|
|
|
/**
|
|
* Ensures the provider has been initialized before use.
|
|
*
|
|
* @protected
|
|
* @throws {AIProviderError} If the provider is 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 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 any parameter is invalid
|
|
*/
|
|
protected validateCompletionParams<T = any>(params: CompletionParams<T>): void {
|
|
if (!params || typeof params !== 'object') {
|
|
throw new AIProviderError(
|
|
'Completion parameters object is required',
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
this.validateMessages(params.messages);
|
|
|
|
this.validateNumberInRange('temperature', params.temperature, CONFIG_BOUNDS.temperature);
|
|
this.validateNumberInRange('topP', params.topP, CONFIG_BOUNDS.topP);
|
|
this.validateNumberInRange('maxTokens', params.maxTokens, { ...CONFIG_BOUNDS.maxTokens, integer: true });
|
|
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(
|
|
`Message at index ${i} must be an object`,
|
|
AIErrorType.INVALID_REQUEST
|
|
);
|
|
}
|
|
|
|
if (!validRoles.includes(message.role)) {
|
|
throw new AIProviderError(
|
|
`Message at index ${i} has invalid role '${message.role}'. Must be: ${validRoles.join(', ')}`,
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes completion parameters to include response type instructions.
|
|
*
|
|
* This method automatically adds system prompt instructions when a response
|
|
* type is specified, ensuring the AI understands the expected output format.
|
|
*
|
|
* @protected
|
|
* @param params - Original completion parameters
|
|
* @returns Processed parameters with response type instructions
|
|
*/
|
|
protected processResponseType<T>(params: CompletionParams<T>): CompletionParams<T> {
|
|
if (!params.responseType) {
|
|
return params;
|
|
}
|
|
|
|
// Create a copy of the parameters to avoid mutation
|
|
const processedParams = { ...params };
|
|
|
|
// Generate the response type instruction
|
|
const responseTypePrompt = generateResponseTypePrompt(params.responseType);
|
|
|
|
// Find existing system messages or create new ones
|
|
const systemMessages = params.messages.filter(msg => msg.role === 'system');
|
|
const nonSystemMessages = params.messages.filter(msg => msg.role !== 'system');
|
|
|
|
// Combine existing system messages with response type instruction
|
|
const combinedSystemContent = systemMessages.length > 0
|
|
? systemMessages.map(msg => msg.content).join('\n\n') + '\n\n' + responseTypePrompt
|
|
: responseTypePrompt;
|
|
|
|
// Create new messages array with combined system message
|
|
processedParams.messages = [
|
|
{ role: 'system', content: combinedSystemContent },
|
|
...nonSystemMessages
|
|
];
|
|
|
|
return processedParams;
|
|
}
|
|
|
|
/**
|
|
* 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 (error instanceof AIProviderError) {
|
|
return error;
|
|
}
|
|
|
|
const providerError = this.mapProviderError(error);
|
|
if (providerError) {
|
|
return providerError;
|
|
}
|
|
|
|
const status = (error as any).status || (error as any).statusCode;
|
|
const message = error.message || 'Unknown error occurred';
|
|
const overrides = this.providerErrorMessages();
|
|
|
|
const statusTypeMap: Record<number, AIErrorType> = {
|
|
400: AIErrorType.INVALID_REQUEST,
|
|
401: AIErrorType.AUTHENTICATION,
|
|
403: AIErrorType.AUTHENTICATION,
|
|
404: AIErrorType.MODEL_NOT_FOUND,
|
|
429: AIErrorType.RATE_LIMIT,
|
|
500: AIErrorType.NETWORK,
|
|
502: AIErrorType.NETWORK,
|
|
503: AIErrorType.NETWORK,
|
|
504: AIErrorType.NETWORK
|
|
};
|
|
|
|
const defaultMessages: Record<number, string> = {
|
|
400: `Bad request: ${message}`,
|
|
401: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
|
403: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
|
404: 'The specified model or endpoint was not found. Please check the model name and availability.',
|
|
429: 'Rate limit exceeded. Please reduce your request frequency and try again later.',
|
|
500: 'Service temporarily unavailable. Please try again in a few moments.',
|
|
502: 'Service temporarily unavailable. Please try again in a few moments.',
|
|
503: 'Service temporarily unavailable. Please try again in a few moments.',
|
|
504: 'Service temporarily unavailable. Please try again in a few moments.'
|
|
};
|
|
|
|
if (status && statusTypeMap[status]) {
|
|
return new AIProviderError(
|
|
overrides[status] ?? defaultMessages[status]!,
|
|
statusTypeMap[status]!,
|
|
status,
|
|
error
|
|
);
|
|
}
|
|
|
|
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
|
|
return new AIProviderError(
|
|
'Request timed out. The operation took longer than expected.',
|
|
AIErrorType.TIMEOUT,
|
|
undefined,
|
|
error
|
|
);
|
|
}
|
|
|
|
if (message.includes('network') || message.includes('ENOTFOUND') || message.includes('ECONNREFUSED')) {
|
|
return new AIProviderError(
|
|
'Network error occurred. Please check your internet connection and try again.',
|
|
AIErrorType.NETWORK,
|
|
undefined,
|
|
error
|
|
);
|
|
}
|
|
|
|
return new AIProviderError(
|
|
`${this.providerName} error: ${message}`,
|
|
AIErrorType.UNKNOWN,
|
|
status,
|
|
error
|
|
);
|
|
}
|
|
}
|