claude-autopm 1.31.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,112 +1,423 @@
1
1
  /**
2
- * ClaudeProvider - Anthropic Claude API integration
2
+ * ClaudeProvider - Anthropic Claude API Integration
3
3
  *
4
- * Provides both synchronous completion and streaming capabilities
5
- * for Claude AI interactions.
4
+ * Extends AbstractAIProvider to provide Claude-specific AI capabilities
5
+ * with full backward compatibility for existing code.
6
6
  *
7
7
  * Documentation Queries:
8
8
  * - mcp://context7/anthropic/sdk - Anthropic SDK patterns
9
9
  * - mcp://context7/anthropic/streaming - Streaming best practices
10
10
  * - mcp://context7/nodejs/async-generators - Async generator patterns
11
+ *
12
+ * @extends AbstractAIProvider
13
+ *
14
+ * @example
15
+ * // Legacy usage (backward compatible)
16
+ * const provider = new ClaudeProvider('sk-ant-...');
17
+ * const result = await provider.complete('Hello');
18
+ *
19
+ * @example
20
+ * // Enhanced usage with config
21
+ * const provider = new ClaudeProvider({
22
+ * apiKey: 'sk-ant-...',
23
+ * model: 'claude-sonnet-4-20250514',
24
+ * temperature: 0.7,
25
+ * maxTokens: 4096
26
+ * });
27
+ *
28
+ * @example
29
+ * // Using environment variable
30
+ * process.env.ANTHROPIC_API_KEY = 'sk-ant-...';
31
+ * const provider = new ClaudeProvider({});
11
32
  */
12
33
 
13
34
  const Anthropic = require('@anthropic-ai/sdk');
35
+ const AbstractAIProvider = require('./AbstractAIProvider');
36
+ const AIProviderError = require('../errors/AIProviderError');
14
37
 
15
38
  /**
16
39
  * ClaudeProvider class for Anthropic Claude API integration
40
+ *
41
+ * @class ClaudeProvider
42
+ * @extends AbstractAIProvider
17
43
  */
