/** * Anthropic Claude AI Provider Implementation * * 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 type { AIProviderConfig, CompletionParams, CompletionResponse, CompletionChunk, ProviderInfo, AIMessage } from '../types/index.js'; import { BaseAIProvider } from './base.js'; import { AIProviderError, AIErrorType } from '../types/index.js'; // ============================================================================ // TYPES AND INTERFACES // ============================================================================ /** * 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 { /** * 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; /** * 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; } /** * 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 { // ======================================================================== // INSTANCE PROPERTIES // ======================================================================== /** Anthropic SDK client instance (initialized during doInitialize) */ private client: Anthropic | null = null; /** Default model identifier for requests */ private readonly defaultModel: string; /** API version for Anthropic requests */ private readonly version: string; // ======================================================================== // CONSTRUCTOR // ======================================================================== /** * 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 { try { // Create Anthropic client with optimized configuration this.client = new Anthropic({ apiKey: this.config.apiKey, baseURL: this.config.baseUrl, timeout: this.config.timeout, maxRetries: this.config.maxRetries, defaultHeaders: { 'anthropic-version': this.version, 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15', // Enable extended context } }); // Validate connection and permissions await this.validateConnection(); } catch (error) { // Clean up on failure this.client = null; throw new AIProviderError( `Failed to initialize Claude provider: ${(error as Error).message}`, AIErrorType.AUTHENTICATION, undefined, error as Error ); } } /** * 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 { if (!this.client) { throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST); } try { // Process messages for Claude's format requirements const { system, messages } = this.processMessages(params.messages); // Build optimized request parameters const requestParams = this.buildRequestParams(params, system, messages, false); // Make API request const response = await this.client.messages.create(requestParams); // Format and return response return this.formatCompletionResponse(response); } catch (error) { throw this.handleAnthropicError(error as Error); } } /** * 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 { if (!this.client) { throw new AIProviderError('Claude client not initialized', AIErrorType.INVALID_REQUEST); } try { // Process messages for Claude's format requirements const { system, messages } = this.processMessages(params.messages); // Build streaming request parameters const requestParams = this.buildRequestParams(params, system, messages, true); // Create streaming request const stream = await this.client.messages.create(requestParams); // Process stream chunks yield* this.processStreamChunks(stream); } catch (error) { throw this.handleAnthropicError(error as Error); } } // ======================================================================== // 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 { 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, 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 })), stream }; } /** * 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 { let messageId = ''; let totalInputTokens = 0; let totalOutputTokens = 0; try { for await (const chunk of stream) { // Handle different chunk types switch (chunk.type) { case 'message_start': messageId = chunk.message.id; totalInputTokens = chunk.message.usage?.input_tokens || 0; break; case 'content_block_delta': if (chunk.delta?.type === 'text_delta') { yield { content: chunk.delta.text, isComplete: false, id: messageId }; } 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 { content: '', isComplete: true, id: messageId, usage: { promptTokens: totalInputTokens, completionTokens: totalOutputTokens, totalTokens: totalInputTokens + totalOutputTokens } }; return; } } } catch (error) { // Handle streaming errors gracefully throw new AIProviderError( `Streaming interrupted: ${(error as Error).message}`, AIErrorType.NETWORK, undefined, error as Error ); } } /** * Formats Anthropic's completion response to our standard interface. * * @private * @param response - Raw Anthropic API response * @returns Formatted completion response * @throws {AIProviderError} If response format is unexpected */ private formatCompletionResponse(response: any): CompletionResponse { // Extract text content from response blocks const content = response.content ?.filter((block: any) => block.type === 'text') ?.map((block: any) => block.text) ?.join('') || ''; if (!content) { throw new AIProviderError( 'No text content found in Claude response', AIErrorType.UNKNOWN ); } return { content, model: response.model, usage: { promptTokens: response.usage?.input_tokens || 0, completionTokens: response.usage?.output_tokens || 0, totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0) }, id: response.id, metadata: { stopReason: response.stop_reason, stopSequence: response.stop_sequence || null, created: Date.now() // Anthropic doesn't provide timestamp } }; } /** * Handles and transforms Anthropic-specific errors. * * This method maps Anthropic's error responses to our standardized * error format, providing helpful context and suggestions. * * @private * @param error - Original error from Anthropic API * @returns Normalized AIProviderError */ private handleAnthropicError(error: any): AIProviderError { if (error instanceof AIProviderError) { return error; } const message = error.message || 'Unknown Anthropic API error'; const status = error.status || error.statusCode; // Map Anthropic-specific error codes switch (status) { case 400: if (message.includes('max_tokens')) { return new AIProviderError( 'Max tokens value is invalid. Must be between 1 and model limit.', AIErrorType.INVALID_REQUEST, status, error ); } if (message.includes('model')) { return new AIProviderError( 'Invalid model specified. Please check model availability.', AIErrorType.MODEL_NOT_FOUND, status, error ); } return new AIProviderError( `Invalid request: ${message}`, AIErrorType.INVALID_REQUEST, status, error ); case 401: return new AIProviderError( 'Authentication failed. Please check your Anthropic API key from https://console.anthropic.com/', AIErrorType.AUTHENTICATION, status, error ); case 403: return new AIProviderError( 'Access forbidden. Your API key may not have permission for this model or feature.', AIErrorType.AUTHENTICATION, status, error ); case 404: return new AIProviderError( 'Model not found. The specified Claude model may not be available to your account.', AIErrorType.MODEL_NOT_FOUND, status, error ); case 429: return new AIProviderError( 'Rate limit exceeded. Claude APIs have usage limits. Please wait before retrying.', AIErrorType.RATE_LIMIT, status, error ); case 500: case 502: case 503: return new AIProviderError( 'Anthropic service temporarily unavailable. Please try again in a few moments.', AIErrorType.NETWORK, status, error ); default: // Handle timeout and network errors if (message.includes('timeout') || error.code === 'ETIMEDOUT') { return new AIProviderError( 'Request timed out. Claude may be experiencing high load.', AIErrorType.TIMEOUT, status, error ); } if (message.includes('network') || error.code === 'ECONNREFUSED') { return new AIProviderError( 'Network error connecting to Anthropic servers.', AIErrorType.NETWORK, status, error ); } return new AIProviderError( `Claude API error: ${message}`, AIErrorType.UNKNOWN, status, error ); } } }