diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..7cdd2c7 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,35 @@ +/** + * Shared defaults used across all providers. + */ + +export const DEFAULT_TIMEOUT_MS = 30_000; +export const DEFAULT_MAX_RETRIES = 3; +export const DEFAULT_MAX_TOKENS = 1000; +export const DEFAULT_TEMPERATURE = 0.7; + +/** + * Minimal prompt used to validate API credentials on initialization. + */ +export const VALIDATION_PROMPT = 'Hi'; + +export const DEFAULT_MODELS = { + claude: 'claude-3-5-sonnet-20241022', + openai: 'gpt-4o', + gemini: 'gemini-2.5-flash', + openwebui: 'llama3.1:latest' +} as const; + +export const DEFAULT_OPENWEBUI_BASE_URL = 'http://localhost:3000'; + +export const DEFAULT_ANTHROPIC_VERSION = '2023-06-01'; + +/** + * Bounds for configuration value validation. + */ +export const CONFIG_BOUNDS = { + timeoutMs: { min: 1000, max: 300_000 }, + maxRetries: { min: 0, max: 10 }, + temperature: { min: 0, max: 1 }, + topP: { min: 0, max: 1, exclusiveMin: true }, + maxTokens: { min: 1 } +} as const; diff --git a/src/index.ts b/src/index.ts index b10b84a..1164a50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,4 +58,4 @@ export const SUPPORTED_PROVIDERS = ['claude', 'openai', 'gemini', 'openwebui'] a /** * Package version */ -export const VERSION = '1.3.1'; \ No newline at end of file +export const VERSION = '2.0.0'; diff --git a/src/providers/base.ts b/src/providers/base.ts index d52b1a8..bad8ba3 100644 --- a/src/providers/base.ts +++ b/src/providers/base.ts @@ -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)) { diff --git a/src/providers/claude.ts b/src/providers/claude.ts index 167cabc..f1bd7a9 100644 --- a/src/providers/claude.ts +++ b/src/providers/claude.ts @@ -34,6 +34,13 @@ import type { } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; +import { + DEFAULT_ANTHROPIC_VERSION, + DEFAULT_MAX_TOKENS, + DEFAULT_MODELS, + DEFAULT_TEMPERATURE, + VALIDATION_PROMPT +} from '../constants.js'; // ============================================================================ // TYPES AND INTERFACES @@ -153,9 +160,8 @@ export class ClaudeProvider extends BaseAIProvider { constructor(config: ClaudeConfig) { super(config); - // Set Claude-specific defaults - this.defaultModel = config.defaultModel || 'claude-3-5-sonnet-20241022'; - this.version = config.version || '2023-06-01'; + this.defaultModel = config.defaultModel || DEFAULT_MODELS.claude; + this.version = config.version || DEFAULT_ANTHROPIC_VERSION; // Validate model name format this.validateModelName(this.defaultModel); @@ -329,11 +335,10 @@ export class ClaudeProvider extends BaseAIProvider { } try { - // Make minimal request to test connection and permissions await this.client.messages.create({ model: this.defaultModel, max_tokens: 1, - messages: [{ role: 'user', content: 'Hi' }] + messages: [{ role: 'user', content: VALIDATION_PROMPT }] }); } catch (error: any) { @@ -449,8 +454,8 @@ export class ClaudeProvider extends BaseAIProvider { ) { return { model: params.model || this.defaultModel, - max_tokens: params.maxTokens || 1000, - temperature: params.temperature ?? 0.7, + max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, stop_sequences: params.stopSequences, system: system || undefined, diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 7ddee45..78b342f 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -31,6 +31,12 @@ import type { } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; +import { + DEFAULT_MAX_TOKENS, + DEFAULT_MODELS, + DEFAULT_TEMPERATURE, + VALIDATION_PROMPT +} from '../constants.js'; // ============================================================================ // TYPES AND INTERFACES @@ -95,7 +101,7 @@ export class GeminiProvider extends BaseAIProvider { constructor(config: GeminiConfig) { super(config); - this.defaultModel = config.defaultModel || 'gemini-2.5-flash'; + this.defaultModel = config.defaultModel || DEFAULT_MODELS.gemini; this.safetySettings = config.safetySettings; this.defaultGenerationConfig = config.generationConfig; @@ -207,7 +213,7 @@ export class GeminiProvider extends BaseAIProvider { try { await this.client.models.generateContent({ model: this.defaultModel, - contents: [{ role: 'user', parts: [{ text: 'Hi' }] }], + contents: [{ role: 'user', parts: [{ text: VALIDATION_PROMPT }] }], config: { maxOutputTokens: 1 } }); } catch (error: any) { @@ -291,8 +297,8 @@ export class GeminiProvider extends BaseAIProvider { const defaults = this.defaultGenerationConfig; const config: GenerateContentConfig = { - temperature: params.temperature ?? defaults?.temperature ?? 0.7, - maxOutputTokens: params.maxTokens ?? defaults?.maxOutputTokens ?? 1000 + temperature: params.temperature ?? defaults?.temperature ?? DEFAULT_TEMPERATURE, + maxOutputTokens: params.maxTokens ?? defaults?.maxOutputTokens ?? DEFAULT_MAX_TOKENS }; const topP = params.topP ?? defaults?.topP; diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 135157d..00e89f0 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -35,6 +35,12 @@ import type { } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; +import { + DEFAULT_MAX_TOKENS, + DEFAULT_MODELS, + DEFAULT_TEMPERATURE, + VALIDATION_PROMPT +} from '../constants.js'; // ============================================================================ // TYPES AND INTERFACES @@ -173,7 +179,7 @@ export class OpenAIProvider extends BaseAIProvider { super(config); // Set OpenAI-specific defaults - this.defaultModel = config.defaultModel || 'gpt-4o'; + this.defaultModel = config.defaultModel || DEFAULT_MODELS.openai; this.organization = config.organization; this.project = config.project; @@ -352,10 +358,9 @@ export class OpenAIProvider extends BaseAIProvider { } try { - // Make minimal request to test connection and permissions await this.client.chat.completions.create({ model: this.defaultModel, - messages: [{ role: 'user', content: 'Hi' }], + messages: [{ role: 'user', content: VALIDATION_PROMPT }], max_tokens: 1 }); @@ -435,8 +440,8 @@ export class OpenAIProvider extends BaseAIProvider { return { model: params.model || this.defaultModel, messages: this.convertMessages(params.messages), - max_tokens: params.maxTokens || 1000, - temperature: params.temperature ?? 0.7, + max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, stop: params.stopSequences, stream, diff --git a/src/providers/openwebui.ts b/src/providers/openwebui.ts index 6a2fc40..d8220b2 100644 --- a/src/providers/openwebui.ts +++ b/src/providers/openwebui.ts @@ -34,6 +34,12 @@ import type { } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; +import { + DEFAULT_MAX_TOKENS, + DEFAULT_MODELS, + DEFAULT_OPENWEBUI_BASE_URL, + DEFAULT_TEMPERATURE +} from '../constants.js'; // ============================================================================ // TYPES AND INTERFACES @@ -281,8 +287,8 @@ export class OpenWebUIProvider extends BaseAIProvider { super(config); // Set OpenWebUI-specific defaults and normalize configuration - this.defaultModel = config.defaultModel || 'llama3.1:latest'; - this.baseUrl = this.normalizeBaseUrl(config.baseUrl || 'http://localhost:3000'); + this.defaultModel = config.defaultModel || DEFAULT_MODELS.openwebui; + this.baseUrl = this.normalizeBaseUrl(config.baseUrl || DEFAULT_OPENWEBUI_BASE_URL); this.useOllamaProxy = config.useOllamaProxy ?? false; this.dangerouslyAllowInsecureConnections = config.dangerouslyAllowInsecureConnections ?? true; @@ -689,8 +695,8 @@ export class OpenWebUIProvider extends BaseAIProvider { const requestBody = { model: params.model || this.defaultModel, messages: this.convertMessages(params.messages), - max_tokens: params.maxTokens || 1000, - temperature: params.temperature ?? 0.7, + max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, stop: params.stopSequences, stream: false @@ -724,9 +730,9 @@ export class OpenWebUIProvider extends BaseAIProvider { prompt: prompt, stream: false, options: { - temperature: params.temperature ?? 0.7, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, - num_predict: params.maxTokens || 1000, + num_predict: params.maxTokens || DEFAULT_MAX_TOKENS, stop: params.stopSequences } }; @@ -754,8 +760,8 @@ export class OpenWebUIProvider extends BaseAIProvider { const requestBody = { model: params.model || this.defaultModel, messages: this.convertMessages(params.messages), - max_tokens: params.maxTokens || 1000, - temperature: params.temperature ?? 0.7, + max_tokens: params.maxTokens || DEFAULT_MAX_TOKENS, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, stop: params.stopSequences, stream: true @@ -850,9 +856,9 @@ export class OpenWebUIProvider extends BaseAIProvider { prompt: prompt, stream: true, options: { - temperature: params.temperature ?? 0.7, + temperature: params.temperature ?? DEFAULT_TEMPERATURE, top_p: params.topP, - num_predict: params.maxTokens || 1000, + num_predict: params.maxTokens || DEFAULT_MAX_TOKENS, stop: params.stopSequences } }; diff --git a/src/utils/factory.ts b/src/utils/factory.ts index cef23af..cf68796 100644 --- a/src/utils/factory.ts +++ b/src/utils/factory.ts @@ -1,23 +1,22 @@ /** - * Factory utilities for creating AI providers - * Provides convenient methods for instantiating and configuring providers + * Factory utilities for creating AI providers. */ -import type { AIProviderConfig } from '../types/index.js'; import { ClaudeProvider, type ClaudeConfig } from '../providers/claude.js'; import { OpenAIProvider, type OpenAIConfig } from '../providers/openai.js'; import { GeminiProvider, type GeminiConfig } from '../providers/gemini.js'; import { OpenWebUIProvider, type OpenWebUIConfig } from '../providers/openwebui.js'; import { BaseAIProvider } from '../providers/base.js'; -/** - * Supported AI provider types - */ -export type ProviderType = 'claude' | 'openai' | 'gemini' | 'openwebui'; +export const PROVIDER_REGISTRY = { + claude: ClaudeProvider, + openai: OpenAIProvider, + gemini: GeminiProvider, + openwebui: OpenWebUIProvider +} as const; + +export type ProviderType = keyof typeof PROVIDER_REGISTRY; -/** - * Configuration map for different provider types - */ export interface ProviderConfigMap { claude: ClaudeConfig; openai: OpenAIConfig; @@ -25,144 +24,38 @@ export interface ProviderConfigMap { openwebui: OpenWebUIConfig; } -/** - * Factory function to create AI providers - * @param type - The type of provider to create - * @param config - Configuration for the provider - * @returns Configured AI provider instance - */ export function createProvider( type: T, config: ProviderConfigMap[T] ): BaseAIProvider { - switch (type) { - case 'claude': - return new ClaudeProvider(config as ClaudeConfig); - case 'openai': - return new OpenAIProvider(config as OpenAIConfig); - case 'gemini': - return new GeminiProvider(config as GeminiConfig); - case 'openwebui': - return new OpenWebUIProvider(config as OpenWebUIConfig); - default: - throw new Error(`Unsupported provider type: ${type}`); + const ProviderClass = PROVIDER_REGISTRY[type]; + if (!ProviderClass) { + throw new Error(`Unsupported provider type: ${type}`); } + return new ProviderClass(config as any); } -/** - * Create a Claude provider with simplified configuration - * @param apiKey - Anthropic API key - * @param options - Optional additional configuration - * @returns Configured Claude provider instance - */ export function createClaudeProvider( apiKey: string, options: Partial> = {} ): ClaudeProvider { - return new ClaudeProvider({ - apiKey, - ...options - }); + return new ClaudeProvider({ apiKey, ...options }); } -/** - * Create an OpenAI provider with simplified configuration - * @param apiKey - OpenAI API key - * @param options - Optional additional configuration - * @returns Configured OpenAI provider instance - */ export function createOpenAIProvider( apiKey: string, options: Partial> = {} ): OpenAIProvider { - return new OpenAIProvider({ - apiKey, - ...options - }); + return new OpenAIProvider({ apiKey, ...options }); } -/** - * Create a Gemini provider with simplified configuration - * @param apiKey - Google AI API key - * @param options - Optional additional configuration - * @returns Configured Gemini provider instance - */ export function createGeminiProvider( apiKey: string, options: Partial> = {} ): GeminiProvider { - return new GeminiProvider({ - apiKey, - ...options - }); + return new GeminiProvider({ apiKey, ...options }); } -/** - * Create an OpenWebUI provider instance - */ export function createOpenWebUIProvider(config: OpenWebUIConfig): OpenWebUIProvider { return new OpenWebUIProvider(config); } - -/** - * Provider registry for dynamic provider creation - */ -export class ProviderRegistry { - private static providers = new Map BaseAIProvider>(); - - /** - * Register a new provider type - * @param name - Name of the provider - * @param providerClass - Provider class constructor - */ - static register(name: string, providerClass: new (config: AIProviderConfig) => BaseAIProvider): void { - this.providers.set(name.toLowerCase(), providerClass); - } - - /** - * Create a provider by name - * @param name - Name of the provider - * @param config - Configuration for the provider - * @returns Provider instance - */ - static create(name: string, config: AIProviderConfig): BaseAIProvider { - const ProviderClass = this.providers.get(name.toLowerCase()); - if (!ProviderClass) { - throw new Error(`Provider '${name}' is not registered`); - } - return new ProviderClass(config); - } - - /** - * Get list of registered provider names - * @returns Array of registered provider names - */ - static getRegisteredProviders(): string[] { - return Array.from(this.providers.keys()); - } - - /** - * Check if a provider is registered - * @param name - Name of the provider - * @returns True if provider is registered - */ - static isRegistered(name: string): boolean { - return this.providers.has(name.toLowerCase()); - } -} - -// Pre-register built-in providers -ProviderRegistry.register('claude', ClaudeProvider); -ProviderRegistry.register('openai', OpenAIProvider); -ProviderRegistry.register('gemini', GeminiProvider); -ProviderRegistry.register('openwebui', OpenWebUIProvider); - -/** - * Registry of all available providers - */ -export const PROVIDER_REGISTRY = { - claude: ClaudeProvider, - openai: OpenAIProvider, - gemini: GeminiProvider, - openwebui: OpenWebUIProvider -} as const; \ No newline at end of file