refactor: pull validateConnection / validateModelName / error mapping into base
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.
This commit is contained in:
@@ -288,10 +288,104 @@ export abstract class BaseAIProvider {
|
|||||||
*/
|
*/
|
||||||
protected abstract doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk>;
|
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
|
// 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.
|
* Validates and normalizes provider configuration.
|
||||||
*
|
*
|
||||||
@@ -547,65 +641,52 @@ export abstract class BaseAIProvider {
|
|||||||
* @returns Normalized AIProviderError with appropriate type and context
|
* @returns Normalized AIProviderError with appropriate type and context
|
||||||
*/
|
*/
|
||||||
protected normalizeError(error: Error): AIProviderError {
|
protected normalizeError(error: Error): AIProviderError {
|
||||||
// If already normalized, return as-is
|
|
||||||
if (error instanceof AIProviderError) {
|
if (error instanceof AIProviderError) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract status code if available
|
const providerError = this.mapProviderError(error);
|
||||||
const status = (error as any).status || (error as any).statusCode;
|
if (providerError) {
|
||||||
const message = error.message || 'Unknown error occurred';
|
return providerError;
|
||||||
|
}
|
||||||
// Map HTTP status codes to error types
|
|
||||||
if (status) {
|
const status = (error as any).status || (error as any).statusCode;
|
||||||
switch (status) {
|
const message = error.message || 'Unknown error occurred';
|
||||||
case 400:
|
const overrides = this.providerErrorMessages();
|
||||||
return new AIProviderError(
|
|
||||||
`Bad request: ${message}`,
|
const statusTypeMap: Record<number, AIErrorType> = {
|
||||||
AIErrorType.INVALID_REQUEST,
|
400: AIErrorType.INVALID_REQUEST,
|
||||||
status,
|
401: AIErrorType.AUTHENTICATION,
|
||||||
error
|
403: AIErrorType.AUTHENTICATION,
|
||||||
);
|
404: AIErrorType.MODEL_NOT_FOUND,
|
||||||
|
429: AIErrorType.RATE_LIMIT,
|
||||||
case 401:
|
500: AIErrorType.NETWORK,
|
||||||
case 403:
|
502: AIErrorType.NETWORK,
|
||||||
return new AIProviderError(
|
503: AIErrorType.NETWORK,
|
||||||
'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
504: AIErrorType.NETWORK
|
||||||
AIErrorType.AUTHENTICATION,
|
};
|
||||||
status,
|
|
||||||
error
|
const defaultMessages: Record<number, string> = {
|
||||||
);
|
400: `Bad request: ${message}`,
|
||||||
|
401: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
||||||
case 404:
|
403: 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
|
||||||
return new AIProviderError(
|
404: 'The specified model or endpoint was not found. Please check the model name and availability.',
|
||||||
'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.',
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
500: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||||
status,
|
502: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||||
error
|
503: 'Service temporarily unavailable. Please try again in a few moments.',
|
||||||
);
|
504: 'Service temporarily unavailable. Please try again in a few moments.'
|
||||||
|
};
|
||||||
case 429:
|
|
||||||
return new AIProviderError(
|
if (status && statusTypeMap[status]) {
|
||||||
'Rate limit exceeded. Please reduce your request frequency and try again later.',
|
return new AIProviderError(
|
||||||
AIErrorType.RATE_LIMIT,
|
overrides[status] ?? defaultMessages[status]!,
|
||||||
status,
|
statusTypeMap[status]!,
|
||||||
error
|
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map common error patterns
|
|
||||||
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
|
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
|
||||||
return new AIProviderError(
|
return new AIProviderError(
|
||||||
'Request timed out. The operation took longer than expected.',
|
'Request timed out. The operation took longer than expected.',
|
||||||
@@ -624,9 +705,8 @@ export abstract class BaseAIProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to unknown error type
|
|
||||||
return new AIProviderError(
|
return new AIProviderError(
|
||||||
`Provider error: ${message}`,
|
`${this.providerName} error: ${message}`,
|
||||||
AIErrorType.UNKNOWN,
|
AIErrorType.UNKNOWN,
|
||||||
status,
|
status,
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
*/
|
*/
|
||||||
protected async doInitialize(): Promise<void> {
|
protected async doInitialize(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Create Anthropic client with optimized configuration
|
|
||||||
this.client = new Anthropic({
|
this.client = new Anthropic({
|
||||||
apiKey: this.config.apiKey,
|
apiKey: this.config.apiKey,
|
||||||
baseURL: this.config.baseUrl,
|
baseURL: this.config.baseUrl,
|
||||||
@@ -192,15 +191,12 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
maxRetries: this.config.maxRetries,
|
maxRetries: this.config.maxRetries,
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
'anthropic-version': this.version,
|
'anthropic-version': this.version,
|
||||||
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15', // Enable extended context
|
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate connection and permissions
|
|
||||||
await this.validateConnection();
|
await this.validateConnection();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up on failure
|
|
||||||
this.client = null;
|
this.client = null;
|
||||||
|
|
||||||
throw new AIProviderError(
|
throw new AIProviderError(
|
||||||
@@ -231,22 +227,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { system, messages } = this.processMessages(params.messages);
|
||||||
// Process messages for Claude's format requirements
|
const requestParams = this.buildRequestParams(params, system, messages, false);
|
||||||
const { system, messages } = this.processMessages(params.messages);
|
const response = await this.client.messages.create(requestParams);
|
||||||
|
return this.formatCompletionResponse(response);
|
||||||
// Build optimized request parameters
|
|
||||||
const requestParams = this.buildRequestParams(params, system, messages, false);
|
|
||||||
|
|
||||||
// Make API request
|
|
||||||
const response = await this.client.messages.create(requestParams);
|
|
||||||
|
|
||||||
// Format and return response
|
|
||||||
return this.formatCompletionResponse(response);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleAnthropicError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,22 +252,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { system, messages } = this.processMessages(params.messages);
|
||||||
// Process messages for Claude's format requirements
|
const requestParams = this.buildRequestParams(params, system, messages, true);
|
||||||
const { system, messages } = this.processMessages(params.messages);
|
const stream = await this.client.messages.create(requestParams);
|
||||||
|
yield* this.processStreamChunks(stream);
|
||||||
// Build streaming request parameters
|
|
||||||
const requestParams = this.buildRequestParams(params, system, messages, true);
|
|
||||||
|
|
||||||
// Create streaming request
|
|
||||||
const stream = await this.client.messages.create(requestParams);
|
|
||||||
|
|
||||||
// Process stream chunks
|
|
||||||
yield* this.processStreamChunks(stream);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleAnthropicError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -323,75 +295,62 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
// PRIVATE UTILITY METHODS
|
// PRIVATE UTILITY METHODS
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
protected override getModelNamePatterns(): RegExp[] {
|
||||||
* Validates the connection by making a minimal test request.
|
return [
|
||||||
*
|
/^claude-3(?:-5)?-(?:opus|sonnet|haiku)-\d{8}$/,
|
||||||
* @private
|
/^claude-instant-[0-9.]+$/,
|
||||||
* @throws {AIProviderError} If connection validation fails
|
/^claude-[0-9.]+$/
|
||||||
*/
|
];
|
||||||
private async validateConnection(): Promise<void> {
|
}
|
||||||
|
|
||||||
|
protected override async sendValidationProbe(): Promise<void> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new Error('Client not initialized');
|
throw new Error('Client not initialized');
|
||||||
}
|
}
|
||||||
|
await this.client.messages.create({
|
||||||
try {
|
model: this.defaultModel,
|
||||||
await this.client.messages.create({
|
max_tokens: 1,
|
||||||
model: this.defaultModel,
|
messages: [{ role: 'user', content: VALIDATION_PROMPT }]
|
||||||
max_tokens: 1,
|
});
|
||||||
messages: [{ role: 'user', content: VALIDATION_PROMPT }]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle specific validation errors
|
|
||||||
if (error.status === 401 || error.status === 403) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'Invalid Anthropic API key. Please verify your API key from https://console.anthropic.com/',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.status === 404 && error.message?.includes('model')) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access.`,
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other errors during validation, log but don't fail initialization
|
|
||||||
// They might be temporary network issues
|
|
||||||
console.warn('Claude connection validation warning:', error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||||
* Validates model name format for Claude models.
|
return {
|
||||||
*
|
401: 'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
|
||||||
* @private
|
403: 'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||||
* @param modelName - Model name to validate
|
404: 'Model not found. The specified Claude model may not be available to your account.',
|
||||||
* @throws {AIProviderError} If model name format is invalid
|
429: 'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.',
|
||||||
*/
|
500: 'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||||
private validateModelName(modelName: string): void {
|
502: 'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||||
if (!modelName || typeof modelName !== 'string') {
|
503: 'Anthropic service temporarily unavailable. Please try again in a few moments.'
|
||||||
throw new AIProviderError(
|
};
|
||||||
'Model name must be a non-empty string',
|
}
|
||||||
AIErrorType.INVALID_REQUEST
|
|
||||||
);
|
protected override mapProviderError(error: any): AIProviderError | null {
|
||||||
|
if (!error) return null;
|
||||||
|
const message: string = error.message || '';
|
||||||
|
const status = error.status || error.statusCode;
|
||||||
|
|
||||||
|
if (status === 400) {
|
||||||
|
if (message.includes('max_tokens')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Max tokens value is invalid. Must be between 1 and model limit.',
|
||||||
|
AIErrorType.INVALID_REQUEST,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (message.includes('model')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Invalid model specified. Please check model availability.',
|
||||||
|
AIErrorType.MODEL_NOT_FOUND,
|
||||||
|
status,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude model names follow specific patterns
|
return null;
|
||||||
const validPatterns = [
|
|
||||||
/^claude-3(?:-5)?-(?:opus|sonnet|haiku)-\d{8}$/, // e.g., claude-3-5-sonnet-20241022
|
|
||||||
/^claude-instant-[0-9.]+$/, // e.g., claude-instant-1.2
|
|
||||||
/^claude-[0-9.]+$/ // e.g., claude-2.1
|
|
||||||
];
|
|
||||||
|
|
||||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
console.warn(`Model name '${modelName}' doesn't match expected Claude naming patterns. This may cause API errors.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -569,118 +528,4 @@ export class ClaudeProvider extends BaseAIProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles and transforms Anthropic-specific errors.
|
|
||||||
*
|
|
||||||
* This method maps Anthropic's error responses to our standardized
|
|
||||||
* error format, providing helpful context and suggestions.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param error - Original error from Anthropic API
|
|
||||||
* @returns Normalized AIProviderError
|
|
||||||
*/
|
|
||||||
private handleAnthropicError(error: any): AIProviderError {
|
|
||||||
if (error instanceof AIProviderError) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = error.message || 'Unknown Anthropic API error';
|
|
||||||
const status = error.status || error.statusCode;
|
|
||||||
|
|
||||||
// Map Anthropic-specific error codes
|
|
||||||
switch (status) {
|
|
||||||
case 400:
|
|
||||||
if (message.includes('max_tokens')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Max tokens value is invalid. Must be between 1 and model limit.',
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.includes('model')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Invalid model specified. Please check model availability.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new AIProviderError(
|
|
||||||
`Invalid request: ${message}`,
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 401:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 403:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Access forbidden. Your API key may not have permission for this model or feature.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 404:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Model not found. The specified Claude model may not be available to your account.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 429:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 500:
|
|
||||||
case 502:
|
|
||||||
case 503:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Handle timeout and network errors
|
|
||||||
if (message.includes('timeout') || error.code === 'ETIMEDOUT') {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Request timed out. Claude may be experiencing high load.',
|
|
||||||
AIErrorType.TIMEOUT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('network') || error.code === 'ECONNREFUSED') {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Network error connecting to Anthropic servers.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AIProviderError(
|
|
||||||
`Claude API error: ${message}`,
|
|
||||||
AIErrorType.UNKNOWN,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -134,20 +134,16 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
||||||
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
const model = params.model || this.defaultModel;
|
||||||
const model = params.model || this.defaultModel;
|
|
||||||
|
|
||||||
const response = await this.client.models.generateContent({
|
const response = await this.client.models.generateContent({
|
||||||
model,
|
model,
|
||||||
contents,
|
contents,
|
||||||
config: this.buildConfig(params, systemInstruction)
|
config: this.buildConfig(params, systemInstruction)
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.formatCompletionResponse(response, model);
|
return this.formatCompletionResponse(response, model);
|
||||||
} catch (error) {
|
|
||||||
throw this.handleGeminiError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
protected async *doStream<T = any>(params: CompletionParams<T>): AsyncIterable<CompletionChunk> {
|
||||||
@@ -155,20 +151,16 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
||||||
const { systemInstruction, contents } = this.convertMessages(params.messages);
|
const model = params.model || this.defaultModel;
|
||||||
const model = params.model || this.defaultModel;
|
|
||||||
|
|
||||||
const stream = await this.client.models.generateContentStream({
|
const stream = await this.client.models.generateContentStream({
|
||||||
model,
|
model,
|
||||||
contents,
|
contents,
|
||||||
config: this.buildConfig(params, systemInstruction)
|
config: this.buildConfig(params, systemInstruction)
|
||||||
});
|
});
|
||||||
|
|
||||||
yield* this.processStreamChunks(stream);
|
yield* this.processStreamChunks(stream);
|
||||||
} catch (error) {
|
|
||||||
throw this.handleGeminiError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -205,55 +197,8 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
// PRIVATE UTILITY METHODS
|
// PRIVATE UTILITY METHODS
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
private async validateConnection(): Promise<void> {
|
protected override getModelNamePatterns(): RegExp[] {
|
||||||
if (!this.client) {
|
return [
|
||||||
throw new Error('Client not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.client.models.generateContent({
|
|
||||||
model: this.defaultModel,
|
|
||||||
contents: [{ role: 'user', parts: [{ text: VALIDATION_PROMPT }] }],
|
|
||||||
config: { maxOutputTokens: 1 }
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.message?.includes('API key')) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message?.includes('quota') || error.message?.includes('billing')) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'API quota exceeded or billing issue. Please check your Google Cloud billing and API quotas.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message?.includes('model')) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`,
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn('Gemini connection validation warning:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateModelName(modelName: string): void {
|
|
||||||
if (!modelName || typeof modelName !== 'string') {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'Model name must be a non-empty string',
|
|
||||||
AIErrorType.INVALID_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPatterns = [
|
|
||||||
/^gemini-2\.5-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
/^gemini-2\.5-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||||
/^gemini-2\.0-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
/^gemini-2\.0-(?:pro|flash)(?:-lite)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||||
/^gemini-1\.5-pro(?:-latest|-vision)?$/,
|
/^gemini-1\.5-pro(?:-latest|-vision)?$/,
|
||||||
@@ -262,12 +207,72 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
/^gemini-pro(?:-vision)?$/,
|
/^gemini-pro(?:-vision)?$/,
|
||||||
/^models\/gemini-.+$/
|
/^models\/gemini-.+$/
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
protected override async sendValidationProbe(): Promise<void> {
|
||||||
|
if (!this.client) {
|
||||||
if (!isValid) {
|
throw new Error('Client not initialized');
|
||||||
console.warn(`Model name '${modelName}' doesn't match expected Gemini naming patterns. This may cause API errors.`);
|
|
||||||
}
|
}
|
||||||
|
await this.client.models.generateContent({
|
||||||
|
model: this.defaultModel,
|
||||||
|
contents: [{ role: 'user', parts: [{ text: VALIDATION_PROMPT }] }],
|
||||||
|
config: { maxOutputTokens: 1 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||||
|
return {
|
||||||
|
401: 'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||||
|
403: 'Authentication failed with Gemini API. Please check your API key and permissions.',
|
||||||
|
404: 'Gemini API endpoint not found. Please check the model name and API version.',
|
||||||
|
429: 'Rate limit exceeded. Please reduce request frequency.',
|
||||||
|
500: 'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||||
|
502: 'Gemini service temporarily unavailable. Please try again in a few moments.',
|
||||||
|
503: 'Gemini service temporarily unavailable. Please try again in a few moments.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override mapProviderError(error: any): AIProviderError | null {
|
||||||
|
if (!error) return null;
|
||||||
|
const message: string = error.message || '';
|
||||||
|
const status = error.status || error.statusCode;
|
||||||
|
|
||||||
|
if (message.includes('API key')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
||||||
|
AIErrorType.AUTHENTICATION, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('quota') || message.includes('limit exceeded')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'API quota exceeded. Please check your Google Cloud quotas and billing.',
|
||||||
|
AIErrorType.RATE_LIMIT, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('model not found') || message.includes('invalid model')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Model not found. The specified Gemini model may not exist or be available to your API key.',
|
||||||
|
AIErrorType.MODEL_NOT_FOUND, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('safety') || message.includes('blocked')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Content blocked by safety filters. Please modify your input or adjust safety settings.',
|
||||||
|
AIErrorType.INVALID_REQUEST, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes('length') || message.includes('token')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Input too long or exceeds token limits. Please reduce input size or max tokens.',
|
||||||
|
AIErrorType.INVALID_REQUEST, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertMessages(messages: AIMessage[]): ProcessedMessages {
|
private convertMessages(messages: AIMessage[]): ProcessedMessages {
|
||||||
@@ -389,128 +394,4 @@ export class GeminiProvider extends BaseAIProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleGeminiError(error: any): AIProviderError {
|
|
||||||
if (error instanceof AIProviderError) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = error.message || 'Unknown Gemini API error';
|
|
||||||
const status = error.status || error.statusCode;
|
|
||||||
|
|
||||||
if (message.includes('API key')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('quota') || message.includes('limit exceeded')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'API quota exceeded. Please check your Google Cloud quotas and billing.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('model not found') || message.includes('invalid model')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Model not found. The specified Gemini model may not exist or be available to your API key.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('safety') || message.includes('blocked')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Content blocked by safety filters. Please modify your input or adjust safety settings.',
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('length') || message.includes('token')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Input too long or exceeds token limits. Please reduce input size or max tokens.',
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('timeout')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Request timed out. Gemini may be experiencing high load.',
|
|
||||||
AIErrorType.TIMEOUT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('network') || message.includes('connection')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Network error connecting to Gemini servers.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 400:
|
|
||||||
return new AIProviderError(
|
|
||||||
`Invalid request to Gemini API: ${message}`,
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 401:
|
|
||||||
case 403:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Authentication failed with Gemini API. Please check your API key and permissions.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 404:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Gemini API endpoint not found. Please check the model name and API version.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 429:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Rate limit exceeded. Please reduce request frequency.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 500:
|
|
||||||
case 502:
|
|
||||||
case 503:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Gemini service temporarily unavailable. Please try again in a few moments.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return new AIProviderError(
|
|
||||||
`Gemini API error: ${message}`,
|
|
||||||
AIErrorType.UNKNOWN,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,19 +255,9 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const requestParams = this.buildRequestParams(params, false);
|
||||||
// Build optimized request parameters
|
const response = await this.client.chat.completions.create(requestParams) as OpenAI.Chat.Completions.ChatCompletion;
|
||||||
const requestParams = this.buildRequestParams(params, false);
|
return this.formatCompletionResponse(response);
|
||||||
|
|
||||||
// Make API request - explicitly type as ChatCompletion for non-streaming
|
|
||||||
const response = await this.client.chat.completions.create(requestParams) as OpenAI.Chat.Completions.ChatCompletion;
|
|
||||||
|
|
||||||
// Format and return response
|
|
||||||
return this.formatCompletionResponse(response);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleOpenAIError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -289,19 +279,9 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const requestParams = this.buildRequestParams(params, true);
|
||||||
// Build streaming request parameters
|
const stream = this.client.chat.completions.create(requestParams);
|
||||||
const requestParams = this.buildRequestParams(params, true);
|
yield* this.processStreamChunks(stream);
|
||||||
|
|
||||||
// Create streaming request
|
|
||||||
const stream = this.client.chat.completions.create(requestParams);
|
|
||||||
|
|
||||||
// Process stream chunks
|
|
||||||
yield* this.processStreamChunks(stream);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw this.handleOpenAIError(error as Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -346,86 +326,82 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
// PRIVATE UTILITY METHODS
|
// PRIVATE UTILITY METHODS
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
/**
|
protected override getModelNamePatterns(): RegExp[] {
|
||||||
* Validates the connection by making a minimal test request.
|
return [
|
||||||
*
|
/^gpt-4o(?:-mini)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||||
* @private
|
/^gpt-4(?:-turbo)?(?:-\d{4}-\d{2}-\d{2})?$/,
|
||||||
* @throws {AIProviderError} If connection validation fails
|
/^gpt-3\.5-turbo(?:-\d{4})?$/,
|
||||||
*/
|
/^gpt-4-\d{4}-preview$/,
|
||||||
private async validateConnection(): Promise<void> {
|
/^text-davinci-\d{3}$/,
|
||||||
|
/^ft:.+$/
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async sendValidationProbe(): Promise<void> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
throw new Error('Client not initialized');
|
throw new Error('Client not initialized');
|
||||||
}
|
}
|
||||||
|
await this.client.chat.completions.create({
|
||||||
try {
|
model: this.defaultModel,
|
||||||
await this.client.chat.completions.create({
|
messages: [{ role: 'user', content: VALIDATION_PROMPT }],
|
||||||
model: this.defaultModel,
|
max_tokens: 1
|
||||||
messages: [{ role: 'user', content: VALIDATION_PROMPT }],
|
});
|
||||||
max_tokens: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
// Handle specific validation errors
|
|
||||||
if (error.status === 401) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'Invalid OpenAI API key. Please verify your API key from https://platform.openai.com/api-keys',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.status === 403) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
'Access forbidden. Your API key may not have permission for this model or your account may have insufficient credits.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.status === 404 && error.message?.includes('model')) {
|
|
||||||
throw new AIProviderError(
|
|
||||||
`Model '${this.defaultModel}' is not available. Please check the model name or your API access level.`,
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
error.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other errors during validation, log but don't fail initialization
|
|
||||||
// They might be temporary network issues
|
|
||||||
console.warn('OpenAI connection validation warning:', error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||||
* Validates model name format for OpenAI models.
|
return {
|
||||||
*
|
401: 'Authentication failed. Please verify your OpenAI API key from https://platform.openai.com/api-keys',
|
||||||
* @private
|
403: 'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||||
* @param modelName - Model name to validate
|
404: 'Model not found. The specified OpenAI model may not exist or be available to your account.',
|
||||||
* @throws {AIProviderError} If model name format is invalid
|
429: 'Too many requests. Please slow down and try again.',
|
||||||
*/
|
500: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||||
private validateModelName(modelName: string): void {
|
502: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||||
if (!modelName || typeof modelName !== 'string') {
|
503: 'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
||||||
throw new AIProviderError(
|
504: 'OpenAI service temporarily unavailable. Please try again in a few moments.'
|
||||||
'Model name must be a non-empty string',
|
};
|
||||||
AIErrorType.INVALID_REQUEST
|
}
|
||||||
|
|
||||||
|
protected override mapProviderError(error: any): AIProviderError | null {
|
||||||
|
if (!error) return null;
|
||||||
|
const message: string = error.message || '';
|
||||||
|
const status = error.status || error.statusCode;
|
||||||
|
|
||||||
|
if (status === 400) {
|
||||||
|
if (message.includes('max_tokens')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Max tokens value exceeds model limit. Please reduce max_tokens or use a model with higher limits.',
|
||||||
|
AIErrorType.INVALID_REQUEST, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (message.includes('model')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Invalid model specified. Please check model name and availability.',
|
||||||
|
AIErrorType.MODEL_NOT_FOUND, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (message.includes('messages')) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Invalid message format. Please check message structure and content.',
|
||||||
|
AIErrorType.INVALID_REQUEST, status, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 403 && (message.includes('billing') || message.includes('quota'))) {
|
||||||
|
return new AIProviderError(
|
||||||
|
'Insufficient quota or billing issue. Please check your OpenAI account billing and usage limits.',
|
||||||
|
AIErrorType.AUTHENTICATION, status, error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI model names follow specific patterns
|
if (status === 429 && message.includes('quota')) {
|
||||||
const validPatterns = [
|
return new AIProviderError(
|
||||||
/^gpt-4o(?:-mini)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4o, gpt-4o-mini, gpt-4o-2024-11-20
|
'Usage quota exceeded. Please check your OpenAI usage limits and billing.',
|
||||||
/^gpt-4(?:-turbo)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4, gpt-4-turbo, gpt-4-turbo-2024-04-09
|
AIErrorType.RATE_LIMIT, status, error
|
||||||
/^gpt-3\.5-turbo(?:-\d{4})?$/, // e.g., gpt-3.5-turbo, gpt-3.5-turbo-0125
|
);
|
||||||
/^gpt-4-\d{4}-preview$/, // e.g., gpt-4-0125-preview
|
|
||||||
/^text-davinci-\d{3}$/, // Legacy models
|
|
||||||
/^ft:.+$/ // Fine-tuned models
|
|
||||||
];
|
|
||||||
|
|
||||||
const isValid = validPatterns.some(pattern => pattern.test(modelName));
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
console.warn(`Model name '${modelName}' doesn't match expected OpenAI naming patterns. This may cause API errors.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -568,151 +544,4 @@ export class OpenAIProvider extends BaseAIProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles and transforms OpenAI-specific errors.
|
|
||||||
*
|
|
||||||
* This method maps OpenAI's error responses to our standardized
|
|
||||||
* error format, providing helpful context and actionable suggestions.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param error - Original error from OpenAI API
|
|
||||||
* @returns Normalized AIProviderError
|
|
||||||
*/
|
|
||||||
private handleOpenAIError(error: any): AIProviderError {
|
|
||||||
if (error instanceof AIProviderError) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = error.message || 'Unknown OpenAI API error';
|
|
||||||
const status = error.status || error.statusCode;
|
|
||||||
|
|
||||||
// Map OpenAI-specific error codes and types
|
|
||||||
switch (status) {
|
|
||||||
case 400:
|
|
||||||
if (message.includes('max_tokens')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Max tokens value exceeds model limit. Please reduce max_tokens or use a model with higher limits.',
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.includes('model')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Invalid model specified. Please check model name and availability.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.includes('messages')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Invalid message format. Please check message structure and content.',
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new AIProviderError(
|
|
||||||
`Invalid request: ${message}`,
|
|
||||||
AIErrorType.INVALID_REQUEST,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 401:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Authentication failed. Please verify your OpenAI API key from https://platform.openai.com/api-keys',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 403:
|
|
||||||
if (message.includes('billing') || message.includes('quota')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Insufficient quota or billing issue. Please check your OpenAI account billing and usage limits.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new AIProviderError(
|
|
||||||
'Access forbidden. Your API key may not have permission for this model or feature.',
|
|
||||||
AIErrorType.AUTHENTICATION,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 404:
|
|
||||||
return new AIProviderError(
|
|
||||||
'Model not found. The specified OpenAI model may not exist or be available to your account.',
|
|
||||||
AIErrorType.MODEL_NOT_FOUND,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 429:
|
|
||||||
if (message.includes('rate')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Rate limit exceeded. Please reduce request frequency and implement exponential backoff.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (message.includes('quota')) {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Usage quota exceeded. Please check your OpenAI usage limits and billing.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new AIProviderError(
|
|
||||||
'Too many requests. Please slow down and try again.',
|
|
||||||
AIErrorType.RATE_LIMIT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
case 500:
|
|
||||||
case 502:
|
|
||||||
case 503:
|
|
||||||
case 504:
|
|
||||||
return new AIProviderError(
|
|
||||||
'OpenAI service temporarily unavailable. Please try again in a few moments.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Handle timeout and network errors
|
|
||||||
if (message.includes('timeout') || error.code === 'ETIMEDOUT') {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Request timed out. OpenAI may be experiencing high load.',
|
|
||||||
AIErrorType.TIMEOUT,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.includes('network') || error.code === 'ECONNREFUSED') {
|
|
||||||
return new AIProviderError(
|
|
||||||
'Network error connecting to OpenAI servers.',
|
|
||||||
AIErrorType.NETWORK,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AIProviderError(
|
|
||||||
`OpenAI API error: ${message}`,
|
|
||||||
AIErrorType.UNKNOWN,
|
|
||||||
status,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ export class OpenWebUIProvider extends BaseAIProvider {
|
|||||||
* @private
|
* @private
|
||||||
* @throws {AIProviderError} If connection validation fails
|
* @throws {AIProviderError} If connection validation fails
|
||||||
*/
|
*/
|
||||||
private async validateConnection(): Promise<void> {
|
protected override async validateConnection(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Try to fetch available models to test connectivity
|
// Try to fetch available models to test connectivity
|
||||||
const url = this.useOllamaProxy
|
const url = this.useOllamaProxy
|
||||||
|
|||||||
Reference in New Issue
Block a user