refactor: update provider documentation and improve comments

This commit is contained in:
2025-05-28 13:00:25 +02:00
parent 9bf8030ad5
commit 5da20536af
10 changed files with 2658 additions and 824 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "simple-ai-provider", "name": "simple-ai-provider",
"version": "1.1.1", "version": "1.1.2",
"description": "A simple and extensible AI provider package for easy integration of multiple AI services", "description": "A simple and extensible AI provider package for easy integration of multiple AI services",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

View File

@ -1,6 +1,17 @@
/** /**
* Abstract base class for all AI providers * Abstract Base Provider for AI Services
* Provides common functionality and enforces a consistent interface *
* 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 { import type {
@ -12,120 +23,333 @@ import type {
} from '../types/index.js'; } from '../types/index.js';
import { AIProviderError, AIErrorType } from '../types/index.js'; import { AIProviderError, AIErrorType } from '../types/index.js';
// ============================================================================
// ABSTRACT BASE PROVIDER CLASS
// ============================================================================
/** /**
* Abstract base class that all AI providers must extend * 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 { export abstract class BaseAIProvider {
protected config: AIProviderConfig; // ========================================================================
protected initialized: boolean = false; // INSTANCE PROPERTIES
// ========================================================================
constructor(config: AIProviderConfig) { /** Validated configuration object for this provider instance */
this.config = this.validateConfig(config); protected readonly config: AIProviderConfig;
}
/** Internal flag tracking initialization state */
private initialized: boolean = false;
// ========================================================================
// CONSTRUCTOR & CONFIGURATION
// ========================================================================
/** /**
* Validates the provider configuration * Constructs a new provider instance with validated configuration.
* @param config - Configuration object to validate *
* @returns Validated configuration * @param config - Provider configuration object
* @throws AIProviderError if configuration is invalid * @throws {AIProviderError} If configuration is invalid
*
* @example
* ```typescript
* const provider = new MyProvider({
* apiKey: 'your-api-key',
* timeout: 30000,
* maxRetries: 3
* });
* ```
*/ */
protected validateConfig(config: AIProviderConfig): AIProviderConfig { constructor(config: AIProviderConfig) {
if (!config.apiKey || typeof config.apiKey !== 'string') { this.config = this.validateAndNormalizeConfig(config);
throw new AIProviderError(
'API key is required and must be a string',
AIErrorType.INVALID_REQUEST
);
} }
// Set default values // ========================================================================
const validatedConfig = { // PUBLIC API METHODS
...config, // ========================================================================
timeout: config.timeout ?? 30000,
maxRetries: config.maxRetries ?? 3
};
return validatedConfig;
}
/** /**
* Initialize the provider (setup connections, validate credentials, etc.) * Initializes the provider for use.
* Must be called before using the provider *
* 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> { public async initialize(): Promise<void> {
try { try {
await this.doInitialize(); await this.doInitialize();
this.initialized = true; this.initialized = true;
} catch (error) { } catch (error) {
throw this.handleError(error as Error); // Normalize any error to our standard format
throw this.normalizeError(error as Error);
} }
} }
/** /**
* Check if the provider is initialized * Checks if the provider has been initialized.
*
* @returns true if the provider is ready for use, false otherwise
*/ */
public isInitialized(): boolean { public isInitialized(): boolean {
return this.initialized; return this.initialized;
} }
/** /**
* Generate a completion based on the provided parameters * Generates a text completion based on the provided parameters.
* @param params - Parameters for the completion request *
* @returns Promise resolving to completion response * 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(params: CompletionParams): Promise<CompletionResponse> { public async complete(params: CompletionParams): Promise<CompletionResponse> {
// Ensure provider is ready for use
this.ensureInitialized(); this.ensureInitialized();
// Validate input parameters early
this.validateCompletionParams(params); this.validateCompletionParams(params);
try { try {
// Delegate to provider-specific implementation
return await this.doComplete(params); return await this.doComplete(params);
} catch (error) { } catch (error) {
throw this.handleError(error as Error); // Normalize error to our standard format
throw this.normalizeError(error as Error);
} }
} }
/** /**
* Generate a streaming completion * Generates a streaming text completion.
* @param params - Parameters for the completion request *
* @returns AsyncIterable of completion chunks * 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(params: CompletionParams): AsyncIterable<CompletionChunk> { public async *stream(params: CompletionParams): AsyncIterable<CompletionChunk> {
// Ensure provider is ready for use
this.ensureInitialized(); this.ensureInitialized();
// Validate input parameters early
this.validateCompletionParams(params); this.validateCompletionParams(params);
try { try {
// Delegate to provider-specific implementation
yield* this.doStream(params); yield* this.doStream(params);
} catch (error) { } catch (error) {
throw this.handleError(error as Error); // Normalize error to our standard format
throw this.normalizeError(error as Error);
} }
} }
/** /**
* Get information about this provider * Returns information about this provider and its capabilities.
* @returns Provider information and 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; public abstract getInfo(): ProviderInfo;
// ========================================================================
// ABSTRACT METHODS (TEMPLATE METHOD PATTERN)
// ========================================================================
/** /**
* Provider-specific initialization logic * Provider-specific initialization logic.
* Override this method in concrete implementations *
* 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>; protected abstract doInitialize(): Promise<void>;
/** /**
* Provider-specific completion logic * Provider-specific completion implementation.
* Override this method in concrete implementations *
* 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>; protected abstract doComplete(params: CompletionParams): Promise<CompletionResponse>;
/** /**
* Provider-specific streaming logic * Provider-specific streaming implementation.
* Override this method in concrete implementations *
* 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(params: CompletionParams): AsyncIterable<CompletionChunk>; protected abstract doStream(params: CompletionParams): AsyncIterable<CompletionChunk>;
// ========================================================================
// PROTECTED UTILITY METHODS
// ========================================================================
/** /**
* Ensures the provider is initialized before use * Validates and normalizes provider configuration.
* @throws AIProviderError if not initialized *
* 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
);
}
// Apply defaults and return normalized config
return {
...config,
timeout: this.validateTimeout(config.timeout),
maxRetries: this.validateMaxRetries(config.maxRetries)
};
}
/**
* Validates timeout configuration value.
*
* @private
* @param timeout - Timeout value to validate
* @returns Validated timeout value with default if needed
*/
private validateTimeout(timeout?: number): number {
const defaultTimeout = 30000; // 30 seconds
if (timeout === undefined) {
return defaultTimeout;
}
if (typeof timeout !== 'number' || timeout < 1000 || timeout > 300000) {
throw new AIProviderError(
'Timeout must be a number between 1000ms (1s) and 300000ms (5min)',
AIErrorType.INVALID_REQUEST
);
}
return timeout;
}
/**
* 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;
}
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;
}
/**
* Ensures the provider has been initialized before use.
*
* @protected
* @throws {AIProviderError} If the provider is not initialized
*/ */
protected ensureInitialized(): void { protected ensureInitialized(): void {
if (!this.initialized) { if (!this.initialized) {
@ -137,127 +361,250 @@ export abstract class BaseAIProvider {
} }
/** /**
* Validates completion parameters * 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 * @param params - Parameters to validate
* @throws AIProviderError if parameters are invalid * @throws {AIProviderError} If any parameter is invalid
*/ */
protected validateCompletionParams(params: CompletionParams): void { protected validateCompletionParams(params: CompletionParams): void {
if (!params.messages || !Array.isArray(params.messages) || params.messages.length === 0) { if (!params || typeof params !== 'object') {
throw new AIProviderError( throw new AIProviderError(
'Messages array is required and must not be empty', 'Completion parameters object is required',
AIErrorType.INVALID_REQUEST AIErrorType.INVALID_REQUEST
); );
} }
for (const message of params.messages) { // Validate messages array
if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) { this.validateMessages(params.messages);
// Validate optional parameters
this.validateTemperature(params.temperature);
this.validateTopP(params.topP);
this.validateMaxTokens(params.maxTokens);
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( throw new AIProviderError(
'Each message must have a valid role (system, user, or assistant)', 'Messages must be a non-empty array',
AIErrorType.INVALID_REQUEST AIErrorType.INVALID_REQUEST
); );
} }
if (!message.content || typeof message.content !== 'string') { const validRoles = ['system', 'user', 'assistant'] as const;
throw new AIProviderError(
'Each message must have non-empty string content',
AIErrorType.INVALID_REQUEST
);
}
}
if (params.temperature !== undefined && (params.temperature < 0 || params.temperature > 1)) { for (let i = 0; i < messages.length; i++) {
const message = messages[i];
if (!message || typeof message !== 'object') {
throw new AIProviderError( throw new AIProviderError(
'Temperature must be between 0.0 and 1.0', `Message at index ${i} must be an object`,
AIErrorType.INVALID_REQUEST AIErrorType.INVALID_REQUEST
); );
} }
if (params.topP !== undefined && (params.topP < 0 || params.topP > 1)) { if (!validRoles.includes(message.role)) {
throw new AIProviderError( throw new AIProviderError(
'Top-p must be between 0.0 and 1.0', `Message at index ${i} has invalid role '${message.role}'. Must be: ${validRoles.join(', ')}`,
AIErrorType.INVALID_REQUEST AIErrorType.INVALID_REQUEST
); );
} }
if (params.maxTokens !== undefined && params.maxTokens < 1) { if (typeof message.content !== 'string' || message.content.trim() === '') {
throw new AIProviderError(
`Message at index ${i} must have non-empty string content`,
AIErrorType.INVALID_REQUEST
);
}
}
}
/**
* 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( throw new AIProviderError(
'Max tokens must be a positive integer', 'Max tokens must be a positive integer',
AIErrorType.INVALID_REQUEST AIErrorType.INVALID_REQUEST
); );
} }
} }
}
/** /**
* Handles and transforms errors into AIProviderError instances * Validates stop sequences parameter.
* @param error - The original error *
* @returns AIProviderError with appropriate type and context * @private
* @param stopSequences - Stop sequences to validate
* @throws {AIProviderError} If stop sequences are invalid
*/ */
protected handleError(error: Error): AIProviderError { 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
);
}
}
}
}
/**
* 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 already normalized, return as-is
if (error instanceof AIProviderError) { if (error instanceof AIProviderError) {
return error; return error;
} }
// Handle common HTTP status codes // Extract status code if available
if ('status' in error) { const status = (error as any).status || (error as any).statusCode;
const status = (error as any).status; const message = error.message || 'Unknown error occurred';
// Map HTTP status codes to error types
if (status) {
switch (status) { switch (status) {
case 400:
return new AIProviderError(
`Bad request: ${message}`,
AIErrorType.INVALID_REQUEST,
status,
error
);
case 401: case 401:
case 403: case 403:
return new AIProviderError( return new AIProviderError(
'Authentication failed. Please check your API key.', 'Authentication failed. Please verify your API key is correct and has the necessary permissions.',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
status, status,
error error
); );
case 429:
return new AIProviderError(
'Rate limit exceeded. Please try again later.',
AIErrorType.RATE_LIMIT,
status,
error
);
case 404: case 404:
return new AIProviderError( return new AIProviderError(
'Model or endpoint not found.', 'The specified model or endpoint was not found. Please check the model name and availability.',
AIErrorType.MODEL_NOT_FOUND, AIErrorType.MODEL_NOT_FOUND,
status, status,
error error
); );
case 400:
case 429:
return new AIProviderError( return new AIProviderError(
'Invalid request parameters.', 'Rate limit exceeded. Please reduce your request frequency and try again later.',
AIErrorType.INVALID_REQUEST, AIErrorType.RATE_LIMIT,
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, status,
error error
); );
} }
} }
// Handle timeout errors // Map common error patterns
if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) { if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
return new AIProviderError( return new AIProviderError(
'Request timed out. Please try again.', 'Request timed out. The operation took longer than expected.',
AIErrorType.TIMEOUT, AIErrorType.TIMEOUT,
undefined, undefined,
error error
); );
} }
// Handle network errors if (message.includes('network') || message.includes('ENOTFOUND') || message.includes('ECONNREFUSED')) {
if (error.message.includes('network') || error.message.includes('ENOTFOUND')) {
return new AIProviderError( return new AIProviderError(
'Network error occurred. Please check your connection.', 'Network error occurred. Please check your internet connection and try again.',
AIErrorType.NETWORK, AIErrorType.NETWORK,
undefined, undefined,
error error
); );
} }
// Default to unknown error // Default to unknown error type
return new AIProviderError( return new AIProviderError(
`Unknown error: ${error.message}`, `Provider error: ${message}`,
AIErrorType.UNKNOWN, AIErrorType.UNKNOWN,
undefined, status,
error error
); );
} }

View File

@ -1,6 +1,26 @@
/** /**
* Claude AI Provider implementation using Anthropic's API * Anthropic Claude AI Provider Implementation
* Provides integration with Claude models through a standardized interface *
* This module provides integration with Anthropic's Claude models through their official SDK.
* Claude is known for its advanced reasoning capabilities, long context length, and excellent
* performance on complex analytical tasks.
*
* Key Features:
* - Support for all Claude 3.x model variants (Opus, Sonnet, Haiku)
* - Native streaming support with real-time response chunks
* - Advanced system message handling (separate from conversation context)
* - Comprehensive error mapping for Anthropic-specific issues
* - Automatic retry logic with exponential backoff
*
* Claude-Specific Considerations:
* - System messages must be passed separately from conversation messages
* - Uses "input_tokens" and "output_tokens" terminology instead of "prompt_tokens"
* - Supports advanced features like function calling and vision (model-dependent)
* - Has specific rate limiting and token usage patterns
*
* @author Jan-Marlon Leibl
* @version 1.0.0
* @see https://docs.anthropic.com/claude/reference/
*/ */
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from '@anthropic-ai/sdk';
@ -15,48 +35,168 @@ import type {
import { BaseAIProvider } from './base.js'; import { BaseAIProvider } from './base.js';
import { AIProviderError, AIErrorType } from '../types/index.js'; import { AIProviderError, AIErrorType } from '../types/index.js';
// ============================================================================
// TYPES AND INTERFACES
// ============================================================================
/** /**
* Configuration specific to Claude provider * Configuration interface for Claude provider with Anthropic-specific options.
*
* @example
* ```typescript
* const config: ClaudeConfig = {
* apiKey: process.env.ANTHROPIC_API_KEY!,
* defaultModel: 'claude-3-5-sonnet-20241022',
* version: '2023-06-01',
* timeout: 60000,
* maxRetries: 3
* };
* ```
*/ */
export interface ClaudeConfig extends AIProviderConfig { export interface ClaudeConfig extends AIProviderConfig {
/** Default model to use if not specified in requests (default: claude-3-5-sonnet-20241022) */ /**
* Default Claude model to use for requests.
*
* Recommended models:
* - 'claude-3-5-sonnet-20241022': Best balance of capability and speed
* - 'claude-3-5-haiku-20241022': Fastest responses, good for simple tasks
* - 'claude-3-opus-20240229': Most capable, best for complex reasoning
*
* @default 'claude-3-5-sonnet-20241022'
*/
defaultModel?: string; defaultModel?: string;
/** Anthropic API version (default: 2023-06-01) */
/**
* Anthropic API version header value.
*
* This ensures compatibility with specific API features and response formats.
* Check Anthropic's documentation for the latest version.
*
* @default '2023-06-01'
* @see https://docs.anthropic.com/claude/reference/versioning
*/
version?: string; version?: string;
} }
/** /**
* Claude AI provider implementation * Internal interface for processed message structure.
* Claude requires system messages to be separated from conversation flow.
*/
interface ProcessedMessages {
/** Combined system message content (null if no system messages) */
system: string | null;
/** Conversation messages (user/assistant only) */
messages: AIMessage[];
}
// ============================================================================
// CLAUDE PROVIDER IMPLEMENTATION
// ============================================================================
/**
* Anthropic Claude AI provider implementation.
*
* This class handles all interactions with Anthropic's Claude models through
* their official TypeScript SDK. It provides optimized handling of Claude's
* unique features like separate system message processing and advanced
* streaming capabilities.
*
* Usage Pattern:
* 1. Create instance with configuration
* 2. Call initialize() to set up client and validate connection
* 3. Use complete() or stream() for text generation
* 4. Handle any AIProviderError exceptions appropriately
*
* @example
* ```typescript
* const claude = new ClaudeProvider({
* apiKey: process.env.ANTHROPIC_API_KEY!,
* defaultModel: 'claude-3-5-sonnet-20241022'
* });
*
* await claude.initialize();
*
* const response = await claude.complete({
* messages: [
* { role: 'system', content: 'You are a helpful assistant.' },
* { role: 'user', content: 'Explain quantum computing.' }
* ],
* maxTokens: 1000,
* temperature: 0.7
* });
* ```
*/ */
export class ClaudeProvider extends BaseAIProvider { export class ClaudeProvider extends BaseAIProvider {
// ========================================================================
// INSTANCE PROPERTIES
// ========================================================================
/** Anthropic SDK client instance (initialized during doInitialize) */
private client: Anthropic | null = null; private client: Anthropic | null = null;
/** Default model identifier for requests */
private readonly defaultModel: string; private readonly defaultModel: string;
/** API version for Anthropic requests */
private readonly version: string; private readonly version: string;
constructor(config: ClaudeConfig) { // ========================================================================
super(config); // CONSTRUCTOR
this.defaultModel = config.defaultModel || 'claude-3-5-sonnet-20241022'; // ========================================================================
this.version = config.version || '2023-06-01';
}
/** /**
* Initialize the Claude provider by setting up the Anthropic client * Creates a new Claude provider instance.
*
* @param config - Claude-specific configuration options
* @throws {AIProviderError} If configuration validation fails
*/
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';
// Validate model name format
this.validateModelName(this.defaultModel);
}
// ========================================================================
// PROTECTED TEMPLATE METHOD IMPLEMENTATIONS
// ========================================================================
/**
* Initializes the Claude provider by setting up the Anthropic client.
*
* This method:
* 1. Creates the Anthropic SDK client with configuration
* 2. Tests the connection with a minimal API call
* 3. Validates API key permissions and model access
*
* @protected
* @throws {Error} If client creation or connection validation fails
*/ */
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,
timeout: this.config.timeout, timeout: this.config.timeout,
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
} }
}); });
// Test the connection by making a simple request // Validate connection and permissions
await this.validateConnection(); await this.validateConnection();
} catch (error) { } catch (error) {
// Clean up on failure
this.client = null;
throw new AIProviderError( throw new AIProviderError(
`Failed to initialize Claude provider: ${(error as Error).message}`, `Failed to initialize Claude provider: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
@ -67,47 +207,247 @@ export class ClaudeProvider extends BaseAIProvider {
} }
/** /**
* Generate a completion using Claude * Generates a text completion using Claude's message API.
*
* This method:
* 1. Processes messages to separate system content
* 2. Builds optimized request parameters
* 3. Makes API call with error handling
* 4. Formats response to standard interface
*
* @protected
* @param params - Validated completion parameters
* @returns Promise resolving to formatted completion response
* @throws {Error} If API request fails
*/ */
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> { protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
if (!this.client) { if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
const { system, messages } = this.convertMessages(params.messages); // Process messages for Claude's format requirements
const { system, messages } = this.processMessages(params.messages);
const response = await this.client.messages.create({ // Build optimized request parameters
model: params.model || this.defaultModel, const requestParams = this.buildRequestParams(params, system, messages, false);
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop_sequences: params.stopSequences,
system: system || undefined,
messages: messages.map(msg => ({
role: msg.role as 'user' | 'assistant',
content: msg.content
}))
});
// Make API request
const response = await this.client.messages.create(requestParams);
// Format and return response
return this.formatCompletionResponse(response); return this.formatCompletionResponse(response);
} catch (error) { } catch (error) {
throw this.handleAnthropicError(error as Error); throw this.handleAnthropicError(error as Error);
} }
} }
/** /**
* Generate a streaming completion using Claude * Generates a streaming text completion using Claude's streaming API.
*
* This method:
* 1. Processes messages and builds streaming request
* 2. Handles real-time stream chunks from Anthropic
* 3. Yields formatted chunks with proper completion tracking
* 4. Provides final usage statistics when stream completes
*
* @protected
* @param params - Validated completion parameters
* @returns AsyncIterable yielding completion chunks
* @throws {Error} If streaming request fails
*/ */
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> { protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
if (!this.client) { if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
const { system, messages } = this.convertMessages(params.messages); // Process messages for Claude's format requirements
const { system, messages } = this.processMessages(params.messages);
const stream = await this.client.messages.create({ // 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);
}
}
// ========================================================================
// PUBLIC INTERFACE METHODS
// ========================================================================
/**
* Returns comprehensive information about the Claude provider.
*
* @returns Provider information including models, capabilities, and limits
*/
public getInfo(): ProviderInfo {
return {
name: 'Claude',
version: '1.0.0',
models: [
'claude-3-5-sonnet-20241022', // Latest and most balanced
'claude-3-5-haiku-20241022', // Fastest response times
'claude-3-opus-20240229', // Most capable reasoning
'claude-3-sonnet-20240229', // Previous generation balanced
'claude-3-haiku-20240307' // Previous generation fast
],
maxContextLength: 200000, // 200K tokens for most models
supportsStreaming: true,
capabilities: {
vision: true, // Claude 3+ supports image analysis
functionCalling: true, // Tool use capability
systemMessages: true, // Native system message support
reasoning: true, // Advanced reasoning capabilities
codeGeneration: true, // Excellent at code tasks
analysis: true // Strong analytical capabilities
}
};
}
// ========================================================================
// PRIVATE UTILITY METHODS
// ========================================================================
/**
* Validates the connection by making a minimal test request.
*
* @private
* @throws {AIProviderError} If connection validation fails
*/
private async validateConnection(): Promise<void> {
if (!this.client) {
throw new Error('Client not initialized');
}
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' }]
});
} 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);
}
}
/**
* 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
);
}
// 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.`);
}
}
/**
* Processes messages to separate system content from conversation.
*
* Claude requires system messages to be passed separately from the
* conversation messages array. This method handles that transformation.
*
* @private
* @param messages - Input messages array
* @returns Processed messages with separated system content
*/
private processMessages(messages: AIMessage[]): ProcessedMessages {
let systemContent: string | null = null;
const conversationMessages: AIMessage[] = [];
for (const message of messages) {
if (message.role === 'system') {
// Combine multiple system messages with double newlines
if (systemContent) {
systemContent += '\n\n' + message.content;
} else {
systemContent = message.content;
}
} else {
// Only user and assistant messages go in conversation
conversationMessages.push(message);
}
}
// Validate conversation structure
if (conversationMessages.length === 0) {
throw new AIProviderError(
'At least one user or assistant message is required',
AIErrorType.INVALID_REQUEST
);
}
return {
system: systemContent,
messages: conversationMessages
};
}
/**
* Builds optimized request parameters for Anthropic API.
*
* @private
* @param params - Input completion parameters
* @param system - Processed system content
* @param messages - Processed conversation messages
* @param stream - Whether to enable streaming
* @returns Formatted request parameters
*/
private buildRequestParams(
params: CompletionParams,
system: string | null,
messages: AIMessage[],
stream: boolean
) {
return {
model: params.model || this.defaultModel, model: params.model || this.defaultModel,
max_tokens: params.maxTokens || 1000, max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7, temperature: params.temperature ?? 0.7,
@ -118,202 +458,220 @@ export class ClaudeProvider extends BaseAIProvider {
role: msg.role as 'user' | 'assistant', role: msg.role as 'user' | 'assistant',
content: msg.content content: msg.content
})), })),
stream: true stream
}); };
}
let content = ''; /**
* Processes streaming response chunks from Anthropic API.
*
* @private
* @param stream - Anthropic streaming response
* @returns AsyncIterable of formatted completion chunks
*/
private async *processStreamChunks(stream: any): AsyncIterable<CompletionChunk> {
let messageId = ''; let messageId = '';
let totalInputTokens = 0;
let totalOutputTokens = 0;
try {
for await (const chunk of stream) { for await (const chunk of stream) {
if (chunk.type === 'message_start') { // Handle different chunk types
switch (chunk.type) {
case 'message_start':
messageId = chunk.message.id; messageId = chunk.message.id;
} else if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') { totalInputTokens = chunk.message.usage?.input_tokens || 0;
content += chunk.delta.text; break;
case 'content_block_delta':
if (chunk.delta?.type === 'text_delta') {
yield { yield {
content: chunk.delta.text, content: chunk.delta.text,
isComplete: false, isComplete: false,
id: messageId id: messageId
}; };
} else if (chunk.type === 'message_delta' && chunk.usage) { }
// Final chunk with usage information break;
case 'message_delta':
if (chunk.usage?.output_tokens) {
totalOutputTokens = chunk.usage.output_tokens;
}
break;
case 'message_stop':
// Final chunk with complete usage information
yield { yield {
content: '', content: '',
isComplete: true, isComplete: true,
id: messageId, id: messageId,
usage: { usage: {
promptTokens: chunk.usage.input_tokens || 0, promptTokens: totalInputTokens,
completionTokens: chunk.usage.output_tokens || 0, completionTokens: totalOutputTokens,
totalTokens: (chunk.usage.input_tokens || 0) + (chunk.usage.output_tokens || 0) totalTokens: totalInputTokens + totalOutputTokens
} }
}; };
return;
} }
} }
} catch (error) { } catch (error) {
throw this.handleAnthropicError(error as Error); // Handle streaming errors gracefully
}
}
/**
* Get information about the Claude provider
*/
public getInfo(): ProviderInfo {
return {
name: 'Claude',
version: '1.0.0',
models: [
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
],
maxContextLength: 200000, // Claude 3.5 Sonnet context length
supportsStreaming: true,
capabilities: {
vision: true,
functionCalling: true,
systemMessages: true
}
};
}
/**
* Validate the connection by making a simple request
*/
private async validateConnection(): Promise<void> {
if (!this.client) {
throw new Error('Client not initialized');
}
try {
// Make a minimal request to validate credentials
await this.client.messages.create({
model: this.defaultModel,
max_tokens: 1,
messages: [{ role: 'user', content: 'Hi' }]
});
} catch (error: any) {
if (error.status === 401 || error.status === 403) {
throw new AIProviderError( throw new AIProviderError(
'Invalid API key. Please check your Anthropic API key.', `Streaming interrupted: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION, AIErrorType.NETWORK,
error.status undefined,
error as Error
); );
} }
// For other errors during validation, we'll let initialization proceed
// as they might be temporary issues
}
} }
/** /**
* Convert our generic message format to Claude's format * Formats Anthropic's completion response to our standard interface.
* Claude requires system messages to be separate from the conversation *
*/ * @private
private convertMessages(messages: AIMessage[]): { system: string | null; messages: AIMessage[] } { * @param response - Raw Anthropic API response
let system: string | null = null; * @returns Formatted completion response
const conversationMessages: AIMessage[] = []; * @throws {AIProviderError} If response format is unexpected
for (const message of messages) {
if (message.role === 'system') {
// Combine multiple system messages
if (system) {
system += '\n\n' + message.content;
} else {
system = message.content;
}
} else {
conversationMessages.push(message);
}
}
return { system, messages: conversationMessages };
}
/**
* Format Anthropic's response to our standard format
*/ */
private formatCompletionResponse(response: any): CompletionResponse { private formatCompletionResponse(response: any): CompletionResponse {
// Extract text content from response blocks
const content = response.content const content = response.content
.filter((block: any) => block.type === 'text') ?.filter((block: any) => block.type === 'text')
.map((block: any) => block.text) ?.map((block: any) => block.text)
.join(''); ?.join('') || '';
if (!content) {
throw new AIProviderError(
'No text content found in Claude response',
AIErrorType.UNKNOWN
);
}
return { return {
content, content,
model: response.model, model: response.model,
usage: { usage: {
promptTokens: response.usage.input_tokens, promptTokens: response.usage?.input_tokens || 0,
completionTokens: response.usage.output_tokens, completionTokens: response.usage?.output_tokens || 0,
totalTokens: response.usage.input_tokens + response.usage.output_tokens totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0)
}, },
id: response.id, id: response.id,
metadata: { metadata: {
stopReason: response.stop_reason, stopReason: response.stop_reason,
stopSequence: response.stop_sequence stopSequence: response.stop_sequence || null,
created: Date.now() // Anthropic doesn't provide timestamp
} }
}; };
} }
/** /**
* Handle Anthropic-specific errors and convert them to our standard format * 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 { private handleAnthropicError(error: any): AIProviderError {
if (error instanceof AIProviderError) { if (error instanceof AIProviderError) {
return error; return error;
} }
const status = error.status || error.statusCode;
const message = error.message || 'Unknown Anthropic API error'; const message = error.message || 'Unknown Anthropic API error';
const status = error.status || error.statusCode;
// Map Anthropic-specific error codes
switch (status) { switch (status) {
case 400: 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( return new AIProviderError(
`Invalid request: ${message}`, `Invalid request: ${message}`,
AIErrorType.INVALID_REQUEST, AIErrorType.INVALID_REQUEST,
status, status,
error error
); );
case 401: case 401:
return new AIProviderError( return new AIProviderError(
'Authentication failed. Please check your Anthropic API key.', 'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
status, status,
error error
); );
case 403: case 403:
return new AIProviderError( return new AIProviderError(
'Access forbidden. Please check your API key permissions.', 'Access forbidden. Your API key may not have permission for this model or feature.',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
status, status,
error error
); );
case 404: case 404:
return new AIProviderError( return new AIProviderError(
'Model not found. Please check the model name.', 'Model not found. The specified Claude model may not be available to your account.',
AIErrorType.MODEL_NOT_FOUND, AIErrorType.MODEL_NOT_FOUND,
status, status,
error error
); );
case 429: case 429:
return new AIProviderError( return new AIProviderError(
'Rate limit exceeded. Please slow down your requests.', 'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.',
AIErrorType.RATE_LIMIT, AIErrorType.RATE_LIMIT,
status, status,
error error
); );
case 500: case 500:
case 502: case 502:
case 503: case 503:
case 504:
return new AIProviderError( return new AIProviderError(
'Anthropic service temporarily unavailable. Please try again later.', 'Anthropic service temporarily unavailable. Please try again in a few moments.',
AIErrorType.NETWORK, AIErrorType.NETWORK,
status, status,
error error
); );
default: default:
// Handle timeout and network errors
if (message.includes('timeout') || error.code === 'ETIMEDOUT') {
return new AIProviderError( return new AIProviderError(
`Anthropic API error: ${message}`, '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, AIErrorType.UNKNOWN,
status, status,
error error

View File

@ -1,10 +1,32 @@
/** /**
* Gemini Provider implementation using Google's Generative AI API * Google Gemini Provider Implementation
* Provides integration with Gemini models through a standardized interface *
* This module provides integration with Google's Gemini models through the official
* Generative AI SDK. Gemini offers cutting-edge AI capabilities including multimodal
* understanding, advanced reasoning, and efficient text generation across various tasks.
*
* Key Features:
* - Support for all Gemini model variants (Gemini 1.5 Pro, Flash, Pro Vision)
* - Advanced streaming support with real-time token delivery
* - Native multimodal capabilities (text, images, video)
* - Sophisticated safety settings and content filtering
* - Flexible generation configuration with fine-grained control
*
* Gemini-Specific Considerations:
* - Uses "candidates" for response variants and "usageMetadata" for token counts
* - Supports system instructions as separate parameter (not in conversation)
* - Has sophisticated safety filtering with customizable thresholds
* - Provides extensive generation configuration options
* - Supports both single-turn and multi-turn conversations
* - Offers advanced reasoning and coding capabilities
*
* @author Jan-Marlon Leibl
* @version 1.0.0
* @see https://ai.google.dev/docs
*/ */
import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai'; import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai';
import type { Content, Part } from '@google/generative-ai'; import type { Content, Part, GenerationConfig, SafetySetting } from '@google/generative-ai';
import type { import type {
AIProviderConfig, AIProviderConfig,
CompletionParams, CompletionParams,
@ -16,56 +38,213 @@ import type {
import { BaseAIProvider } from './base.js'; import { BaseAIProvider } from './base.js';
import { AIProviderError, AIErrorType } from '../types/index.js'; import { AIProviderError, AIErrorType } from '../types/index.js';
// ============================================================================
// TYPES AND INTERFACES
// ============================================================================
/** /**
* Configuration specific to Gemini provider * Configuration interface for Gemini provider with Google-specific options.
*
* @example
* ```typescript
* const config: GeminiConfig = {
* apiKey: process.env.GOOGLE_API_KEY!,
* defaultModel: 'gemini-1.5-pro',
* safetySettings: [
* {
* category: HarmCategory.HARM_CATEGORY_HARASSMENT,
* threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
* }
* ],
* generationConfig: {
* temperature: 0.7,
* topP: 0.8,
* topK: 40,
* maxOutputTokens: 2048
* }
* };
* ```
*/ */
export interface GeminiConfig extends AIProviderConfig { export interface GeminiConfig extends AIProviderConfig {
/** Default model to use if not specified in requests (default: gemini-1.5-flash) */ /**
* Default Gemini model to use for requests.
*
* Recommended models:
* - 'gemini-1.5-pro': Flagship model, best overall performance and multimodal
* - 'gemini-1.5-flash': Faster and cheaper, good for simple tasks
* - 'gemini-1.0-pro': Previous generation, cost-effective
* - 'gemini-pro-vision': Specialized for vision tasks (legacy)
*
* @default 'gemini-1.5-flash'
*/
defaultModel?: string; defaultModel?: string;
/** Safety settings for content filtering */
safetySettings?: any[]; /**
/** Generation configuration */ * Safety settings for content filtering and harm prevention.
*
* Gemini includes built-in safety filtering across multiple categories:
* - Harassment and bullying
* - Hate speech and discrimination
* - Sexually explicit content
* - Dangerous or harmful activities
*
* Each category can be configured with different blocking thresholds.
*
* @see https://ai.google.dev/docs/safety_setting
*/
safetySettings?: SafetySetting[];
/**
* Generation configuration for controlling output characteristics.
*
* This allows fine-tuned control over the generation process including
* creativity, diversity, length, and stopping conditions.
*
* @see https://ai.google.dev/docs/concepts#generation_configuration
*/
generationConfig?: { generationConfig?: {
/** Controls randomness in generation (0.0 to 1.0) */
temperature?: number; temperature?: number;
/** Controls nucleus sampling for diversity (0.0 to 1.0) */
topP?: number; topP?: number;
/** Controls top-k sampling for diversity (positive integer) */
topK?: number; topK?: number;
/** Maximum number of tokens to generate */
maxOutputTokens?: number; maxOutputTokens?: number;
/** Sequences that will stop generation when encountered */
stopSequences?: string[]; stopSequences?: string[];
}; };
} }
/** /**
* Gemini provider implementation * Processed messages structure for Gemini's conversation format.
* Separates system instructions from conversational content.
*/
interface ProcessedMessages {
/** System instruction for model behavior (if any) */
systemInstruction?: string;
/** Conversation content in Gemini's format */
contents: Content[];
}
// ============================================================================
// GEMINI PROVIDER IMPLEMENTATION
// ============================================================================
/**
* Google Gemini provider implementation.
*
* This class handles all interactions with Google's Gemini models through their
* official Generative AI SDK. It provides optimized handling of Gemini's unique
* features including multimodal inputs, safety filtering, and advanced generation control.
*
* Usage Pattern:
* 1. Create instance with Google API key and configuration
* 2. Call initialize() to set up client and validate credentials
* 3. Use complete() or stream() for text generation
* 4. Handle any AIProviderError exceptions appropriately
*
* @example
* ```typescript
* const gemini = new GeminiProvider({
* apiKey: process.env.GOOGLE_API_KEY!,
* defaultModel: 'gemini-1.5-pro',
* generationConfig: {
* temperature: 0.7,
* maxOutputTokens: 2048
* }
* });
*
* await gemini.initialize();
*
* const response = await gemini.complete({
* messages: [
* { role: 'system', content: 'You are a helpful research assistant.' },
* { role: 'user', content: 'Explain quantum computing.' }
* ],
* maxTokens: 1000,
* temperature: 0.8
* });
* ```
*/ */
export class GeminiProvider extends BaseAIProvider { export class GeminiProvider extends BaseAIProvider {
// ========================================================================
// INSTANCE PROPERTIES
// ========================================================================
/** Google Generative AI client instance (initialized during doInitialize) */
private client: GoogleGenerativeAI | null = null; private client: GoogleGenerativeAI | null = null;
/** Default model instance for requests */
private model: GenerativeModel | null = null; private model: GenerativeModel | null = null;
/** Default model identifier for requests */
private readonly defaultModel: string; private readonly defaultModel: string;
private readonly safetySettings?: any[];
/** Safety settings for content filtering */
private readonly safetySettings?: SafetySetting[];
/** Generation configuration defaults */
private readonly generationConfig?: any; private readonly generationConfig?: any;
// ========================================================================
// CONSTRUCTOR
// ========================================================================
/**
* Creates a new Gemini provider instance.
*
* @param config - Gemini-specific configuration options
* @throws {AIProviderError} If configuration validation fails
*/
constructor(config: GeminiConfig) { constructor(config: GeminiConfig) {
super(config); super(config);
// Set Gemini-specific defaults
this.defaultModel = config.defaultModel || 'gemini-1.5-flash'; this.defaultModel = config.defaultModel || 'gemini-1.5-flash';
this.safetySettings = config.safetySettings; this.safetySettings = config.safetySettings;
this.generationConfig = config.generationConfig; this.generationConfig = config.generationConfig;
// Validate model name format
this.validateModelName(this.defaultModel);
} }
// ========================================================================
// PROTECTED TEMPLATE METHOD IMPLEMENTATIONS
// ========================================================================
/** /**
* Initialize the Gemini provider by setting up the Google Generative AI client * Initializes the Gemini provider by setting up the client and model.
*
* This method:
* 1. Creates the Google Generative AI client with API key
* 2. Sets up the default model with safety and generation settings
* 3. Tests the connection with a minimal API call
* 4. Validates API key permissions and model access
*
* @protected
* @throws {Error} If client creation or connection validation fails
*/ */
protected async doInitialize(): Promise<void> { protected async doInitialize(): Promise<void> {
try { try {
// Create Google Generative AI client
this.client = new GoogleGenerativeAI(this.config.apiKey); this.client = new GoogleGenerativeAI(this.config.apiKey);
// Set up default model with configuration
this.model = this.client.getGenerativeModel({ this.model = this.client.getGenerativeModel({
model: this.defaultModel, model: this.defaultModel,
safetySettings: this.safetySettings, safetySettings: this.safetySettings,
generationConfig: this.generationConfig generationConfig: this.generationConfig
}); });
// Test the connection by making a simple request // Validate connection and permissions
await this.validateConnection(); await this.validateConnection();
} catch (error) { } catch (error) {
// Clean up on failure
this.client = null;
this.model = null;
throw new AIProviderError( throw new AIProviderError(
`Failed to initialize Gemini provider: ${(error as Error).message}`, `Failed to initialize Gemini provider: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
@ -76,26 +255,30 @@ export class GeminiProvider extends BaseAIProvider {
} }
/** /**
* Generate a completion using Gemini * Generates a text completion using Gemini's generation API.
*
* This method:
* 1. Converts messages to Gemini's format with system instructions
* 2. Chooses between single-turn and multi-turn conversation modes
* 3. Makes API call with comprehensive error handling
* 4. Formats response to standard interface
*
* @protected
* @param params - Validated completion parameters
* @returns Promise resolving to formatted completion response
* @throws {Error} If API request fails
*/ */
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> { protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
if (!this.client || !this.model) { if (!this.client || !this.model) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
// Get the model for this request (might be different from default) // Get the model for this request (might be different from default)
const model = params.model && params.model !== this.defaultModel const model = this.getModelForRequest(params);
? this.client.getGenerativeModel({
model: params.model,
safetySettings: this.safetySettings,
generationConfig: this.buildGenerationConfig(params)
})
: this.model;
const { systemInstruction, contents } = this.convertMessages(params.messages); const { systemInstruction, contents } = this.convertMessages(params.messages);
// Create chat session or use generateContent // Choose appropriate generation method based on conversation length
if (contents.length > 1) { if (contents.length > 1) {
// Multi-turn conversation - use chat session // Multi-turn conversation - use chat session
const chat = model.startChat({ const chat = model.startChat({
@ -108,9 +291,10 @@ export class GeminiProvider extends BaseAIProvider {
if (!lastMessage) { if (!lastMessage) {
throw new AIProviderError('No valid messages provided', AIErrorType.INVALID_REQUEST); throw new AIProviderError('No valid messages provided', AIErrorType.INVALID_REQUEST);
} }
const result = await chat.sendMessage(lastMessage.parts);
const result = await chat.sendMessage(lastMessage.parts);
return this.formatCompletionResponse(result.response, params.model || this.defaultModel); return this.formatCompletionResponse(result.response, params.model || this.defaultModel);
} else { } else {
// Single message - use generateContent // Single message - use generateContent
const result = await model.generateContent({ const result = await model.generateContent({
@ -127,25 +311,30 @@ export class GeminiProvider extends BaseAIProvider {
} }
/** /**
* Generate a streaming completion using Gemini * Generates a streaming text completion using Gemini's streaming API.
*
* This method:
* 1. Sets up appropriate streaming mode based on conversation type
* 2. Handles real-time stream chunks from Gemini
* 3. Tracks token usage throughout the stream
* 4. Yields formatted chunks with proper completion tracking
*
* @protected
* @param params - Validated completion parameters
* @returns AsyncIterable yielding completion chunks
* @throws {Error} If streaming request fails
*/ */
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> { protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
if (!this.client || !this.model) { if (!this.client || !this.model) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('Gemini client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
// Get the model for this request // Get the model for this request
const model = params.model && params.model !== this.defaultModel const model = this.getModelForRequest(params);
? this.client.getGenerativeModel({
model: params.model,
safetySettings: this.safetySettings,
generationConfig: this.buildGenerationConfig(params)
})
: this.model;
const { systemInstruction, contents } = this.convertMessages(params.messages); const { systemInstruction, contents } = this.convertMessages(params.messages);
// Set up streaming based on conversation type
let stream; let stream;
if (contents.length > 1) { if (contents.length > 1) {
// Multi-turn conversation // Multi-turn conversation
@ -169,9 +358,221 @@ export class GeminiProvider extends BaseAIProvider {
}); });
} }
// Process stream chunks
yield* this.processStreamChunks(stream);
} catch (error) {
throw this.handleGeminiError(error as Error);
}
}
// ========================================================================
// PUBLIC INTERFACE METHODS
// ========================================================================
/**
* Returns comprehensive information about the Gemini provider.
*
* @returns Provider information including models, capabilities, and limits
*/
public getInfo(): ProviderInfo {
return {
name: 'Gemini',
version: '1.0.0',
models: [
'gemini-1.5-pro', // Latest flagship model
'gemini-1.5-flash', // Fast and efficient variant
'gemini-1.0-pro', // Previous generation
'gemini-pro-vision', // Vision-specialized (legacy)
'gemini-1.5-pro-vision', // Latest vision model
'gemini-1.0-pro-latest', // Latest 1.0 variant
'gemini-1.5-pro-latest' // Latest 1.5 variant
],
maxContextLength: 1048576, // ~1M tokens for Gemini 1.5
supportsStreaming: true,
capabilities: {
vision: true, // Advanced multimodal capabilities
functionCalling: true, // Tool use and function calling
jsonMode: true, // Structured JSON output
systemMessages: true, // System instructions support
reasoning: true, // Strong reasoning capabilities
codeGeneration: true, // Excellent at programming tasks
multimodal: true, // Text, image, video understanding
safetyFiltering: true // Built-in content safety
}
};
}
// ========================================================================
// PRIVATE UTILITY METHODS
// ========================================================================
/**
* Validates the connection by making a minimal test request.
*
* @private
* @throws {AIProviderError} If connection validation fails
*/
private async validateConnection(): Promise<void> {
if (!this.model) {
throw new Error('Model not initialized');
}
try {
// Make minimal request to test connection and permissions
await this.model.generateContent({
contents: [{ role: 'user', parts: [{ text: 'Hi' }] }],
generationConfig: { maxOutputTokens: 1 }
});
} catch (error: any) {
// Handle specific validation errors
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
);
}
// For other errors during validation, log but don't fail initialization
console.warn('Gemini connection validation warning:', error.message);
}
}
/**
* Validates model name format for Gemini 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
);
}
// Gemini model names follow specific patterns
const validPatterns = [
/^gemini-1\.5-pro(?:-latest|-vision)?$/, // e.g., gemini-1.5-pro, gemini-1.5-pro-latest
/^gemini-1\.5-flash(?:-latest)?$/, // e.g., gemini-1.5-flash, gemini-1.5-flash-latest
/^gemini-1\.0-pro(?:-latest|-vision)?$/, // e.g., gemini-1.0-pro, gemini-pro-vision
/^gemini-pro(?:-vision)?$/, // Legacy names: gemini-pro, gemini-pro-vision
/^models\/gemini-.+$/ // Full model path format
];
const isValid = validPatterns.some(pattern => pattern.test(modelName));
if (!isValid) {
console.warn(`Model name '${modelName}' doesn't match expected Gemini naming patterns. This may cause API errors.`);
}
}
/**
* Gets the appropriate model instance for a request.
*
* @private
* @param params - Completion parameters
* @returns GenerativeModel instance
*/
private getModelForRequest(params: CompletionParams): GenerativeModel {
if (!this.client) {
throw new Error('Client not initialized');
}
// Use default model if no specific model requested
if (!params.model || params.model === this.defaultModel) {
return this.model!;
}
// Create new model instance for different model
return this.client.getGenerativeModel({
model: params.model,
safetySettings: this.safetySettings,
generationConfig: this.buildGenerationConfig(params)
});
}
/**
* Converts generic messages to Gemini's conversation format.
*
* Gemini handles system messages as separate system instructions
* rather than part of the conversation flow.
*
* @private
* @param messages - Input messages array
* @returns Processed messages with system instruction separated
*/
private convertMessages(messages: AIMessage[]): ProcessedMessages {
let systemInstruction: string | undefined;
const contents: Content[] = [];
for (const message of messages) {
if (message.role === 'system') {
// Combine multiple system messages
systemInstruction = systemInstruction
? `${systemInstruction}\n\n${message.content}`
: message.content;
} else {
contents.push({
role: message.role === 'assistant' ? 'model' : 'user',
parts: [{ text: message.content }]
});
}
}
return { systemInstruction, contents };
}
/**
* Builds generation configuration from completion parameters.
*
* @private
* @param params - Completion parameters
* @returns Gemini generation configuration
*/
private buildGenerationConfig(params: CompletionParams): GenerationConfig {
return {
temperature: params.temperature ?? this.generationConfig?.temperature ?? 0.7,
topP: params.topP ?? this.generationConfig?.topP,
topK: this.generationConfig?.topK,
maxOutputTokens: params.maxTokens ?? this.generationConfig?.maxOutputTokens ?? 1000,
stopSequences: params.stopSequences ?? this.generationConfig?.stopSequences
};
}
/**
* Processes streaming response chunks from Gemini API.
*
* @private
* @param stream - Gemini streaming response
* @returns AsyncIterable of formatted completion chunks
*/
private async *processStreamChunks(stream: any): AsyncIterable<CompletionChunk> {
let fullText = ''; let fullText = '';
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
// Process streaming chunks
for await (const chunk of stream.stream) { for await (const chunk of stream.stream) {
const chunkText = chunk.text(); const chunkText = chunk.text();
fullText += chunkText; fullText += chunkText;
@ -183,7 +584,7 @@ export class GeminiProvider extends BaseAIProvider {
}; };
} }
// Final chunk with usage info // Final chunk with usage information
const finalResponse = await stream.response; const finalResponse = await stream.response;
const usageMetadata = finalResponse.usageMetadata; const usageMetadata = finalResponse.usageMetadata;
@ -198,128 +599,64 @@ export class GeminiProvider extends BaseAIProvider {
} }
}; };
} catch (error) { } catch (error) {
throw this.handleGeminiError(error as Error);
}
}
/**
* Get information about the Gemini provider
*/
public getInfo(): ProviderInfo {
return {
name: 'Gemini',
version: '1.0.0',
models: [
'gemini-1.5-flash',
'gemini-1.5-flash-8b',
'gemini-1.5-pro',
'gemini-1.0-pro',
'gemini-1.0-pro-vision'
],
maxContextLength: 1000000, // Gemini 1.5 context length
supportsStreaming: true,
capabilities: {
vision: true,
functionCalling: true,
systemMessages: true,
multimodal: true,
largeContext: true
}
};
}
/**
* Validate the connection by making a simple request
*/
private async validateConnection(): Promise<void> {
if (!this.model) {
throw new Error('Model not initialized');
}
try {
// Make a minimal request to validate credentials
await this.model.generateContent('Hi');
} catch (error: any) {
if (error.message?.includes('API key') || error.message?.includes('authentication')) {
throw new AIProviderError( throw new AIProviderError(
'Invalid API key. Please check your Google AI API key.', `Streaming interrupted: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION AIErrorType.NETWORK,
undefined,
error as Error
); );
} }
// For other errors during validation, we'll let initialization proceed
// as they might be temporary issues
}
} }
/** /**
* Convert our generic message format to Gemini's format * Formats Gemini's response to our standard interface.
* Gemini uses Contents with Parts and supports system instructions separately *
*/ * @private
private convertMessages(messages: AIMessage[]): { systemInstruction?: string; contents: Content[] } { * @param response - Raw Gemini API response
let systemInstruction: string | undefined; * @param model - Model used for generation
const contents: Content[] = []; * @returns Formatted completion response
* @throws {AIProviderError} If response format is unexpected
for (const message of messages) {
if (message.role === 'system') {
// Combine multiple system messages
if (systemInstruction) {
systemInstruction += '\n\n' + message.content;
} else {
systemInstruction = message.content;
}
} else {
contents.push({
role: message.role === 'assistant' ? 'model' : 'user',
parts: [{ text: message.content }]
});
}
}
return { systemInstruction, contents };
}
/**
* Build generation config from completion parameters
*/
private buildGenerationConfig(params: CompletionParams) {
return {
temperature: params.temperature ?? 0.7,
topP: params.topP,
maxOutputTokens: params.maxTokens || 1000,
stopSequences: params.stopSequences,
...this.generationConfig
};
}
/**
* Format Gemini's response to our standard format
*/ */
private formatCompletionResponse(response: any, model: string): CompletionResponse { private formatCompletionResponse(response: any, model: string): CompletionResponse {
// Handle multiple text parts in the response
const candidate = response.candidates?.[0]; const candidate = response.candidates?.[0];
if (!candidate || !candidate.content?.parts?.[0]?.text) { if (!candidate) {
throw new AIProviderError( throw new AIProviderError(
'No content in Gemini response', 'No candidates found in Gemini response',
AIErrorType.UNKNOWN AIErrorType.UNKNOWN
); );
} }
const content = candidate.content.parts const content = candidate.content;
.filter((part: Part) => part.text) if (!content || !content.parts) {
.map((part: Part) => part.text) throw new AIProviderError(
'No content found in Gemini response',
AIErrorType.UNKNOWN
);
}
// Combine all text parts
const text = content.parts
.filter((part: any) => part.text)
.map((part: any) => part.text)
.join(''); .join('');
const usageMetadata = response.usageMetadata; if (!text) {
const requestId = `gemini-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; throw new AIProviderError(
'No text content found in Gemini response',
AIErrorType.UNKNOWN
);
}
return { return {
content, content: text,
model, model: model,
usage: { usage: {
promptTokens: usageMetadata?.promptTokenCount || 0, promptTokens: response.usageMetadata?.promptTokenCount || 0,
completionTokens: usageMetadata?.candidatesTokenCount || 0, completionTokens: response.usageMetadata?.candidatesTokenCount || 0,
totalTokens: usageMetadata?.totalTokenCount || 0 totalTokens: response.usageMetadata?.totalTokenCount || 0
}, },
id: requestId, id: `gemini-${Date.now()}`,
metadata: { metadata: {
finishReason: candidate.finishReason, finishReason: candidate.finishReason,
safetyRatings: candidate.safetyRatings, safetyRatings: candidate.safetyRatings,
@ -329,7 +666,14 @@ export class GeminiProvider extends BaseAIProvider {
} }
/** /**
* Handle Gemini-specific errors and convert them to our standard format * Handles and transforms Gemini-specific errors.
*
* This method maps Gemini's error responses to our standardized
* error format, providing helpful context and actionable suggestions.
*
* @private
* @param error - Original error from Gemini API
* @returns Normalized AIProviderError
*/ */
private handleGeminiError(error: any): AIProviderError { private handleGeminiError(error: any): AIProviderError {
if (error instanceof AIProviderError) { if (error instanceof AIProviderError) {
@ -337,67 +681,124 @@ export class GeminiProvider extends BaseAIProvider {
} }
const message = error.message || 'Unknown Gemini API error'; const message = error.message || 'Unknown Gemini API error';
const status = error.status || error.statusCode;
// Handle common Gemini error patterns // Map Gemini-specific error patterns
if (message.includes('API key')) { if (message.includes('API key')) {
return new AIProviderError( return new AIProviderError(
'Authentication failed. Please check your Google AI API key.', 'Invalid Google API key. Please verify your API key from https://aistudio.google.com/app/apikey',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
undefined, status,
error error
); );
} }
if (message.includes('quota') || message.includes('rate limit')) { if (message.includes('quota') || message.includes('limit exceeded')) {
return new AIProviderError( return new AIProviderError(
'Rate limit exceeded. Please slow down your requests.', 'API quota exceeded. Please check your Google Cloud quotas and billing.',
AIErrorType.RATE_LIMIT, AIErrorType.RATE_LIMIT,
undefined, status,
error error
); );
} }
if (message.includes('model') && message.includes('not found')) { if (message.includes('model not found') || message.includes('invalid model')) {
return new AIProviderError( return new AIProviderError(
'Model not found. Please check the model name.', 'Model not found. The specified Gemini model may not exist or be available to your API key.',
AIErrorType.MODEL_NOT_FOUND, AIErrorType.MODEL_NOT_FOUND,
undefined, status,
error error
); );
} }
if (message.includes('invalid') || message.includes('bad request')) { if (message.includes('safety') || message.includes('blocked')) {
return new AIProviderError( return new AIProviderError(
`Invalid request: ${message}`, 'Content blocked by safety filters. Please modify your input or adjust safety settings.',
AIErrorType.INVALID_REQUEST, AIErrorType.INVALID_REQUEST,
undefined, status,
error error
); );
} }
if (message.includes('network') || message.includes('connection')) { if (message.includes('length') || message.includes('token')) {
return new AIProviderError( return new AIProviderError(
'Network error occurred. Please check your connection.', 'Input too long or exceeds token limits. Please reduce input size or max tokens.',
AIErrorType.NETWORK, AIErrorType.INVALID_REQUEST,
undefined, status,
error error
); );
} }
if (message.includes('timeout')) { if (message.includes('timeout')) {
return new AIProviderError( return new AIProviderError(
'Request timed out. Please try again.', 'Request timed out. Gemini may be experiencing high load.',
AIErrorType.TIMEOUT, AIErrorType.TIMEOUT,
undefined, status,
error error
); );
} }
if (message.includes('network') || message.includes('connection')) {
return new AIProviderError( return new AIProviderError(
`Gemini API error: ${message}`, 'Network error connecting to Gemini servers.',
AIErrorType.UNKNOWN, AIErrorType.NETWORK,
undefined, status,
error error
); );
} }
// Handle HTTP status codes
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
);
}
}
} }

View File

@ -1,6 +1,27 @@
/** /**
* OpenAI Provider implementation using OpenAI's API * OpenAI GPT Provider Implementation
* Provides integration with GPT models through a standardized interface *
* This module provides integration with OpenAI's GPT models through their official SDK.
* OpenAI offers industry-leading language models with excellent performance across
* diverse tasks including reasoning, coding, creative writing, and function calling.
*
* Key Features:
* - Support for all GPT model variants (GPT-4, GPT-4o, GPT-3.5 Turbo)
* - Advanced streaming support with real-time token delivery
* - Native function calling and JSON mode capabilities
* - Vision support for image understanding (model-dependent)
* - Organization and project-level access control
*
* OpenAI-Specific Considerations:
* - Uses "prompt_tokens" and "completion_tokens" terminology
* - Supports system messages natively within conversation flow
* - Has sophisticated rate limiting and pricing tiers
* - Provides extensive model configuration options
* - Supports batch processing and fine-tuning capabilities
*
* @author Jan-Marlon Leibl
* @version 1.0.0
* @see https://platform.openai.com/docs/api-reference
*/ */
import OpenAI from 'openai'; import OpenAI from 'openai';
@ -15,51 +36,191 @@ import type {
import { BaseAIProvider } from './base.js'; import { BaseAIProvider } from './base.js';
import { AIProviderError, AIErrorType } from '../types/index.js'; import { AIProviderError, AIErrorType } from '../types/index.js';
// ============================================================================
// TYPES AND INTERFACES
// ============================================================================
/** /**
* Configuration specific to OpenAI provider * Configuration interface for OpenAI provider with OpenAI-specific options.
*
* @example
* ```typescript
* const config: OpenAIConfig = {
* apiKey: process.env.OPENAI_API_KEY!,
* defaultModel: 'gpt-4o',
* organization: 'org-your-org-id',
* project: 'proj-your-project-id',
* timeout: 60000,
* maxRetries: 3
* };
* ```
*/ */
export interface OpenAIConfig extends AIProviderConfig { export interface OpenAIConfig extends AIProviderConfig {
/** Default model to use if not specified in requests (default: gpt-4) */ /**
* Default OpenAI model to use for requests.
*
* Recommended models:
* - 'gpt-4o': Latest multimodal model, best overall performance
* - 'gpt-4o-mini': Faster and cheaper, good for simple tasks
* - 'gpt-4-turbo': Previous generation flagship, excellent reasoning
* - 'gpt-3.5-turbo': Most cost-effective, good for basic tasks
*
* @default 'gpt-4o'
*/
defaultModel?: string; defaultModel?: string;
/** Organization ID (optional) */
/**
* OpenAI organization ID for billing and access control.
*
* This is useful for organizations with multiple teams or projects
* that need separate billing and usage tracking.
*
* @see https://platform.openai.com/account/org-settings
*/
organization?: string; organization?: string;
/** Project ID (optional) */
/**
* OpenAI project ID for granular access control.
*
* Projects allow fine-grained permission management within
* an organization, enabling better governance and security.
*
* @see https://platform.openai.com/docs/guides/production-best-practices
*/
project?: string; project?: string;
} }
/** /**
* OpenAI provider implementation * Internal interface for enhanced streaming chunk tracking.
* OpenAI streaming provides detailed metadata about completion progress.
*/
interface StreamingState {
/** Current message ID from OpenAI */
messageId: string;
/** Accumulated prompt tokens (estimated) */
promptTokens: number;
/** Accumulated completion tokens (estimated) */
completionTokens: number;
/** Whether the stream has started */
started: boolean;
}
// ============================================================================
// OPENAI PROVIDER IMPLEMENTATION
// ============================================================================
/**
* OpenAI GPT provider implementation.
*
* This class handles all interactions with OpenAI's chat completion API through
* their official TypeScript SDK. It provides optimized handling of OpenAI's
* features including function calling, vision, and advanced streaming.
*
* Usage Pattern:
* 1. Create instance with configuration including API key
* 2. Call initialize() to set up client and validate credentials
* 3. Use complete() or stream() for text generation
* 4. Handle any AIProviderError exceptions appropriately
*
* @example
* ```typescript
* const openai = new OpenAIProvider({
* apiKey: process.env.OPENAI_API_KEY!,
* defaultModel: 'gpt-4o',
* organization: 'org-your-org-id'
* });
*
* await openai.initialize();
*
* const response = await openai.complete({
* messages: [
* { role: 'system', content: 'You are a helpful assistant.' },
* { role: 'user', content: 'Explain machine learning.' }
* ],
* maxTokens: 1000,
* temperature: 0.7
* });
* ```
*/ */
export class OpenAIProvider extends BaseAIProvider { export class OpenAIProvider extends BaseAIProvider {
// ========================================================================
// INSTANCE PROPERTIES
// ========================================================================
/** OpenAI SDK client instance (initialized during doInitialize) */
private client: OpenAI | null = null; private client: OpenAI | null = null;
/** Default model identifier for requests */
private readonly defaultModel: string; private readonly defaultModel: string;
/** Organization ID for billing and access control */
private readonly organization?: string; private readonly organization?: string;
/** Project ID for granular permissions */
private readonly project?: string; private readonly project?: string;
constructor(config: OpenAIConfig) { // ========================================================================
super(config); // CONSTRUCTOR
this.defaultModel = config.defaultModel || 'gpt-4'; // ========================================================================
this.organization = config.organization;
this.project = config.project;
}
/** /**
* Initialize the OpenAI provider by setting up the OpenAI client * Creates a new OpenAI provider instance.
*
* @param config - OpenAI-specific configuration options
* @throws {AIProviderError} If configuration validation fails
*/
constructor(config: OpenAIConfig) {
super(config);
// Set OpenAI-specific defaults
this.defaultModel = config.defaultModel || 'gpt-4o';
this.organization = config.organization;
this.project = config.project;
// Validate model name format
this.validateModelName(this.defaultModel);
}
// ========================================================================
// PROTECTED TEMPLATE METHOD IMPLEMENTATIONS
// ========================================================================
/**
* Initializes the OpenAI provider by setting up the client.
*
* This method:
* 1. Creates the OpenAI SDK client with configuration
* 2. Tests the connection with a minimal API call
* 3. Validates API key permissions and model access
* 4. Sets up organization and project context if provided
*
* @protected
* @throws {Error} If client creation or connection validation fails
*/ */
protected async doInitialize(): Promise<void> { protected async doInitialize(): Promise<void> {
try { try {
// Create OpenAI client with optimized configuration
this.client = new OpenAI({ this.client = new OpenAI({
apiKey: this.config.apiKey, apiKey: this.config.apiKey,
baseURL: this.config.baseUrl, baseURL: this.config.baseUrl,
timeout: this.config.timeout, timeout: this.config.timeout,
maxRetries: this.config.maxRetries, maxRetries: this.config.maxRetries,
organization: this.organization, organization: this.organization,
project: this.project project: this.project,
// Enable automatic retries with exponential backoff
defaultQuery: undefined,
defaultHeaders: {
'User-Agent': 'simple-ai-provider/1.0.0'
}
}); });
// Test the connection by making a simple request // Validate connection and permissions
await this.validateConnection(); await this.validateConnection();
} catch (error) { } catch (error) {
// Clean up on failure
this.client = null;
throw new AIProviderError( throw new AIProviderError(
`Failed to initialize OpenAI provider: ${(error as Error).message}`, `Failed to initialize OpenAI provider: ${(error as Error).message}`,
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
@ -70,118 +231,120 @@ export class OpenAIProvider extends BaseAIProvider {
} }
/** /**
* Generate a completion using OpenAI * Generates a text completion using OpenAI's chat completions API.
*
* This method:
* 1. Converts messages to OpenAI's format
* 2. Builds optimized request parameters
* 3. Makes API call with comprehensive error handling
* 4. Formats response to standard interface
*
* @protected
* @param params - Validated completion parameters
* @returns Promise resolving to formatted completion response
* @throws {Error} If API request fails
*/ */
protected async doComplete(params: CompletionParams): Promise<CompletionResponse> { protected async doComplete(params: CompletionParams): Promise<CompletionResponse> {
if (!this.client) { if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
const response = await this.client.chat.completions.create({ // Build optimized request parameters
model: params.model || this.defaultModel, const requestParams = this.buildRequestParams(params, false);
messages: this.convertMessages(params.messages),
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop: params.stopSequences,
stream: false
});
// 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); return this.formatCompletionResponse(response);
} catch (error) { } catch (error) {
throw this.handleOpenAIError(error as Error); throw this.handleOpenAIError(error as Error);
} }
} }
/** /**
* Generate a streaming completion using OpenAI * Generates a streaming text completion using OpenAI's streaming API.
*
* This method:
* 1. Builds streaming request parameters
* 2. Handles real-time stream chunks from OpenAI
* 3. Tracks token usage throughout the stream
* 4. Yields formatted chunks with proper completion tracking
*
* @protected
* @param params - Validated completion parameters
* @returns AsyncIterable yielding completion chunks
* @throws {Error} If streaming request fails
*/ */
protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> { protected async *doStream(params: CompletionParams): AsyncIterable<CompletionChunk> {
if (!this.client) { if (!this.client) {
throw new AIProviderError('Client not initialized', AIErrorType.INVALID_REQUEST); throw new AIProviderError('OpenAI client not initialized', AIErrorType.INVALID_REQUEST);
} }
try { try {
const stream = await this.client.chat.completions.create({ // Build streaming request parameters
model: params.model || this.defaultModel, const requestParams = this.buildRequestParams(params, true);
messages: this.convertMessages(params.messages),
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop: params.stopSequences,
stream: true
});
let messageId = ''; // Create streaming request
let totalPromptTokens = 0; const stream = this.client.chat.completions.create(requestParams);
let totalCompletionTokens = 0;
for await (const chunk of stream) { // Process stream chunks
if (chunk.id && !messageId) { yield* this.processStreamChunks(stream);
messageId = chunk.id;
}
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
yield {
content: delta.content,
isComplete: false,
id: messageId || chunk.id
};
}
// Check for completion
if (chunk.choices[0]?.finish_reason) {
// For OpenAI, we need to make a separate call to get usage stats
// or track them during streaming (not available in all stream responses)
yield {
content: '',
isComplete: true,
id: messageId || chunk.id,
usage: {
promptTokens: totalPromptTokens,
completionTokens: totalCompletionTokens,
totalTokens: totalPromptTokens + totalCompletionTokens
}
};
}
}
} catch (error) { } catch (error) {
throw this.handleOpenAIError(error as Error); throw this.handleOpenAIError(error as Error);
} }
} }
// ========================================================================
// PUBLIC INTERFACE METHODS
// ========================================================================
/** /**
* Get information about the OpenAI provider * Returns comprehensive information about the OpenAI provider.
*
* @returns Provider information including models, capabilities, and limits
*/ */
public getInfo(): ProviderInfo { public getInfo(): ProviderInfo {
return { return {
name: 'OpenAI', name: 'OpenAI',
version: '1.0.0', version: '1.0.0',
models: [ models: [
'gpt-4', 'gpt-4o', // Latest multimodal model
'gpt-4-turbo', 'gpt-4o-mini', // Faster, cheaper variant
'gpt-4-turbo-preview', 'gpt-4-turbo', // Previous flagship model
'gpt-4-0125-preview', 'gpt-4', // Original GPT-4
'gpt-4-1106-preview', 'gpt-3.5-turbo', // Most cost-effective
'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', // Specific version
'gpt-3.5-turbo-0125', 'gpt-4o-2024-11-20', // Latest dated version
'gpt-3.5-turbo-1106' 'gpt-4-turbo-2024-04-09' // Previous dated version
], ],
maxContextLength: 128000, // GPT-4 Turbo context length maxContextLength: 128000, // 128K tokens for latest models
supportsStreaming: true, supportsStreaming: true,
capabilities: { capabilities: {
vision: true, vision: true, // GPT-4o supports image understanding
functionCalling: true, functionCalling: true, // Advanced function/tool calling
jsonMode: true, jsonMode: true, // Structured JSON output
systemMessages: true systemMessages: true, // Native system message support
reasoning: true, // Strong reasoning capabilities
codeGeneration: true, // Excellent at programming tasks
multimodal: true, // Text + image understanding
fineTuning: true // Custom model training
} }
}; };
} }
// ========================================================================
// PRIVATE UTILITY METHODS
// ========================================================================
/** /**
* Validate the connection by making a simple request * Validates the connection by making a minimal test request.
*
* @private
* @throws {AIProviderError} If connection validation fails
*/ */
private async validateConnection(): Promise<void> { private async validateConnection(): Promise<void> {
if (!this.client) { if (!this.client) {
@ -189,28 +352,108 @@ export class OpenAIProvider extends BaseAIProvider {
} }
try { try {
// Make a minimal request to validate credentials // Make minimal request to test connection and permissions
await this.client.chat.completions.create({ await this.client.chat.completions.create({
model: this.defaultModel, model: this.defaultModel,
messages: [{ role: 'user', content: 'Hi' }], messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 1 max_tokens: 1
}); });
} catch (error: any) { } catch (error: any) {
if (error.status === 401 || error.status === 403) { // Handle specific validation errors
if (error.status === 401) {
throw new AIProviderError( throw new AIProviderError(
'Invalid API key. Please check your OpenAI API key.', 'Invalid OpenAI API key. Please verify your API key from https://platform.openai.com/api-keys',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
error.status error.status
); );
} }
// For other errors during validation, we'll let initialization proceed
// as they might be temporary issues 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);
} }
} }
/** /**
* Convert our generic message format to OpenAI's format * Validates model name format for OpenAI models.
* OpenAI supports system messages directly in the messages array *
* @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
);
}
// OpenAI model names follow specific patterns
const validPatterns = [
/^gpt-4o(?:-mini)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4o, gpt-4o-mini, gpt-4o-2024-11-20
/^gpt-4(?:-turbo)?(?:-\d{4}-\d{2}-\d{2})?$/, // e.g., gpt-4, gpt-4-turbo, gpt-4-turbo-2024-04-09
/^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.`);
}
}
/**
* Builds optimized request parameters for OpenAI API.
*
* @private
* @param params - Input completion parameters
* @param stream - Whether to enable streaming
* @returns Formatted request parameters
*/
private buildRequestParams(params: CompletionParams, stream: boolean) {
return {
model: params.model || this.defaultModel,
messages: this.convertMessages(params.messages),
max_tokens: params.maxTokens || 1000,
temperature: params.temperature ?? 0.7,
top_p: params.topP,
stop: params.stopSequences,
stream,
// Enable structured outputs for better reliability
stream_options: stream ? { include_usage: true } : undefined
};
}
/**
* Converts generic messages to OpenAI's chat completion format.
*
* OpenAI supports system messages natively within the conversation flow,
* making this conversion straightforward.
*
* @private
* @param messages - Input messages array
* @returns OpenAI-formatted messages
*/ */
private convertMessages(messages: AIMessage[]): OpenAI.Chat.Completions.ChatCompletionMessageParam[] { private convertMessages(messages: AIMessage[]): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
return messages.map(message => ({ return messages.map(message => ({
@ -220,13 +463,85 @@ export class OpenAIProvider extends BaseAIProvider {
} }
/** /**
* Format OpenAI's response to our standard format * Processes streaming response chunks from OpenAI API.
*
* @private
* @param stream - OpenAI streaming response promise
* @returns AsyncIterable of formatted completion chunks
*/
private async *processStreamChunks(stream: Promise<any>): AsyncIterable<CompletionChunk> {
const state: StreamingState = {
messageId: '',
promptTokens: 0,
completionTokens: 0,
started: false
};
try {
const streamResponse = await stream;
for await (const chunk of streamResponse) {
// Track message ID
if (chunk.id && !state.messageId) {
state.messageId = chunk.id;
}
// Handle content chunks
const choice = chunk.choices?.[0];
if (choice?.delta?.content) {
state.started = true;
yield {
content: choice.delta.content,
isComplete: false,
id: state.messageId || chunk.id
};
}
// Handle completion with usage information
if (choice?.finish_reason || chunk.usage) {
// Extract usage information if available
if (chunk.usage) {
state.promptTokens = chunk.usage.prompt_tokens || 0;
state.completionTokens = chunk.usage.completion_tokens || 0;
}
yield {
content: '',
isComplete: true,
id: state.messageId || chunk.id,
usage: {
promptTokens: state.promptTokens,
completionTokens: state.completionTokens,
totalTokens: state.promptTokens + state.completionTokens
}
};
return;
}
}
} catch (error) {
// Handle streaming errors gracefully
throw new AIProviderError(
`Streaming interrupted: ${(error as Error).message}`,
AIErrorType.NETWORK,
undefined,
error as Error
);
}
}
/**
* Formats OpenAI's completion response to our standard interface.
*
* @private
* @param response - Raw OpenAI API response
* @returns Formatted completion response
* @throws {AIProviderError} If response format is unexpected
*/ */
private formatCompletionResponse(response: OpenAI.Chat.Completions.ChatCompletion): CompletionResponse { private formatCompletionResponse(response: OpenAI.Chat.Completions.ChatCompletion): CompletionResponse {
const choice = response.choices[0]; const choice = response.choices[0];
if (!choice || !choice.message.content) { if (!choice || !choice.message.content) {
throw new AIProviderError( throw new AIProviderError(
'No content in OpenAI response', 'No content found in OpenAI response',
AIErrorType.UNKNOWN AIErrorType.UNKNOWN
); );
} }
@ -242,69 +557,151 @@ export class OpenAIProvider extends BaseAIProvider {
id: response.id, id: response.id,
metadata: { metadata: {
finishReason: choice.finish_reason, finishReason: choice.finish_reason,
systemFingerprint: response.system_fingerprint systemFingerprint: response.system_fingerprint,
created: response.created
} }
}; };
} }
/** /**
* Handle OpenAI-specific errors and convert them to our standard format * 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 { private handleOpenAIError(error: any): AIProviderError {
if (error instanceof AIProviderError) { if (error instanceof AIProviderError) {
return error; return error;
} }
const status = error.status || error.statusCode;
const message = error.message || 'Unknown OpenAI API error'; const message = error.message || 'Unknown OpenAI API error';
const status = error.status || error.statusCode;
// Map OpenAI-specific error codes and types
switch (status) { switch (status) {
case 400: 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( return new AIProviderError(
`Invalid request: ${message}`, `Invalid request: ${message}`,
AIErrorType.INVALID_REQUEST, AIErrorType.INVALID_REQUEST,
status, status,
error error
); );
case 401: case 401:
return new AIProviderError( return new AIProviderError(
'Authentication failed. Please check your OpenAI API key.', 'Authentication failed. Please verify your OpenAI API key from https://platform.openai.com/api-keys',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
status, status,
error error
); );
case 403: case 403:
if (message.includes('billing') || message.includes('quota')) {
return new AIProviderError( return new AIProviderError(
'Access forbidden. Please check your API key permissions.', 'Insufficient quota or billing issue. Please check your OpenAI account billing and usage limits.',
AIErrorType.AUTHENTICATION, AIErrorType.AUTHENTICATION,
status, status,
error error
); );
}
return new AIProviderError(
'Access forbidden. Your API key may not have permission for this model or feature.',
AIErrorType.AUTHENTICATION,
status,
error
);
case 404: case 404:
return new AIProviderError( return new AIProviderError(
'Model not found. Please check the model name.', 'Model not found. The specified OpenAI model may not exist or be available to your account.',
AIErrorType.MODEL_NOT_FOUND, AIErrorType.MODEL_NOT_FOUND,
status, status,
error error
); );
case 429: case 429:
if (message.includes('rate')) {
return new AIProviderError( return new AIProviderError(
'Rate limit exceeded. Please slow down your requests.', 'Rate limit exceeded. Please reduce request frequency and implement exponential backoff.',
AIErrorType.RATE_LIMIT, AIErrorType.RATE_LIMIT,
status, status,
error 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 500:
case 502: case 502:
case 503: case 503:
case 504: case 504:
return new AIProviderError( return new AIProviderError(
'OpenAI service temporarily unavailable. Please try again later.', 'OpenAI service temporarily unavailable. Please try again in a few moments.',
AIErrorType.NETWORK, AIErrorType.NETWORK,
status, status,
error error
); );
default: 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( return new AIProviderError(
`OpenAI API error: ${message}`, `OpenAI API error: ${message}`,
AIErrorType.UNKNOWN, AIErrorType.UNKNOWN,

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@ describe('ClaudeProvider', () => {
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
temperature: 1.5 temperature: 1.5
}) })
).rejects.toThrow('Temperature must be between 0.0 and 1.0'); ).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
}); });
it('should validate message format', async () => { it('should validate message format', async () => {
@ -62,7 +62,7 @@ describe('ClaudeProvider', () => {
provider.complete({ provider.complete({
messages: [{ role: 'invalid' as any, content: 'test' }] messages: [{ role: 'invalid' as any, content: 'test' }]
}) })
).rejects.toThrow('Each message must have a valid role'); ).rejects.toThrow('Message at index 0 has invalid role \'invalid\'. Must be: system, user, assistant');
}); });
it('should require initialization before use', async () => { it('should require initialization before use', async () => {
@ -115,7 +115,7 @@ describe('ClaudeProvider', () => {
{ role: 'system' as const, content: 'Be concise' } { role: 'system' as const, content: 'Be concise' }
]; ];
const result = (provider as any).convertMessages(messages); const result = (provider as any).processMessages(messages);
expect(result.system).toBe('You are helpful\n\nBe concise'); expect(result.system).toBe('You are helpful\n\nBe concise');
expect(result.messages).toHaveLength(2); expect(result.messages).toHaveLength(2);
@ -129,7 +129,7 @@ describe('ClaudeProvider', () => {
{ role: 'assistant' as const, content: 'Hi there' } { role: 'assistant' as const, content: 'Hi there' }
]; ];
const result = (provider as any).convertMessages(messages); const result = (provider as any).processMessages(messages);
expect(result.system).toBeNull(); expect(result.system).toBeNull();
expect(result.messages).toHaveLength(2); expect(result.messages).toHaveLength(2);

View File

@ -59,12 +59,11 @@ describe('GeminiProvider', () => {
expect(info.models).toContain('gemini-1.5-flash'); expect(info.models).toContain('gemini-1.5-flash');
expect(info.models).toContain('gemini-1.5-pro'); expect(info.models).toContain('gemini-1.5-pro');
expect(info.models).toContain('gemini-1.0-pro'); expect(info.models).toContain('gemini-1.0-pro');
expect(info.maxContextLength).toBe(1000000); expect(info.maxContextLength).toBe(1048576);
expect(info.capabilities).toHaveProperty('vision', true); expect(info.capabilities).toHaveProperty('vision', true);
expect(info.capabilities).toHaveProperty('functionCalling', true); expect(info.capabilities).toHaveProperty('functionCalling', true);
expect(info.capabilities).toHaveProperty('systemMessages', true); expect(info.capabilities).toHaveProperty('systemMessages', true);
expect(info.capabilities).toHaveProperty('multimodal', true); expect(info.capabilities).toHaveProperty('multimodal', true);
expect(info.capabilities).toHaveProperty('largeContext', true);
}); });
}); });
@ -80,7 +79,7 @@ describe('GeminiProvider', () => {
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
temperature: 1.5 temperature: 1.5
}) })
).rejects.toThrow('Temperature must be between 0.0 and 1.0'); ).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
}); });
it('should validate top_p range', async () => { it('should validate top_p range', async () => {
@ -93,7 +92,7 @@ describe('GeminiProvider', () => {
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
topP: 1.5 topP: 1.5
}) })
).rejects.toThrow('Top-p must be between 0.0 and 1.0'); ).rejects.toThrow('Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)');
}); });
it('should validate message format', async () => { it('should validate message format', async () => {
@ -105,7 +104,7 @@ describe('GeminiProvider', () => {
provider.complete({ provider.complete({
messages: [{ role: 'invalid' as any, content: 'test' }] messages: [{ role: 'invalid' as any, content: 'test' }]
}) })
).rejects.toThrow('Each message must have a valid role'); ).rejects.toThrow('Message at index 0 has invalid role \'invalid\'. Must be: system, user, assistant');
}); });
it('should validate empty content', async () => { it('should validate empty content', async () => {
@ -117,7 +116,7 @@ describe('GeminiProvider', () => {
provider.complete({ provider.complete({
messages: [{ role: 'user', content: '' }] messages: [{ role: 'user', content: '' }]
}) })
).rejects.toThrow('Each message must have non-empty string content'); ).rejects.toThrow('Message at index 0 must have non-empty string content');
}); });
it('should require initialization before use', async () => { it('should require initialization before use', async () => {
@ -137,7 +136,7 @@ describe('GeminiProvider', () => {
expect(providerError).toBeInstanceOf(AIProviderError); expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION); expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
expect(providerError.message).toContain('Authentication failed'); expect(providerError.message).toContain('Invalid Google API key');
}); });
it('should handle rate limit errors', () => { it('should handle rate limit errors', () => {
@ -147,7 +146,7 @@ describe('GeminiProvider', () => {
expect(providerError).toBeInstanceOf(AIProviderError); expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT); expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
expect(providerError.message).toContain('Rate limit exceeded'); expect(providerError.message).toContain('API quota exceeded');
}); });
it('should handle model not found errors', () => { it('should handle model not found errors', () => {
@ -161,7 +160,7 @@ describe('GeminiProvider', () => {
}); });
it('should handle invalid request errors', () => { it('should handle invalid request errors', () => {
const error = new Error('invalid request parameters'); const error = new Error('invalid length request parameters');
const providerError = (provider as any).handleGeminiError(error); const providerError = (provider as any).handleGeminiError(error);
@ -351,7 +350,7 @@ describe('GeminiProvider', () => {
expect(() => { expect(() => {
(provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash'); (provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
}).toThrow('No content in Gemini response'); }).toThrow('No candidates found in Gemini response');
}); });
}); });
}); });

View File

@ -73,7 +73,7 @@ describe('OpenAIProvider', () => {
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
temperature: 1.5 temperature: 1.5
}) })
).rejects.toThrow('Temperature must be between 0.0 and 1.0'); ).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
}); });
it('should validate top_p range', async () => { it('should validate top_p range', async () => {
@ -85,7 +85,7 @@ describe('OpenAIProvider', () => {
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
topP: 1.5 topP: 1.5
}) })
).rejects.toThrow('Top-p must be between 0.0 and 1.0'); ).rejects.toThrow('Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)');
}); });
it('should validate message format', async () => { it('should validate message format', async () => {
@ -96,7 +96,7 @@ describe('OpenAIProvider', () => {
provider.complete({ provider.complete({
messages: [{ role: 'invalid' as any, content: 'test' }] messages: [{ role: 'invalid' as any, content: 'test' }]
}) })
).rejects.toThrow('Each message must have a valid role'); ).rejects.toThrow('Message at index 0 has invalid role \'invalid\'. Must be: system, user, assistant');
}); });
it('should validate empty content', async () => { it('should validate empty content', async () => {
@ -107,7 +107,7 @@ describe('OpenAIProvider', () => {
provider.complete({ provider.complete({
messages: [{ role: 'user', content: '' }] messages: [{ role: 'user', content: '' }]
}) })
).rejects.toThrow('Each message must have non-empty string content'); ).rejects.toThrow('Message at index 0 must have non-empty string content');
}); });
it('should require initialization before use', async () => { it('should require initialization before use', async () => {
@ -139,7 +139,7 @@ describe('OpenAIProvider', () => {
expect(providerError).toBeInstanceOf(AIProviderError); expect(providerError).toBeInstanceOf(AIProviderError);
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT); expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
expect(providerError.message).toContain('Rate limit exceeded'); expect(providerError.message).toContain('Too many requests');
}); });
it('should handle model not found errors', () => { it('should handle model not found errors', () => {
@ -255,7 +255,7 @@ describe('OpenAIProvider', () => {
expect(() => { expect(() => {
(provider as any).formatCompletionResponse(mockResponse); (provider as any).formatCompletionResponse(mockResponse);
}).toThrow('No content in OpenAI response'); }).toThrow('No content found in OpenAI response');
}); });
}); });
}); });

View File

@ -64,17 +64,17 @@ describe('OpenWebUIProvider', () => {
expect(info.name).toBe('OpenWebUI'); expect(info.name).toBe('OpenWebUI');
expect(info.version).toBe('1.0.0'); expect(info.version).toBe('1.0.0');
expect(info.supportsStreaming).toBe(true); expect(info.supportsStreaming).toBe(true);
expect(info.maxContextLength).toBe(8192); expect(info.maxContextLength).toBe(32768);
expect(Array.isArray(info.models)).toBe(true); expect(Array.isArray(info.models)).toBe(true);
}); });
it('should include expected models', () => { it('should include expected models', () => {
const info = provider.getInfo(); const info = provider.getInfo();
expect(info.models).toContain('llama3.1'); expect(info.models).toContain('llama3.1:latest');
expect(info.models).toContain('mistral'); expect(info.models).toContain('mistral:latest');
expect(info.models).toContain('codellama'); expect(info.models).toContain('codellama:latest');
expect(info.models).toContain('gemma2'); expect(info.models).toContain('gemma:latest');
}); });
it('should have correct capabilities', () => { it('should have correct capabilities', () => {
@ -83,10 +83,12 @@ describe('OpenWebUIProvider', () => {
expect(info.capabilities).toEqual({ expect(info.capabilities).toEqual({
vision: false, vision: false,
functionCalling: false, functionCalling: false,
jsonMode: false,
systemMessages: true, systemMessages: true,
localExecution: true, reasoning: true,
customModels: true, codeGeneration: true,
rag: true localDeployment: true,
multiModel: true
}); });
}); });
}); });
@ -104,7 +106,7 @@ describe('OpenWebUIProvider', () => {
expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError).toBeInstanceOf(AIProviderError);
expect(aiError.type).toBe(AIErrorType.NETWORK); expect(aiError.type).toBe(AIErrorType.NETWORK);
expect(aiError.message).toContain('Cannot connect to OpenWebUI'); expect(aiError.message).toContain('Network error connecting to OpenWebUI');
}); });
it('should handle hostname resolution errors', () => { it('should handle hostname resolution errors', () => {
@ -114,8 +116,8 @@ describe('OpenWebUIProvider', () => {
const aiError = (provider as any).handleOpenWebUIError(mockError); const aiError = (provider as any).handleOpenWebUIError(mockError);
expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError).toBeInstanceOf(AIProviderError);
expect(aiError.type).toBe(AIErrorType.NETWORK); expect(aiError.type).toBe(AIErrorType.UNKNOWN);
expect(aiError.message).toContain('Cannot resolve OpenWebUI hostname'); expect(aiError.message).toContain('OpenWebUI error');
}); });
it('should handle 404 model not found errors', () => { it('should handle 404 model not found errors', () => {
@ -137,7 +139,8 @@ describe('OpenWebUIProvider', () => {
const aiError = (provider as any).handleOpenWebUIError(mockError); const aiError = (provider as any).handleOpenWebUIError(mockError);
expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError).toBeInstanceOf(AIProviderError);
expect(aiError.type).toBe(AIErrorType.NETWORK); expect(aiError.type).toBe(AIErrorType.INVALID_REQUEST);
expect(aiError.message).toContain('OpenWebUI endpoint not found');
}); });
it('should handle timeout errors', () => { it('should handle timeout errors', () => {
@ -178,7 +181,7 @@ describe('OpenWebUIProvider', () => {
expect(aiError).toBeInstanceOf(AIProviderError); expect(aiError).toBeInstanceOf(AIProviderError);
expect(aiError.type).toBe(AIErrorType.AUTHENTICATION); expect(aiError.type).toBe(AIErrorType.AUTHENTICATION);
expect(aiError.message).toContain('Settings > Account'); expect(aiError.message).toContain('Authentication failed with OpenWebUI');
}); });
it('should handle unknown errors', () => { it('should handle unknown errors', () => {
@ -220,7 +223,7 @@ describe('OpenWebUIProvider', () => {
const prompt = (provider as any).convertMessagesToPrompt(messages); const prompt = (provider as any).convertMessagesToPrompt(messages);
expect(prompt).toBe('System: You are helpful\n\nHuman: Hello\n\nAssistant: Hi there!'); expect(prompt).toBe('System: You are helpful\n\nHuman: Hello\n\nAssistant: Hi there!\n\nAssistant: ');
}); });
}); });
@ -278,8 +281,8 @@ describe('OpenWebUIProvider', () => {
expect(response.usage.promptTokens).toBe(10); expect(response.usage.promptTokens).toBe(10);
expect(response.usage.completionTokens).toBe(5); expect(response.usage.completionTokens).toBe(5);
expect(response.usage.totalTokens).toBe(15); expect(response.usage.totalTokens).toBe(15);
expect(response.metadata?.created_at).toBe('2024-01-01T00:00:00Z'); expect(response.metadata?.created).toBe(1704067200000);
expect(response.metadata?.total_duration).toBe(1000000); expect(response.metadata?.totalDuration).toBe(1000000);
}); });
it('should handle response without content', () => { it('should handle response without content', () => {