681 lines
21 KiB
TypeScript
681 lines
21 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<CompletionResponse> {
|
|
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<CompletionChunk> {
|
|
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<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,
|
|
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<CompletionChunk> {
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}
|