@bernierllc/ai-provider-anthropic 1.0.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.
@@ -0,0 +1,208 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code only within the scope of the project it was delivered for.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import Anthropic from '@anthropic-ai/sdk';
10
+ import { handleAnthropicError, isRetryableError, getRetryDelay } from '../src/utils/error-handling';
11
+
12
+ describe('Error Handling', () => {
13
+ describe('handleAnthropicError', () => {
14
+ it('should handle Anthropic APIError', () => {
15
+ const apiError = Object.assign(
16
+ new Error('API Error'),
17
+ {
18
+ status: 500,
19
+ headers: {}
20
+ }
21
+ );
22
+ Object.setPrototypeOf(apiError, Anthropic.APIError.prototype);
23
+
24
+ const result = handleAnthropicError(apiError);
25
+
26
+ expect(result.message).toContain('Anthropic API Error');
27
+ expect(result.message).toContain('500');
28
+ expect(result.name).toBe('AnthropicAPIError');
29
+ });
30
+
31
+ it('should handle Anthropic AuthenticationError', () => {
32
+ const authError = new Anthropic.AuthenticationError(
33
+ 401,
34
+ { error: { message: 'Invalid API key' }, type: 'error' },
35
+ 'Invalid API key',
36
+ {}
37
+ );
38
+
39
+ const result = handleAnthropicError(authError);
40
+
41
+ // AuthenticationError extends APIError, so it's caught by APIError handler
42
+ expect(result.message).toContain('Anthropic API Error');
43
+ expect(result.message).toContain('401');
44
+ expect(result.name).toBe('AnthropicAPIError');
45
+ });
46
+
47
+ it('should handle Anthropic RateLimitError', () => {
48
+ const rateLimitError = new Anthropic.RateLimitError(
49
+ 429,
50
+ { error: { message: 'Rate limit exceeded' }, type: 'error' },
51
+ 'Rate limit exceeded',
52
+ {}
53
+ );
54
+
55
+ const result = handleAnthropicError(rateLimitError);
56
+
57
+ // RateLimitError extends APIError, so it's caught by APIError handler
58
+ expect(result.message).toContain('Anthropic API Error');
59
+ expect(result.message).toContain('429');
60
+ expect(result.name).toBe('AnthropicAPIError');
61
+ });
62
+
63
+ it('should handle generic Error', () => {
64
+ const error = new Error('Generic error');
65
+
66
+ const result = handleAnthropicError(error);
67
+
68
+ expect(result).toBe(error);
69
+ });
70
+
71
+ it('should handle unknown error types', () => {
72
+ const result = handleAnthropicError('string error');
73
+
74
+ expect(result.message).toContain('Unknown Anthropic error');
75
+ expect(result.message).toContain('string error');
76
+ });
77
+
78
+ it('should handle null error', () => {
79
+ const result = handleAnthropicError(null);
80
+
81
+ expect(result.message).toContain('Unknown Anthropic error');
82
+ });
83
+ });
84
+
85
+ describe('isRetryableError', () => {
86
+ it('should identify 5xx errors as retryable', () => {
87
+ const error = Object.assign(
88
+ new Error('Server error'),
89
+ {
90
+ status: 500,
91
+ headers: {}
92
+ }
93
+ );
94
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
95
+
96
+ expect(isRetryableError(error)).toBe(true);
97
+ });
98
+
99
+ it('should identify 429 as retryable', () => {
100
+ const error = Object.assign(
101
+ new Error('Rate limit'),
102
+ {
103
+ status: 429,
104
+ headers: {}
105
+ }
106
+ );
107
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
108
+
109
+ expect(isRetryableError(error)).toBe(true);
110
+ });
111
+
112
+ it('should identify RateLimitError as retryable', () => {
113
+ const error = new Anthropic.RateLimitError(
114
+ 429,
115
+ { error: { message: 'Rate limit' }, type: 'error' },
116
+ 'Rate limit',
117
+ {}
118
+ );
119
+
120
+ expect(isRetryableError(error)).toBe(true);
121
+ });
122
+
123
+ it('should not retry 4xx errors (except 429)', () => {
124
+ const error = Object.assign(
125
+ new Error('Bad request'),
126
+ {
127
+ status: 400,
128
+ headers: {}
129
+ }
130
+ );
131
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
132
+
133
+ expect(isRetryableError(error)).toBe(false);
134
+ });
135
+
136
+ it('should not retry non-API errors', () => {
137
+ const error = new Error('Generic error');
138
+
139
+ expect(isRetryableError(error)).toBe(false);
140
+ });
141
+
142
+ it('should not retry unknown errors', () => {
143
+ expect(isRetryableError('string error')).toBe(false);
144
+ expect(isRetryableError(null)).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe('getRetryDelay', () => {
149
+ it('should return 1000ms for RateLimitError', () => {
150
+ const error = new Anthropic.RateLimitError(
151
+ 429,
152
+ { error: { message: 'Rate limit' }, type: 'error' },
153
+ 'Rate limit',
154
+ {}
155
+ );
156
+
157
+ const delay = getRetryDelay(error);
158
+
159
+ expect(delay).toBe(1000);
160
+ });
161
+
162
+ it('should return undefined for non-rate-limit errors', () => {
163
+ const error = Object.assign(
164
+ new Error('Server error'),
165
+ {
166
+ status: 500,
167
+ headers: {}
168
+ }
169
+ );
170
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
171
+
172
+ const delay = getRetryDelay(error);
173
+
174
+ expect(delay).toBeUndefined();
175
+ });
176
+
177
+ it('should return undefined for generic errors', () => {
178
+ const error = new Error('Generic error');
179
+
180
+ const delay = getRetryDelay(error);
181
+
182
+ expect(delay).toBeUndefined();
183
+ });
184
+ });
185
+
186
+ describe('edge cases', () => {
187
+ it('should handle APIError with no status', () => {
188
+ const error = new Error('API Error');
189
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
190
+
191
+ const result = handleAnthropicError(error);
192
+ expect(result.message).toContain('unknown');
193
+ });
194
+
195
+ it('should handle 503 as retryable', () => {
196
+ const error = Object.assign(
197
+ new Error('Service unavailable'),
198
+ {
199
+ status: 503,
200
+ headers: {}
201
+ }
202
+ );
203
+ Object.setPrototypeOf(error, Anthropic.APIError.prototype);
204
+
205
+ expect(isRetryableError(error)).toBe(true);
206
+ });
207
+ });
208
+ });
@@ -0,0 +1,161 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code only within the scope of the project it was delivered for.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { convertMessagesToAnthropicFormat, validateMessages } from '../src/utils/message-conversion';
10
+ import { Message } from '@bernierllc/ai-provider-core';
11
+
12
+ describe('Message Conversion', () => {
13
+ describe('convertMessagesToAnthropicFormat', () => {
14
+ it('should extract system messages separately', () => {
15
+ const messages: Message[] = [
16
+ { role: 'system', content: 'You are a helpful assistant.' },
17
+ { role: 'user', content: 'Hello!' }
18
+ ];
19
+
20
+ const result = convertMessagesToAnthropicFormat(messages);
21
+
22
+ expect(result.system).toBe('You are a helpful assistant.');
23
+ expect(result.messages).toHaveLength(1);
24
+ expect(result.messages[0].role).toBe('user');
25
+ expect(result.messages[0].content).toBe('Hello!');
26
+ });
27
+
28
+ it('should handle multiple system messages', () => {
29
+ const messages: Message[] = [
30
+ { role: 'system', content: 'First instruction.' },
31
+ { role: 'system', content: 'Second instruction.' },
32
+ { role: 'user', content: 'Hello!' }
33
+ ];
34
+
35
+ const result = convertMessagesToAnthropicFormat(messages);
36
+
37
+ expect(result.system).toBe('First instruction.\n\nSecond instruction.');
38
+ expect(result.messages).toHaveLength(1);
39
+ });
40
+
41
+ it('should return undefined system when no system messages', () => {
42
+ const messages: Message[] = [
43
+ { role: 'user', content: 'Hello!' },
44
+ { role: 'assistant', content: 'Hi there!' }
45
+ ];
46
+
47
+ const result = convertMessagesToAnthropicFormat(messages);
48
+
49
+ expect(result.system).toBeUndefined();
50
+ expect(result.messages).toHaveLength(2);
51
+ });
52
+
53
+ it('should convert assistant and user roles correctly', () => {
54
+ const messages: Message[] = [
55
+ { role: 'user', content: 'Question?' },
56
+ { role: 'assistant', content: 'Answer!' }
57
+ ];
58
+
59
+ const result = convertMessagesToAnthropicFormat(messages);
60
+
61
+ expect(result.messages[0].role).toBe('user');
62
+ expect(result.messages[1].role).toBe('assistant');
63
+ });
64
+
65
+ it('should convert function role to user role', () => {
66
+ const messages: Message[] = [
67
+ { role: 'function', content: 'Function result' }
68
+ ];
69
+
70
+ const result = convertMessagesToAnthropicFormat(messages);
71
+
72
+ expect(result.messages[0].role).toBe('user');
73
+ expect(result.messages[0].content).toBe('Function result');
74
+ });
75
+
76
+ it('should handle empty messages array', () => {
77
+ const messages: Message[] = [];
78
+
79
+ const result = convertMessagesToAnthropicFormat(messages);
80
+
81
+ expect(result.system).toBeUndefined();
82
+ expect(result.messages).toHaveLength(0);
83
+ });
84
+ });
85
+
86
+ describe('validateMessages', () => {
87
+ it('should validate correct messages', () => {
88
+ const messages: Message[] = [
89
+ { role: 'user', content: 'Hello!' }
90
+ ];
91
+
92
+ const result = validateMessages(messages);
93
+
94
+ expect(result.isValid).toBe(true);
95
+ });
96
+
97
+ it('should reject empty messages array', () => {
98
+ const messages: Message[] = [];
99
+
100
+ const result = validateMessages(messages);
101
+
102
+ expect(result.isValid).toBe(false);
103
+ expect(result.error).toContain('cannot be empty');
104
+ });
105
+
106
+ it('should reject messages without role', () => {
107
+ const messages = [
108
+ { content: 'Hello!' }
109
+ ] as unknown as Message[];
110
+
111
+ const result = validateMessages(messages);
112
+
113
+ expect(result.isValid).toBe(false);
114
+ expect(result.error).toContain('role and content');
115
+ });
116
+
117
+ it('should reject messages without content', () => {
118
+ const messages = [
119
+ { role: 'user' }
120
+ ] as unknown as Message[];
121
+
122
+ const result = validateMessages(messages);
123
+
124
+ expect(result.isValid).toBe(false);
125
+ expect(result.error).toContain('role and content');
126
+ });
127
+
128
+ it('should reject messages with invalid role', () => {
129
+ const messages = [
130
+ { role: 'invalid', content: 'Hello!' }
131
+ ] as unknown as Message[];
132
+
133
+ const result = validateMessages(messages);
134
+
135
+ expect(result.isValid).toBe(false);
136
+ expect(result.error).toContain('Invalid message role');
137
+ });
138
+
139
+ it('should require at least one non-system message', () => {
140
+ const messages: Message[] = [
141
+ { role: 'system', content: 'System instruction' }
142
+ ];
143
+
144
+ const result = validateMessages(messages);
145
+
146
+ expect(result.isValid).toBe(false);
147
+ expect(result.error).toContain('non-system message');
148
+ });
149
+
150
+ it('should allow system message with user message', () => {
151
+ const messages: Message[] = [
152
+ { role: 'system', content: 'System instruction' },
153
+ { role: 'user', content: 'Hello!' }
154
+ ];
155
+
156
+ const result = validateMessages(messages);
157
+
158
+ expect(result.isValid).toBe(true);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,150 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code only within the scope of the project it was delivered for.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { ClaudeModelRegistry } from '../src/models/model-registry';
10
+
11
+ describe('ClaudeModelRegistry', () => {
12
+ describe('getAllModels', () => {
13
+ it('should return all Claude 3 models', () => {
14
+ const models = ClaudeModelRegistry.getAllModels();
15
+
16
+ expect(models).toHaveLength(3);
17
+ expect(models.map(m => m.name)).toEqual([
18
+ 'Claude 3 Opus',
19
+ 'Claude 3 Sonnet',
20
+ 'Claude 3 Haiku'
21
+ ]);
22
+ });
23
+
24
+ it('should include model capabilities', () => {
25
+ const models = ClaudeModelRegistry.getAllModels();
26
+ const opus = models.find(m => m.id === 'claude-3-opus-20240229');
27
+
28
+ expect(opus?.capabilities).toContain('chat');
29
+ expect(opus?.capabilities).toContain('completion');
30
+ expect(opus?.capabilities).toContain('vision');
31
+ expect(opus?.capabilities).toContain('extended-context');
32
+ expect(opus?.capabilities).toContain('analysis');
33
+ });
34
+
35
+ it('should include pricing information', () => {
36
+ const models = ClaudeModelRegistry.getAllModels();
37
+
38
+ models.forEach(model => {
39
+ expect(model.pricing).toBeDefined();
40
+ expect(model.pricing?.inputPricePerToken).toBeGreaterThan(0);
41
+ expect(model.pricing?.outputPricePerToken).toBeGreaterThan(0);
42
+ expect(model.pricing?.currency).toBe('USD');
43
+ });
44
+ });
45
+
46
+ it('should have 200K context window for all models', () => {
47
+ const models = ClaudeModelRegistry.getAllModels();
48
+
49
+ models.forEach(model => {
50
+ expect(model.contextWindow).toBe(200000);
51
+ });
52
+ });
53
+
54
+ it('should have 4096 max output tokens for all models', () => {
55
+ const models = ClaudeModelRegistry.getAllModels();
56
+
57
+ models.forEach(model => {
58
+ expect(model.maxOutputTokens).toBe(4096);
59
+ });
60
+ });
61
+ });
62
+
63
+ describe('getModel', () => {
64
+ it('should return specific model by ID', () => {
65
+ const opus = ClaudeModelRegistry.getModel('claude-3-opus-20240229');
66
+
67
+ expect(opus).toBeDefined();
68
+ expect(opus?.name).toBe('Claude 3 Opus');
69
+ });
70
+
71
+ it('should return undefined for unknown model ID', () => {
72
+ const unknown = ClaudeModelRegistry.getModel('unknown-model');
73
+
74
+ expect(unknown).toBeUndefined();
75
+ });
76
+
77
+ it('should return correct model info', () => {
78
+ const sonnet = ClaudeModelRegistry.getModel('claude-3-sonnet-20240229');
79
+
80
+ expect(sonnet).toBeDefined();
81
+ expect(sonnet?.id).toBe('claude-3-sonnet-20240229');
82
+ expect(sonnet?.name).toBe('Claude 3 Sonnet');
83
+ expect(sonnet?.description).toContain('Balanced');
84
+ });
85
+ });
86
+
87
+ describe('isValidModel', () => {
88
+ it('should return true for valid model IDs', () => {
89
+ expect(ClaudeModelRegistry.isValidModel('claude-3-opus-20240229')).toBe(true);
90
+ expect(ClaudeModelRegistry.isValidModel('claude-3-sonnet-20240229')).toBe(true);
91
+ expect(ClaudeModelRegistry.isValidModel('claude-3-haiku-20240307')).toBe(true);
92
+ });
93
+
94
+ it('should return false for invalid model IDs', () => {
95
+ expect(ClaudeModelRegistry.isValidModel('unknown-model')).toBe(false);
96
+ expect(ClaudeModelRegistry.isValidModel('')).toBe(false);
97
+ });
98
+ });
99
+
100
+ describe('getModelPricing', () => {
101
+ it('should return correct pricing for Opus', () => {
102
+ const pricing = ClaudeModelRegistry.getModelPricing('claude-3-opus-20240229');
103
+
104
+ expect(pricing.inputPrice).toBe(15);
105
+ expect(pricing.outputPrice).toBe(75);
106
+ });
107
+
108
+ it('should return correct pricing for Sonnet', () => {
109
+ const pricing = ClaudeModelRegistry.getModelPricing('claude-3-sonnet-20240229');
110
+
111
+ expect(pricing.inputPrice).toBe(3);
112
+ expect(pricing.outputPrice).toBe(15);
113
+ });
114
+
115
+ it('should return correct pricing for Haiku', () => {
116
+ const pricing = ClaudeModelRegistry.getModelPricing('claude-3-haiku-20240307');
117
+
118
+ expect(pricing.inputPrice).toBe(0.25);
119
+ expect(pricing.outputPrice).toBe(1.25);
120
+ });
121
+
122
+ it('should return default pricing for unknown model', () => {
123
+ const pricing = ClaudeModelRegistry.getModelPricing('unknown-model');
124
+
125
+ expect(pricing.inputPrice).toBe(3); // Sonnet default
126
+ expect(pricing.outputPrice).toBe(15);
127
+ });
128
+ });
129
+
130
+ describe('pricing comparison', () => {
131
+ it('should have Opus as most expensive', () => {
132
+ const opus = ClaudeModelRegistry.getModelPricing('claude-3-opus-20240229');
133
+ const sonnet = ClaudeModelRegistry.getModelPricing('claude-3-sonnet-20240229');
134
+ const haiku = ClaudeModelRegistry.getModelPricing('claude-3-haiku-20240307');
135
+
136
+ expect(opus.inputPrice).toBeGreaterThan(sonnet.inputPrice);
137
+ expect(opus.inputPrice).toBeGreaterThan(haiku.inputPrice);
138
+ expect(opus.outputPrice).toBeGreaterThan(sonnet.outputPrice);
139
+ expect(opus.outputPrice).toBeGreaterThan(haiku.outputPrice);
140
+ });
141
+
142
+ it('should have Haiku as least expensive', () => {
143
+ const sonnet = ClaudeModelRegistry.getModelPricing('claude-3-sonnet-20240229');
144
+ const haiku = ClaudeModelRegistry.getModelPricing('claude-3-haiku-20240307');
145
+
146
+ expect(haiku.inputPrice).toBeLessThan(sonnet.inputPrice);
147
+ expect(haiku.outputPrice).toBeLessThan(sonnet.outputPrice);
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,54 @@
1
+ import { AIProvider, CompletionRequest, CompletionResponse, StreamChunk, EmbeddingRequest, EmbeddingResponse, ModerationResponse, ModelInfo, HealthStatus, CostEstimate } from '@bernierllc/ai-provider-core';
2
+ import { AnthropicProviderConfig } from './types';
3
+ /**
4
+ * Anthropic Provider Implementation
5
+ * Concrete implementation of the AI provider interface for Anthropic's Claude API
6
+ */
7
+ export declare class AnthropicProvider extends AIProvider {
8
+ private client;
9
+ constructor(config: AnthropicProviderConfig);
10
+ /**
11
+ * Generate text completion using Anthropic Claude API
12
+ */
13
+ complete(request: CompletionRequest): Promise<CompletionResponse>;
14
+ /**
15
+ * Generate streaming text completion
16
+ */
17
+ streamComplete(request: CompletionRequest): AsyncGenerator<StreamChunk, void, unknown>;
18
+ /**
19
+ * Generate embeddings (Anthropic doesn't provide embeddings API)
20
+ * Returns error indicating feature not supported
21
+ */
22
+ generateEmbeddings(_request: EmbeddingRequest): Promise<EmbeddingResponse>;
23
+ /**
24
+ * Check content moderation (Anthropic doesn't provide moderation API)
25
+ * Returns success with no flags (Claude has built-in safety)
26
+ */
27
+ moderate(_content: string): Promise<ModerationResponse>;
28
+ /**
29
+ * Get available Anthropic models
30
+ */
31
+ getAvailableModels(): Promise<ModelInfo[]>;
32
+ /**
33
+ * Check Anthropic API health
34
+ */
35
+ checkHealth(): Promise<HealthStatus>;
36
+ /**
37
+ * Extended context completion (200K tokens for all Claude 3 models)
38
+ */
39
+ extendedContextCompletion(request: CompletionRequest & {
40
+ enableExtendedContext?: boolean;
41
+ }): Promise<CompletionResponse>;
42
+ /**
43
+ * Vision analysis (All Claude 3 models support vision)
44
+ */
45
+ analyzeImage(imageData: string | Buffer, prompt: string, model?: string, maxTokens?: number, temperature?: number): Promise<CompletionResponse>;
46
+ /**
47
+ * Estimate cost using Anthropic pricing
48
+ */
49
+ estimateCost(request: CompletionRequest): CostEstimate;
50
+ /**
51
+ * Map Anthropic stop reason to unified format
52
+ */
53
+ private mapStopReason;
54
+ }