@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,2 @@
1
+ export * from './message-conversion';
2
+ export * from './error-handling';
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code only within the scope of the project it was delivered for.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ __exportStar(require("./message-conversion"), exports);
25
+ __exportStar(require("./error-handling"), exports);
@@ -0,0 +1,17 @@
1
+ import { Message } from '@bernierllc/ai-provider-core';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ /**
4
+ * Convert messages from unified format to Anthropic format
5
+ * Extracts system messages separately (Anthropic uses system parameter, not messages array)
6
+ */
7
+ export declare function convertMessagesToAnthropicFormat(messages: Message[]): {
8
+ system?: string;
9
+ messages: Anthropic.MessageParam[];
10
+ };
11
+ /**
12
+ * Validate message format before conversion
13
+ */
14
+ export declare function validateMessages(messages: Message[]): {
15
+ isValid: boolean;
16
+ error?: string;
17
+ };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code only within the scope of the project it was delivered for.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.convertMessagesToAnthropicFormat = convertMessagesToAnthropicFormat;
11
+ exports.validateMessages = validateMessages;
12
+ /**
13
+ * Convert messages from unified format to Anthropic format
14
+ * Extracts system messages separately (Anthropic uses system parameter, not messages array)
15
+ */
16
+ function convertMessagesToAnthropicFormat(messages) {
17
+ // Extract system messages
18
+ const systemMessages = messages.filter(m => m.role === 'system');
19
+ const system = systemMessages.length > 0
20
+ ? systemMessages.map(m => m.content).join('\n\n')
21
+ : undefined;
22
+ // Convert remaining messages to Anthropic format
23
+ const anthropicMessages = messages
24
+ .filter(m => m.role !== 'system')
25
+ .map(msg => ({
26
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
27
+ content: msg.content
28
+ }));
29
+ return { system, messages: anthropicMessages };
30
+ }
31
+ /**
32
+ * Validate message format before conversion
33
+ */
34
+ function validateMessages(messages) {
35
+ if (!messages || messages.length === 0) {
36
+ return {
37
+ isValid: false,
38
+ error: 'Messages array cannot be empty'
39
+ };
40
+ }
41
+ // Check that all messages have required fields
42
+ for (const msg of messages) {
43
+ if (!msg.role || !msg.content) {
44
+ return {
45
+ isValid: false,
46
+ error: 'All messages must have role and content'
47
+ };
48
+ }
49
+ if (!['system', 'user', 'assistant'].includes(msg.role)) {
50
+ return {
51
+ isValid: false,
52
+ error: `Invalid message role: ${msg.role}`
53
+ };
54
+ }
55
+ }
56
+ // Ensure non-system messages exist
57
+ const nonSystemMessages = messages.filter(m => m.role !== 'system');
58
+ if (nonSystemMessages.length === 0) {
59
+ return {
60
+ isValid: false,
61
+ error: 'At least one non-system message is required'
62
+ };
63
+ }
64
+ return { isValid: true };
65
+ }
package/jest.config.js ADDED
@@ -0,0 +1,30 @@
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
+ module.exports = {
10
+ preset: 'ts-jest',
11
+ testEnvironment: 'node',
12
+ roots: ['<rootDir>/__tests__'],
13
+ testMatch: ['**/__tests__/**/*.test.ts'],
14
+ collectCoverageFrom: [
15
+ 'src/**/*.{ts,tsx}',
16
+ '!src/**/*.d.ts',
17
+ '!src/index.ts'
18
+ ],
19
+ coverageThreshold: {
20
+ global: {
21
+ branches: 90,
22
+ functions: 90,
23
+ lines: 90,
24
+ statements: 90
25
+ }
26
+ },
27
+ coverageDirectory: 'coverage',
28
+ verbose: true,
29
+ testTimeout: 30000
30
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@bernierllc/ai-provider-anthropic",
3
+ "version": "1.0.0",
4
+ "description": "Anthropic Claude API adapter implementing the unified AI provider interface",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest --watch",
10
+ "test:run": "jest",
11
+ "test:coverage": "jest --coverage",
12
+ "lint": "eslint src --ext .ts",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "provider",
18
+ "anthropic",
19
+ "claude",
20
+ "adapter",
21
+ "claude-3",
22
+ "vision",
23
+ "streaming"
24
+ ],
25
+ "author": "Bernier LLC",
26
+ "license": "SEE LICENSE IN LICENSE",
27
+ "dependencies": {
28
+ "@anthropic-ai/sdk": "^0.20.0",
29
+ "@bernierllc/ai-provider-core": "^1.0.0",
30
+ "@bernierllc/retry-policy": "^1.0.0",
31
+ "@bernierllc/logger": "^1.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@bernierllc/neverhub-adapter": "^1.0.0",
35
+ "@types/jest": "^29.5.0",
36
+ "@types/node": "^20.0.0",
37
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
38
+ "@typescript-eslint/parser": "^6.0.0",
39
+ "eslint": "^8.50.0",
40
+ "jest": "^29.7.0",
41
+ "ts-jest": "^29.1.0",
42
+ "typescript": "^5.3.0"
43
+ },
44
+ "bernierllc": {
45
+ "category": "core",
46
+ "tags": [
47
+ "ai",
48
+ "provider",
49
+ "anthropic",
50
+ "claude",
51
+ "adapter"
52
+ ],
53
+ "integration": {
54
+ "neverhub": "optional",
55
+ "logger": "integrated",
56
+ "docs-suite": "ready"
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,392 @@
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 {
11
+ AIProvider,
12
+ CompletionRequest,
13
+ CompletionResponse,
14
+ StreamChunk,
15
+ EmbeddingRequest,
16
+ EmbeddingResponse,
17
+ ModerationResponse,
18
+ ModelInfo,
19
+ HealthStatus,
20
+ CostEstimate
21
+ } from '@bernierllc/ai-provider-core';
22
+ import { AnthropicProviderConfig, AnthropicStopReason } from './types';
23
+ import { ClaudeModelRegistry } from './models/model-registry';
24
+ import { convertMessagesToAnthropicFormat, validateMessages } from './utils/message-conversion';
25
+ import { handleAnthropicError } from './utils/error-handling';
26
+
27
+ /**
28
+ * Anthropic Provider Implementation
29
+ * Concrete implementation of the AI provider interface for Anthropic's Claude API
30
+ */
31
+ export class AnthropicProvider extends AIProvider {
32
+ private client: Anthropic;
33
+
34
+ constructor(config: AnthropicProviderConfig) {
35
+ super(config);
36
+ this.client = new Anthropic({
37
+ apiKey: config.apiKey,
38
+ baseURL: config.baseURL,
39
+ timeout: config.timeout || 60000,
40
+ maxRetries: config.maxRetries || 3
41
+ });
42
+ }
43
+
44
+ // ============================================
45
+ // CORE OPERATIONS
46
+ // ============================================
47
+
48
+ /**
49
+ * Generate text completion using Anthropic Claude API
50
+ */
51
+ async complete(request: CompletionRequest): Promise<CompletionResponse> {
52
+ // Validate request
53
+ const validation = this.validateRequest(request);
54
+ if (!validation.isValid) {
55
+ return {
56
+ success: false,
57
+ error: validation.errors.join(', ')
58
+ };
59
+ }
60
+
61
+ // Validate messages
62
+ const messageValidation = validateMessages(request.messages);
63
+ if (!messageValidation.isValid) {
64
+ return {
65
+ success: false,
66
+ error: messageValidation.error
67
+ };
68
+ }
69
+
70
+ try {
71
+ // Convert messages to Anthropic format
72
+ const { system, messages } = convertMessagesToAnthropicFormat(request.messages);
73
+
74
+ const completion = await this.client.messages.create({
75
+ model: request.model || this.config.defaultModel || 'claude-3-opus-20240229',
76
+ system,
77
+ messages,
78
+ max_tokens: request.maxTokens || 4096,
79
+ temperature: request.temperature,
80
+ top_p: request.topP,
81
+ stop_sequences: request.stop,
82
+ metadata: request.user ? {
83
+ user_id: request.user
84
+ } : undefined
85
+ });
86
+
87
+ // Extract content from response
88
+ const content = completion.content
89
+ .filter((block): block is Anthropic.TextBlock => block.type === 'text')
90
+ .map(block => block.text)
91
+ .join('');
92
+
93
+ return {
94
+ success: true,
95
+ content,
96
+ finishReason: this.mapStopReason(completion.stop_reason),
97
+ usage: {
98
+ promptTokens: completion.usage.input_tokens,
99
+ completionTokens: completion.usage.output_tokens,
100
+ totalTokens: completion.usage.input_tokens + completion.usage.output_tokens
101
+ },
102
+ model: completion.model,
103
+ metadata: {
104
+ id: completion.id,
105
+ type: completion.type,
106
+ role: completion.role,
107
+ stopSequence: completion.stop_sequence
108
+ }
109
+ };
110
+ } catch (error) {
111
+ const aiError = handleAnthropicError(error);
112
+ return {
113
+ success: false,
114
+ error: aiError.message
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate streaming text completion
121
+ */
122
+ async *streamComplete(
123
+ request: CompletionRequest
124
+ ): AsyncGenerator<StreamChunk, void, unknown> {
125
+ const validation = this.validateRequest(request);
126
+ if (!validation.isValid) {
127
+ throw new Error(validation.errors.join(', '));
128
+ }
129
+
130
+ const messageValidation = validateMessages(request.messages);
131
+ if (!messageValidation.isValid) {
132
+ throw new Error(messageValidation.error || 'Invalid messages');
133
+ }
134
+
135
+ try {
136
+ const { system, messages } = convertMessagesToAnthropicFormat(request.messages);
137
+
138
+ const stream = this.client.messages.stream({
139
+ model: request.model || this.config.defaultModel || 'claude-3-opus-20240229',
140
+ system,
141
+ messages,
142
+ max_tokens: request.maxTokens || 4096,
143
+ temperature: request.temperature,
144
+ top_p: request.topP,
145
+ stop_sequences: request.stop
146
+ });
147
+
148
+ let finalUsage: { promptTokens: number; completionTokens: number; totalTokens: number } | undefined;
149
+ let finalStopReason: AnthropicStopReason | undefined;
150
+
151
+ for await (const event of stream) {
152
+ if (event.type === 'content_block_delta') {
153
+ const delta = event.delta;
154
+ if (delta.type === 'text_delta') {
155
+ yield {
156
+ delta: delta.text,
157
+ finishReason: undefined
158
+ };
159
+ }
160
+ }
161
+
162
+ if (event.type === 'message_delta') {
163
+ // Capture usage information from message_delta event
164
+ const usage = (event as unknown as Record<string, unknown>).usage as { input_tokens: number; output_tokens: number } | undefined;
165
+ if (usage) {
166
+ finalUsage = {
167
+ promptTokens: usage.input_tokens,
168
+ completionTokens: usage.output_tokens,
169
+ totalTokens: usage.input_tokens + usage.output_tokens
170
+ };
171
+ }
172
+ }
173
+
174
+ if (event.type === 'message_stop') {
175
+ // Capture stop reason
176
+ finalStopReason = null; // message_stop doesn't provide stop_reason directly
177
+
178
+ yield {
179
+ delta: '',
180
+ finishReason: this.mapStopReason(finalStopReason),
181
+ usage: finalUsage
182
+ };
183
+ }
184
+ }
185
+ } catch (error) {
186
+ throw handleAnthropicError(error);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Generate embeddings (Anthropic doesn't provide embeddings API)
192
+ * Returns error indicating feature not supported
193
+ */
194
+ generateEmbeddings(
195
+ _request: EmbeddingRequest
196
+ ): Promise<EmbeddingResponse> {
197
+ return Promise.resolve({
198
+ success: false,
199
+ error: 'Anthropic does not provide an embeddings API. Use OpenAI provider for embeddings.'
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Check content moderation (Anthropic doesn't provide moderation API)
205
+ * Returns success with no flags (Claude has built-in safety)
206
+ */
207
+ moderate(_content: string): Promise<ModerationResponse> {
208
+ // Claude has built-in safety features
209
+ // We can return success with no flags as Claude refuses unsafe content
210
+ return Promise.resolve({
211
+ success: true,
212
+ flagged: false,
213
+ categories: {},
214
+ categoryScores: {}
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Get available Anthropic models
220
+ */
221
+ getAvailableModels(): Promise<ModelInfo[]> {
222
+ // Anthropic doesn't provide a models list endpoint
223
+ // Return cached model information
224
+ return Promise.resolve(ClaudeModelRegistry.getAllModels());
225
+ }
226
+
227
+ /**
228
+ * Check Anthropic API health
229
+ */
230
+ async checkHealth(): Promise<HealthStatus> {
231
+ const startTime = Date.now();
232
+
233
+ try {
234
+ // Simple test request with minimal tokens
235
+ await this.client.messages.create({
236
+ model: 'claude-3-haiku-20240307',
237
+ max_tokens: 10,
238
+ messages: [
239
+ { role: 'user', content: 'Hi' }
240
+ ]
241
+ });
242
+
243
+ return {
244
+ status: 'healthy',
245
+ latency: Date.now() - startTime,
246
+ lastChecked: new Date()
247
+ };
248
+ } catch (error) {
249
+ return {
250
+ status: 'unavailable',
251
+ latency: Date.now() - startTime,
252
+ lastChecked: new Date(),
253
+ details: {
254
+ error: error instanceof Error ? error.message : 'Unknown error'
255
+ }
256
+ };
257
+ }
258
+ }
259
+
260
+ // ============================================
261
+ // ANTHROPIC-SPECIFIC FEATURES
262
+ // ============================================
263
+
264
+ /**
265
+ * Extended context completion (200K tokens for all Claude 3 models)
266
+ */
267
+ async extendedContextCompletion(
268
+ request: CompletionRequest & { enableExtendedContext?: boolean }
269
+ ): Promise<CompletionResponse> {
270
+ // All Claude 3 models support 200K context window
271
+ const model = request.model || 'claude-3-opus-20240229';
272
+
273
+ return this.complete({
274
+ ...request,
275
+ model
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Vision analysis (All Claude 3 models support vision)
281
+ */
282
+ async analyzeImage(
283
+ imageData: string | Buffer,
284
+ prompt: string,
285
+ model: string = 'claude-3-opus-20240229',
286
+ maxTokens?: number,
287
+ temperature?: number
288
+ ): Promise<CompletionResponse> {
289
+ try {
290
+ // Convert image to base64 if needed
291
+ const base64Image = Buffer.isBuffer(imageData)
292
+ ? imageData.toString('base64')
293
+ : imageData;
294
+
295
+ const completion = await this.client.messages.create({
296
+ model,
297
+ max_tokens: maxTokens || 4096,
298
+ temperature,
299
+ messages: [
300
+ {
301
+ role: 'user',
302
+ content: [
303
+ {
304
+ type: 'image',
305
+ source: {
306
+ type: 'base64',
307
+ media_type: 'image/jpeg',
308
+ data: base64Image
309
+ }
310
+ },
311
+ {
312
+ type: 'text',
313
+ text: prompt
314
+ }
315
+ ]
316
+ }
317
+ ]
318
+ });
319
+
320
+ const content = completion.content
321
+ .filter((block): block is Anthropic.TextBlock => block.type === 'text')
322
+ .map(block => block.text)
323
+ .join('');
324
+
325
+ return {
326
+ success: true,
327
+ content,
328
+ finishReason: this.mapStopReason(completion.stop_reason),
329
+ usage: {
330
+ promptTokens: completion.usage.input_tokens,
331
+ completionTokens: completion.usage.output_tokens,
332
+ totalTokens: completion.usage.input_tokens + completion.usage.output_tokens
333
+ },
334
+ model: completion.model
335
+ };
336
+ } catch (error) {
337
+ const aiError = handleAnthropicError(error);
338
+ return {
339
+ success: false,
340
+ error: aiError.message
341
+ };
342
+ }
343
+ }
344
+
345
+ // ============================================
346
+ // COST ESTIMATION (OVERRIDE)
347
+ // ============================================
348
+
349
+ /**
350
+ * Estimate cost using Anthropic pricing
351
+ */
352
+ estimateCost(request: CompletionRequest): CostEstimate {
353
+ const model = request.model || this.config.defaultModel || 'claude-3-opus-20240229';
354
+ const pricing = ClaudeModelRegistry.getModelPricing(model);
355
+
356
+ const inputTokens = this.estimateTokens(
357
+ request.messages.map(m => m.content).join(' ')
358
+ );
359
+ const outputTokens = request.maxTokens || 4096;
360
+
361
+ const inputCost = (inputTokens / 1000000) * pricing.inputPrice;
362
+ const outputCost = (outputTokens / 1000000) * pricing.outputPrice;
363
+
364
+ return {
365
+ inputTokens,
366
+ outputTokens,
367
+ totalTokens: inputTokens + outputTokens,
368
+ estimatedCostUSD: inputCost + outputCost,
369
+ currency: 'USD'
370
+ };
371
+ }
372
+
373
+ // ============================================
374
+ // PRIVATE METHODS
375
+ // ============================================
376
+
377
+ /**
378
+ * Map Anthropic stop reason to unified format
379
+ */
380
+ private mapStopReason(stopReason: AnthropicStopReason): 'stop' | 'length' | 'content_filter' {
381
+ switch (stopReason) {
382
+ case 'end_turn':
383
+ return 'stop';
384
+ case 'max_tokens':
385
+ return 'length';
386
+ case 'stop_sequence':
387
+ return 'stop';
388
+ default:
389
+ return 'stop';
390
+ }
391
+ }
392
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
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
+ // Main provider class
10
+ export { AnthropicProvider } from './AnthropicProvider';
11
+
12
+ // Types
13
+ export * from './types';
14
+
15
+ // Models
16
+ export { ClaudeModelRegistry } from './models/model-registry';
17
+
18
+ // Utilities
19
+ export * from './utils';