18
- class ClaudeProvider {
44
+ class ClaudeProvider extends AbstractAIProvider {
19
45
  /**
20
46
  * Create a new ClaudeProvider instance
21
- * @param {string} apiKey - Anthropic API key
22
- * @throws {Error} If API key is not provided
47
+ *
48
+ * Supports two constructor signatures for backward compatibility:
49
+ * 1. new ClaudeProvider('api-key') <- Legacy
50
+ * 2. new ClaudeProvider({ apiKey: 'key', ...options }) <- Enhanced
51
+ *
52
+ * @param {string|Object} [config={}] - API key string or configuration object
53
+ * @param {string} [config.apiKey] - Anthropic API key (or use ANTHROPIC_API_KEY env var)
54
+ * @param {string} [config.model] - Model to use (default: claude-sonnet-4-20250514)
55
+ * @param {number} [config.maxTokens] - Maximum tokens (default: 4096)
56
+ * @param {number} [config.temperature] - Temperature (default: 0.7)
57
+ *
58
+ * @throws {Error} If API key is not provided and ANTHROPIC_API_KEY env var is not set
23
59
  */
24
- constructor(apiKey) {
25
- if (!apiKey) {
26
- throw new Error('API key is required for ClaudeProvider');
60
+ constructor(config = {}) {
61
+ // Handle backward compatibility: string API key as first parameter
62
+ if (typeof config === 'string') {
63
+ config = { apiKey: config };
27
64
  }
28
65
 
29
- this.apiKey = apiKey;
30
- this.client = new Anthropic({ apiKey });
66
+ // Call parent constructor (handles config normalization and defaults)
67
+ super(config);
68
+
69
+ // Validate API key is available after parent constructor
70
+ if (!this.apiKey) {
71
+ throw new Error(
72
+ 'API key is required for ClaudeProvider. ' +
73
+ 'Provide it via constructor or set ANTHROPIC_API_KEY environment variable.'
74
+ );
75
+ }
76
+
77
+ // Initialize Anthropic client with API key
78
+ this.client = new Anthropic({ apiKey: this.apiKey });
79
+ }
80
+
81
+ // ============================================================
82
+ // ABSTRACT METHOD IMPLEMENTATIONS (Required by AbstractAIProvider)
83
+ // ============================================================
84
+
85
+ /**
86
+ * Get the default model identifier for Claude
87
+ *
88
+ * @returns {string} Default Claude model
89
+ */
90
+ getDefaultModel() {
91
+ return 'claude-sonnet-4-20250514';
92
+ }
93
+
94
+ /**
95
+ * Get the environment variable name for API key
96
+ *
97
+ * @returns {string} Environment variable name
98
+ */
99
+ getApiKeyEnvVar() {
100
+ return 'ANTHROPIC_API_KEY';
31
101
  }
32
102
 
33
103
  /**
34
104
  * Complete a prompt synchronously (wait for full response)
105
+ *
106
+ * Uses parent's _mergeOptions to combine instance config with method options.
107
+ * Method-level options take precedence over instance config.
108
+ *
109
+ * Automatically applies rate limiting if configured in constructor.
110
+ *
35
111
  * @param {string} prompt - The prompt to complete
36
- * @param {Object} options - Optional configuration
37
- * @param {string} options.model - Model to use (default: claude-sonnet-4-20250514)
38
- * @param {number} options.maxTokens - Maximum tokens to generate (default: 4096)
112
+ * @param {Object} [options={}] - Optional configuration
113
+ * @param {string} [options.model] - Model to use (overrides instance model)
114
+ * @param {number} [options.maxTokens] - Maximum tokens (overrides instance maxTokens)
115
+ * @param {number} [options.temperature] - Temperature (overrides instance temperature)
116
+ *
39
117
  * @returns {Promise<string>} The completed text
118
+ * @throws {AIProviderError} On API errors
40
119
  */
41
120
  async complete(prompt, options = {}) {
42
- try {
43
- const response = await this.client.messages.create({
44
- model: options.model || 'claude-sonnet-4-20250514',
45
- max_tokens: options.maxTokens || 4096,
46
- messages: [{ role: 'user', content: prompt }]
47
- });
121
+ return this._withRateLimit(async () => {
122
+ // Merge instance config with method options
123
+ const finalOptions = this._mergeOptions(options);
48
124
 
49
- // Extract text from response
50
- if (response.content && response.content.length > 0) {
51
- return response.content[0].text;
52
- }
125
+ try {
126
+ const response = await this.client.messages.create({
127
+ model: finalOptions.model,
128
+ max_tokens: finalOptions.maxTokens,
129
+ temperature: finalOptions.temperature,
130
+ messages: [{ role: 'user', content: prompt }]
131
+ });
53
132
 
54
- return '';
55
- } catch (error) {
56
- throw new Error(`Claude API error: ${error.message}`);
57
- }
133
+ // Extract text from response
134
+ if (response.content && response.content.length > 0) {
135
+ return response.content[0].text || '';
136
+ }
137
+
138
+ return '';
139
+ } catch (error) {
140
+ // Format error using parent's formatError + Claude-specific mapping
141
+ throw this.formatError(error);
142
+ }
143
+ });
58
144
  }
59
145
 
60
146
  /**
61
147
  * Stream a prompt response (async generator for real-time feedback)
148
+ *
149
+ * Uses parent's _mergeOptions to combine instance config with method options.
150
+ * Method-level options take precedence over instance config.
151
+ *
152
+ * Automatically applies rate limiting if configured in constructor.
153
+ *
62
154
  * @param {string} prompt - The prompt to complete
63
- * @param {Object} options - Optional configuration
64
- * @param {string} options.model - Model to use (default: claude-sonnet-4-20250514)
65
- * @param {number} options.maxTokens - Maximum tokens to generate (default: 4096)
155
+ * @param {Object} [options={}] - Optional configuration
156
+ * @param {string} [options.model] - Model to use (overrides instance model)
157
+ * @param {number} [options.maxTokens] - Maximum tokens (overrides instance maxTokens)
158
+ * @param {number} [options.temperature] - Temperature (overrides instance temperature)
159
+ *
66
160
  * @yields {string} Text chunks as they arrive
161
+ * @throws {AIProviderError} On API errors
67
162
  */
68
163
  async *stream(prompt, options = {}) {
164
+ // Apply rate limiting before initiating stream
165
+ if (this.rateLimiter) {
166
+ await this.rateLimiter.removeTokens(1);
167
+ }
168
+
169
+ // Merge instance config with method options
170
+ const finalOptions = this._mergeOptions(options);
171
+
69
172
  try {
70
173
  const stream = await this.client.messages.create({
71
- model: options.model || 'claude-sonnet-4-20250514',
72
- max_tokens: options.maxTokens || 4096,
174
+ model: finalOptions.model,
175
+ max_tokens: finalOptions.maxTokens,
176
+ temperature: finalOptions.temperature,
73
177
  stream: true,
74
178
  messages: [{ role: 'user', content: prompt }]
75
179
  });
76
180
 
77
181
  // Yield text deltas as they arrive
78
182
  for await (const event of stream) {
79
- if (event.type === 'content_block_delta' &&
80
- event.delta &&
81
- event.delta.type === 'text_delta') {
183
+ if (
184
+ event.type === 'content_block_delta' &&
185
+ event.delta &&
186
+ event.delta.type === 'text_delta'
187
+ ) {
82
188
  yield event.delta.text;
83
189
  }
84
190
  }
85
191
  } catch (error) {
86
- throw new Error(`Claude API streaming error: ${error.message}`);
192
+ // Format error using parent's formatError + Claude-specific mapping
193
+ throw this.formatError(error);
194
+ }
195
+ }
196
+
197
+ // ============================================================
198
+ // CAPABILITY OVERRIDES
199
+ // ============================================================
200
+
201
+ /**
202
+ * Check if Claude supports streaming
203
+ *
204
+ * @returns {boolean} True (Claude supports streaming)
205
+ */
206
+ supportsStreaming() {
207
+ return true;
208
+ }
209
+
210
+ /**
211
+ * Check if Claude supports function calling
212
+ *
213
+ * @returns {boolean} True (Claude supports tools/function calling)
214
+ */
215
+ supportsFunctionCalling() {
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * Check if Claude supports chat format
221
+ *
222
+ * @returns {boolean} True (Claude has native chat format)
223
+ */
224
+ supportsChat() {
225
+ return true;
226
+ }
227
+
228
+ /**
229
+ * Check if Claude supports vision/image inputs
230
+ *
231
+ * @returns {boolean} False (not implemented yet)
232
+ */
233
+ supportsVision() {
234
+ return false;
235
+ }
236
+
237
+ // ============================================================
238
+ // ENHANCED ERROR HANDLING
239
+ // ============================================================
240
+
241
+ /**
242
+ * Format Anthropic API errors into AIProviderError
243
+ *
244
+ * Maps Anthropic-specific error codes and HTTP statuses to AIProviderError codes.
245
+ * Falls back to parent's formatError for unknown errors.
246
+ *
247
+ * @param {Error} error - The error to format
248
+ * @returns {AIProviderError} Formatted error with appropriate code and metadata
249
+ */
250
+ formatError(error) {
251
+ // Already an AIProviderError, return as-is
252
+ if (error instanceof AIProviderError) {
253
+ return error;
254
+ }
255
+
256
+ // Map Anthropic HTTP status codes to AIProviderError codes
257
+ if (error.status) {
258
+ switch (error.status) {
259
+ case 401:
260
+ return new AIProviderError(
261
+ AIProviderError.INVALID_API_KEY,
262
+ 'Invalid Anthropic API key or authentication failed',
263
+ true,
264
+ 401
265
+ );
266
+
267
+ case 429:
268
+ return new AIProviderError(
269
+ AIProviderError.RATE_LIMIT,
270
+ 'Claude API rate limit exceeded. Please retry after a delay.',
271
+ true,
272
+ 429
273
+ );
274
+
275
+ case 500:
276
+ case 502:
277
+ case 503:
278
+ case 504:
279
+ return new AIProviderError(
280
+ AIProviderError.SERVICE_UNAVAILABLE,
281
+ `Claude API service temporarily unavailable (${error.status})`,
282
+ true,
283
+ error.status
284
+ );
285
+
286
+ case 400:
287
+ return new AIProviderError(
288
+ AIProviderError.INVALID_REQUEST,
289
+ error.message || 'Invalid request parameters',
290
+ true,
291
+ 400
292
+ );
293
+
294
+ default:
295
+ // Unknown status code, wrap in generic error
296
+ return new AIProviderError(
297
+ 'UNKNOWN_ERROR',
298
+ `Claude API error (${error.status}): ${error.message}`,
299
+ true,
300
+ error.status
301
+ );
302
+ }
303
+ }
304
+
305
+ // No status code, check for specific error types
306
+ if (error.message) {
307
+ const lowerMessage = error.message.toLowerCase();
308
+
309
+ if (lowerMessage.includes('api key') || lowerMessage.includes('authentication')) {
310
+ return new AIProviderError(
311
+ AIProviderError.INVALID_API_KEY,
312
+ error.message,
313
+ true
314
+ );
315
+ }
316
+
317
+ if (lowerMessage.includes('rate limit')) {
318
+ return new AIProviderError(
319
+ AIProviderError.RATE_LIMIT,
320
+ error.message,
321
+ true
322
+ );
323
+ }
324
+
325
+ if (lowerMessage.includes('network') || lowerMessage.includes('connection')) {
326
+ return new AIProviderError(
327
+ AIProviderError.NETWORK_ERROR,
328
+ error.message,
329
+ true
330
+ );
331
+ }
332
+
333
+ if (lowerMessage.includes('context') || lowerMessage.includes('too long')) {
334
+ return new AIProviderError(
335
+ AIProviderError.CONTEXT_LENGTH_EXCEEDED,
336
+ error.message,
337
+ true
338
+ );
339
+ }
87
340
  }
341
+
342
+ // Fall back to parent's error handling
343
+ return super.formatError(error);
344
+ }
345
+
346
+ // ============================================================
347
+ // ENHANCED CHAT SUPPORT
348
+ // ============================================================
349
+
350
+ /**
351
+ * Chat completion with message history
352
+ *
353
+ * Overrides parent's fallback implementation to use Claude's native chat format.
354
+ * Converts system role to user role (Claude requirement).
355
+ *
356
+ * Automatically applies rate limiting if configured in constructor.
357
+ *
358
+ * @param {Array<{role: string, content: string}>} messages - Chat messages
359
+ * @param {Object} [options={}] - Optional configuration
360
+ *
361
+ * @returns {Promise<string>} Completion response
362
+ * @throws {AIProviderError} On API errors
363
+ */
364
+ async chat(messages, options = {}) {
365
+ return this._withRateLimit(async () => {
366
+ // Merge instance config with method options
367
+ const finalOptions = this._mergeOptions(options);
368
+
369
+ try {
370
+ // Convert system role to user (Claude doesn't support system role in messages array)
371
+ const claudeMessages = messages.map(msg => ({
372
+ role: msg.role === 'system' ? 'user' : msg.role,
373
+ content: msg.content
374
+ }));
375
+
376
+ const response = await this.client.messages.create({
377
+ model: finalOptions.model,
378
+ max_tokens: finalOptions.maxTokens,
379
+ temperature: finalOptions.temperature,
380
+ messages: claudeMessages
381
+ });
382
+
383
+ // Extract text from response
384
+ if (response.content && response.content.length > 0) {
385
+ return response.content[0].text || '';
386
+ }
387
+
388
+ return '';
389
+ } catch (error) {
390
+ throw this.formatError(error);
391
+ }
392
+ });
88
393
  }
89
394
 
395
+ // ============================================================
396
+ // BACKWARD COMPATIBILITY METHODS
397
+ // ============================================================
398
+
90
399
  /**
91
- * Get the current model being used
400
+ * Get the current model being used (legacy method for backward compatibility)
401
+ *
402
+ * Note: This returns the default model, not the instance model.
403
+ * For instance model, use provider.model property.
404
+ *
92
405
  * @returns {string} The default model name
406
+ * @deprecated Use getDefaultModel() or access provider.model instead
93
407
  */
94
408
  getModel() {
95
- return 'claude-sonnet-4-20250514';
409
+ return this.getDefaultModel();
96
410
  }
97
411
 
98
412
  /**
99
- * Test the API connection
413
+ * Test the API connection (inherited from AbstractAIProvider)
414
+ *
415
+ * This method is provided by the parent class and works by making a test
416
+ * complete() call. Included here for documentation completeness.
417
+ *
100
418
  * @returns {Promise<boolean>} True if connection is successful
101
419
  */
102
- async testConnection() {
103
- try {
104
- await this.complete('Hello');
105
- return true;
106
- } catch (error) {
107
- return false;
108
- }
109
- }
420
+ // testConnection() is inherited from AbstractAIProvider
110
421
  }
111
422
 
112
423
  module.exports = ClaudeProvider;