refactor: extract constants, parameterize validators, simplify factory

Quick-win refactors from the Refactoring Guru catalog:

- Replace Magic Number with Symbolic Constant: extract DEFAULT_TIMEOUT_MS,
  DEFAULT_MAX_RETRIES, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE, default
  models per provider, and validation prompt into src/constants.ts.
- Parameterize Method: collapse validateTemperature / validateTopP /
  validateMaxTokens (and the inline timeout/maxRetries checks) into a
  single validateNumberInRange helper with bounds metadata.
- Inline Class / Remove Middle Man: drop the unused ProviderRegistry
  class from utils/factory.ts. createProvider now dispatches through
  the PROVIDER_REGISTRY const directly instead of a parallel switch.

Also fixes stale VERSION constant in src/index.ts (was 1.3.1).
This commit is contained in:
2026-05-21 13:35:12 +02:00
parent 797fad1b00
commit bb37e61eaf
8 changed files with 163 additions and 253 deletions

View File

@@ -23,6 +23,11 @@ import type {
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
@@ -314,60 +319,78 @@ export abstract class BaseAIProvider {
);
}
// Apply defaults and return normalized config
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: this.validateTimeout(config.timeout),
maxRetries: this.validateMaxRetries(config.maxRetries)
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES
};
}
/**
* Validates timeout configuration value.
*
* @private
* @param timeout - Timeout value to validate
* @returns Validated timeout value with default if needed
* Validates an optional numeric parameter against a range.
* Skips validation when value is undefined so callers can apply defaults afterward.
*/
private validateTimeout(timeout?: number): number {
const defaultTimeout = 30000; // 30 seconds
if (timeout === undefined) {
return defaultTimeout;
private validateNumberInRange(
name: string,
value: number | undefined,
bounds: {
min?: number;
max?: number;
exclusiveMin?: boolean;
integer?: boolean;
label?: string;
}
): void {
if (value === undefined) return;
if (typeof timeout !== 'number' || timeout < 1000 || timeout > 300000) {
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(
'Timeout must be a number between 1000ms (1s) and 300000ms (5min)',
`${name} must be a number between ${range}`,
AIErrorType.INVALID_REQUEST
);
}
return timeout;
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
);
}
}
/**
* Validates max retries configuration value.
*
* @private
* @param maxRetries - Max retries value to validate
* @returns Validated max retries value with default if needed
*/
private validateMaxRetries(maxRetries?: number): number {
const defaultMaxRetries = 3;
if (maxRetries === undefined) {
return defaultMaxRetries;
private describeRange(min?: number, max?: number, exclusiveMin?: boolean): string {
if (min !== undefined && max !== undefined) {
return `${exclusiveMin ? '>' : '>='}${min} and <=${max}`;
}
if (typeof maxRetries !== 'number' || maxRetries < 0 || maxRetries > 10) {
throw new AIProviderError(
'Max retries must be a number between 0 and 10',
AIErrorType.INVALID_REQUEST
);
}
return maxRetries;
if (min !== undefined) return `${exclusiveMin ? '>' : '>='}${min}`;
if (max !== undefined) return `<=${max}`;
return 'a valid number';
}
/**
@@ -403,13 +426,11 @@ export abstract class BaseAIProvider {
);
}
// Validate messages array
this.validateMessages(params.messages);
// Validate optional parameters
this.validateTemperature(params.temperature);
this.validateTopP(params.topP);
this.validateMaxTokens(params.maxTokens);
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);
}
@@ -456,67 +477,6 @@ export abstract class BaseAIProvider {
}
}
/**
* Validates temperature parameter.
*
* @private
* @param temperature - Temperature value to validate
* @throws {AIProviderError} If temperature is invalid
*/
private validateTemperature(temperature?: number): void {
if (temperature !== undefined) {
if (typeof temperature !== 'number' || temperature < 0 || temperature > 1) {
throw new AIProviderError(
'Temperature must be a number between 0.0 and 1.0',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates top-p parameter.
*
* @private
* @param topP - Top-p value to validate
* @throws {AIProviderError} If top-p is invalid
*/
private validateTopP(topP?: number): void {
if (topP !== undefined) {
if (typeof topP !== 'number' || topP <= 0 || topP > 1) {
throw new AIProviderError(
'Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates max tokens parameter.
*
* @private
* @param maxTokens - Max tokens value to validate
* @throws {AIProviderError} If max tokens is invalid
*/
private validateMaxTokens(maxTokens?: number): void {
if (maxTokens !== undefined) {
if (typeof maxTokens !== 'number' || !Number.isInteger(maxTokens) || maxTokens < 1) {
throw new AIProviderError(
'Max tokens must be a positive integer',
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* Validates stop sequences parameter.
*
* @private
* @param stopSequences - Stop sequences to validate
* @throws {AIProviderError} If stop sequences are invalid
*/
private validateStopSequences(stopSequences?: string[]): void {
if (stopSequences !== undefined) {
if (!Array.isArray(stopSequences)) {