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:
@@ -184,7 +184,6 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
*/
|
||||
protected async doInitialize(): Promise<void> {
|
||||
try {
|
||||
// Create Anthropic client with optimized configuration
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.config.apiKey,
|
||||
baseURL: this.config.baseUrl,
|
||||
@@ -192,17 +191,14 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
maxRetries: this.config.maxRetries,
|
||||
defaultHeaders: {
|
||||
'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();
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on failure
|
||||
this.client = null;
|
||||
|
||||
|
||||
throw new AIProviderError(
|
||||
`Failed to initialize Claude provider: ${(error as Error).message}`,
|
||||
AIErrorType.AUTHENTICATION,
|
||||
@@ -231,22 +227,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process messages for Claude's format requirements
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
|
||||
// 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);
|
||||
}
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
const requestParams = this.buildRequestParams(params, system, messages, false);
|
||||
const response = await this.client.messages.create(requestParams);
|
||||
return this.formatCompletionResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,22 +252,10 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
// Process messages for Claude's format requirements
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
|
||||
// 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);
|
||||
}
|
||||
const { system, messages } = this.processMessages(params.messages);
|
||||
const requestParams = this.buildRequestParams(params, system, messages, true);
|
||||
const stream = await this.client.messages.create(requestParams);
|
||||
yield* this.processStreamChunks(stream);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -323,75 +295,62 @@ export class ClaudeProvider extends BaseAIProvider {
|
||||
// PRIVATE UTILITY METHODS
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Validates the connection by making a minimal test request.
|
||||
*
|
||||
* @private
|
||||
* @throws {AIProviderError} If connection validation fails
|
||||
*/
|
||||
private async validateConnection(): Promise<void> {
|
||||
protected override getModelNamePatterns(): RegExp[] {
|
||||
return [
|
||||
/^claude-3(?:-5)?-(?:opus|sonnet|haiku)-\d{8}$/,
|
||||
/^claude-instant-[0-9.]+$/,
|
||||
/^claude-[0-9.]+$/
|
||||
];
|
||||
}
|
||||
|
||||
protected override async sendValidationProbe(): Promise<void> {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.messages.create({
|
||||
model: this.defaultModel,
|
||||
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);
|
||||
}
|
||||
await this.client.messages.create({
|
||||
model: this.defaultModel,
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: VALIDATION_PROMPT }]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates model name format for Claude models.
|
||||
*
|
||||
* @private
|
||||
* @param modelName - Model name to validate
|
||||
* @throws {AIProviderError} If model name format is invalid
|
||||
*/
|
||||
private validateModelName(modelName: string): void {
|
||||
if (!modelName || typeof modelName !== 'string') {
|
||||
throw new AIProviderError(
|
||||
'Model name must be a non-empty string',
|
||||
AIErrorType.INVALID_REQUEST
|
||||
);
|
||||
protected override providerErrorMessages(): Partial<Record<number, string>> {
|
||||
return {
|
||||
401: 'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
|
||||
403: 'Access forbidden. Your API key may not have permission for this model or feature.',
|
||||
404: 'Model not found. The specified Claude model may not be available to your account.',
|
||||
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.',
|
||||
502: 'Anthropic service temporarily unavailable. Please try again in a few moments.',
|
||||
503: 'Anthropic 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 (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
|
||||
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.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user