@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.
- package/.eslintrc.js +34 -0
- package/README.md +310 -0
- package/__tests__/AnthropicProvider.test.ts +655 -0
- package/__tests__/error-handling.test.ts +208 -0
- package/__tests__/message-conversion.test.ts +161 -0
- package/__tests__/model-registry.test.ts +150 -0
- package/dist/AnthropicProvider.d.ts +54 -0
- package/dist/AnthropicProvider.js +337 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +34 -0
- package/dist/models/model-registry.d.ts +31 -0
- package/dist/models/model-registry.js +113 -0
- package/dist/types/anthropic-types.d.ts +46 -0
- package/dist/types/anthropic-types.js +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +24 -0
- package/dist/utils/error-handling.d.ts +12 -0
- package/dist/utils/error-handling.js +67 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +25 -0
- package/dist/utils/message-conversion.d.ts +17 -0
- package/dist/utils/message-conversion.js +65 -0
- package/jest.config.js +30 -0
- package/package.json +59 -0
- package/src/AnthropicProvider.ts +392 -0
- package/src/index.ts +19 -0
- package/src/models/model-registry.ts +120 -0
- package/src/types/anthropic-types.ts +60 -0
- package/src/types/index.ts +9 -0
- package/src/utils/error-handling.ts +71 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/message-conversion.ts +78 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,655 @@
|
|
|
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 { AnthropicProvider } from '../src/AnthropicProvider';
|
|
10
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
11
|
+
|
|
12
|
+
// Mock the Anthropic SDK
|
|
13
|
+
jest.mock('@anthropic-ai/sdk');
|
|
14
|
+
|
|
15
|
+
describe('AnthropicProvider', () => {
|
|
16
|
+
let provider: AnthropicProvider;
|
|
17
|
+
let mockCreate: jest.Mock;
|
|
18
|
+
let mockStream: jest.Mock;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
|
|
23
|
+
// Create mock functions
|
|
24
|
+
mockCreate = jest.fn();
|
|
25
|
+
mockStream = jest.fn();
|
|
26
|
+
|
|
27
|
+
// Mock the Anthropic constructor
|
|
28
|
+
(Anthropic as jest.MockedClass<typeof Anthropic>).mockImplementation(() => {
|
|
29
|
+
return {
|
|
30
|
+
messages: {
|
|
31
|
+
create: mockCreate,
|
|
32
|
+
stream: mockStream
|
|
33
|
+
}
|
|
34
|
+
} as unknown as Anthropic;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
provider = new AnthropicProvider({
|
|
38
|
+
providerName: 'anthropic',
|
|
39
|
+
apiKey: 'test-api-key',
|
|
40
|
+
defaultModel: 'claude-3-opus-20240229'
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('constructor', () => {
|
|
45
|
+
it('should create provider with config', () => {
|
|
46
|
+
expect(provider).toBeDefined();
|
|
47
|
+
expect(provider.getProviderName()).toBe('anthropic');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should initialize Anthropic client', () => {
|
|
51
|
+
expect(Anthropic).toHaveBeenCalledWith({
|
|
52
|
+
apiKey: 'test-api-key',
|
|
53
|
+
baseURL: undefined,
|
|
54
|
+
timeout: 60000,
|
|
55
|
+
maxRetries: 3
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should use custom timeout and retries', () => {
|
|
60
|
+
new AnthropicProvider({
|
|
61
|
+
providerName: 'anthropic',
|
|
62
|
+
apiKey: 'test-key',
|
|
63
|
+
timeout: 30000,
|
|
64
|
+
maxRetries: 5
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(Anthropic).toHaveBeenCalledWith(
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
maxRetries: 5
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('complete', () => {
|
|
77
|
+
it('should generate text completion successfully', async () => {
|
|
78
|
+
const mockResponse = {
|
|
79
|
+
id: 'msg_123',
|
|
80
|
+
type: 'message',
|
|
81
|
+
role: 'assistant',
|
|
82
|
+
model: 'claude-3-opus-20240229',
|
|
83
|
+
content: [
|
|
84
|
+
{ type: 'text', text: 'Hello! How can I help you?' }
|
|
85
|
+
],
|
|
86
|
+
stop_reason: 'end_turn',
|
|
87
|
+
stop_sequence: null,
|
|
88
|
+
usage: {
|
|
89
|
+
input_tokens: 10,
|
|
90
|
+
output_tokens: 20
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
95
|
+
|
|
96
|
+
const result = await provider.complete({
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: 'user', content: 'Hello!' }
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.success).toBe(true);
|
|
103
|
+
expect(result.content).toBe('Hello! How can I help you?');
|
|
104
|
+
expect(result.usage).toEqual({
|
|
105
|
+
promptTokens: 10,
|
|
106
|
+
completionTokens: 20,
|
|
107
|
+
totalTokens: 30
|
|
108
|
+
});
|
|
109
|
+
expect(result.finishReason).toBe('stop');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle system messages correctly', async () => {
|
|
113
|
+
const mockResponse = {
|
|
114
|
+
id: 'msg_123',
|
|
115
|
+
type: 'message',
|
|
116
|
+
role: 'assistant',
|
|
117
|
+
model: 'claude-3-opus-20240229',
|
|
118
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
119
|
+
stop_reason: 'end_turn',
|
|
120
|
+
stop_sequence: null,
|
|
121
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
125
|
+
|
|
126
|
+
await provider.complete({
|
|
127
|
+
messages: [
|
|
128
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
129
|
+
{ role: 'user', content: 'Hello!' }
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
system: 'You are helpful.',
|
|
136
|
+
messages: [{ role: 'user', content: 'Hello!' }]
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should use custom parameters', async () => {
|
|
142
|
+
const mockResponse = {
|
|
143
|
+
id: 'msg_123',
|
|
144
|
+
type: 'message',
|
|
145
|
+
role: 'assistant',
|
|
146
|
+
model: 'claude-3-sonnet-20240229',
|
|
147
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
148
|
+
stop_reason: 'end_turn',
|
|
149
|
+
stop_sequence: null,
|
|
150
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
154
|
+
|
|
155
|
+
await provider.complete({
|
|
156
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
157
|
+
model: 'claude-3-sonnet-20240229',
|
|
158
|
+
maxTokens: 1000,
|
|
159
|
+
temperature: 0.7,
|
|
160
|
+
topP: 0.9,
|
|
161
|
+
stop: ['STOP']
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
model: 'claude-3-sonnet-20240229',
|
|
167
|
+
max_tokens: 1000,
|
|
168
|
+
temperature: 0.7,
|
|
169
|
+
top_p: 0.9,
|
|
170
|
+
stop_sequences: ['STOP']
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle max_tokens stop reason', async () => {
|
|
176
|
+
const mockResponse = {
|
|
177
|
+
id: 'msg_123',
|
|
178
|
+
type: 'message',
|
|
179
|
+
role: 'assistant',
|
|
180
|
+
model: 'claude-3-opus-20240229',
|
|
181
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
182
|
+
stop_reason: 'max_tokens',
|
|
183
|
+
stop_sequence: null,
|
|
184
|
+
usage: { input_tokens: 10, output_tokens: 100 }
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
188
|
+
|
|
189
|
+
const result = await provider.complete({
|
|
190
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.finishReason).toBe('length');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should return error on API failure', async () => {
|
|
197
|
+
const apiError = Object.assign(
|
|
198
|
+
new Error('Server error'),
|
|
199
|
+
{
|
|
200
|
+
status: 500,
|
|
201
|
+
headers: {}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
// Make it look like an Anthropic.APIError
|
|
205
|
+
Object.setPrototypeOf(apiError, Anthropic.APIError.prototype);
|
|
206
|
+
|
|
207
|
+
mockCreate.mockRejectedValue(apiError);
|
|
208
|
+
|
|
209
|
+
const result = await provider.complete({
|
|
210
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(result.success).toBe(false);
|
|
214
|
+
expect(result.error).toContain('Anthropic API Error');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return error for empty messages', async () => {
|
|
218
|
+
const result = await provider.complete({
|
|
219
|
+
messages: []
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.success).toBe(false);
|
|
223
|
+
expect(result.error).toContain('cannot be empty');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle multiple content blocks', async () => {
|
|
227
|
+
const mockResponse = {
|
|
228
|
+
id: 'msg_123',
|
|
229
|
+
type: 'message',
|
|
230
|
+
role: 'assistant',
|
|
231
|
+
model: 'claude-3-opus-20240229',
|
|
232
|
+
content: [
|
|
233
|
+
{ type: 'text', text: 'First part. ' },
|
|
234
|
+
{ type: 'text', text: 'Second part.' }
|
|
235
|
+
],
|
|
236
|
+
stop_reason: 'end_turn',
|
|
237
|
+
stop_sequence: null,
|
|
238
|
+
usage: { input_tokens: 10, output_tokens: 20 }
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
242
|
+
|
|
243
|
+
const result = await provider.complete({
|
|
244
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result.content).toBe('First part. Second part.');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('streamComplete', () => {
|
|
252
|
+
it('should stream completion chunks', async () => {
|
|
253
|
+
const mockEvents = [
|
|
254
|
+
{ type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello ' } },
|
|
255
|
+
{ type: 'content_block_delta', delta: { type: 'text_delta', text: 'world!' } },
|
|
256
|
+
{ type: 'message_delta', usage: { input_tokens: 10, output_tokens: 20 } },
|
|
257
|
+
{ type: 'message_stop' }
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const mockStreamObj = {
|
|
261
|
+
[Symbol.asyncIterator]: async function* () {
|
|
262
|
+
for (const event of mockEvents) {
|
|
263
|
+
yield event;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
mockStream.mockReturnValue(mockStreamObj as never);
|
|
269
|
+
|
|
270
|
+
const chunks: string[] = [];
|
|
271
|
+
for await (const chunk of provider.streamComplete({
|
|
272
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
273
|
+
})) {
|
|
274
|
+
if (chunk.delta) {
|
|
275
|
+
chunks.push(chunk.delta);
|
|
276
|
+
}
|
|
277
|
+
if (chunk.finishReason) {
|
|
278
|
+
expect(chunk.finishReason).toBe('stop');
|
|
279
|
+
expect(chunk.usage?.totalTokens).toBe(30);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
expect(chunks.join('')).toBe('Hello world!');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should throw error for invalid messages', async () => {
|
|
287
|
+
await expect(async () => {
|
|
288
|
+
for await (const _ of provider.streamComplete({ messages: [] })) {
|
|
289
|
+
// Should not reach here
|
|
290
|
+
}
|
|
291
|
+
}).rejects.toThrow('cannot be empty');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should handle streaming errors', async () => {
|
|
295
|
+
const error = new Anthropic.APIError(
|
|
296
|
+
429,
|
|
297
|
+
{ error: { message: 'Rate limit' }, type: 'error' },
|
|
298
|
+
'Rate limit',
|
|
299
|
+
{}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const errorStream = {
|
|
303
|
+
[Symbol.asyncIterator]: async function* () {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
mockStream.mockReturnValue(errorStream as never);
|
|
309
|
+
|
|
310
|
+
await expect(async () => {
|
|
311
|
+
for await (const _ of provider.streamComplete({
|
|
312
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
313
|
+
})) {
|
|
314
|
+
// Should not reach here
|
|
315
|
+
}
|
|
316
|
+
}).rejects.toThrow();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('generateEmbeddings', () => {
|
|
321
|
+
it('should return error indicating not supported', async () => {
|
|
322
|
+
const result = await provider.generateEmbeddings({
|
|
323
|
+
input: 'Test text'
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(result.success).toBe(false);
|
|
327
|
+
expect(result.error).toContain('does not provide an embeddings API');
|
|
328
|
+
expect(result.error).toContain('OpenAI');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('moderate', () => {
|
|
333
|
+
it('should return success with no flags', async () => {
|
|
334
|
+
const result = await provider.moderate('Test content');
|
|
335
|
+
|
|
336
|
+
expect(result.success).toBe(true);
|
|
337
|
+
expect(result.flagged).toBe(false);
|
|
338
|
+
expect(result.categories).toEqual({});
|
|
339
|
+
expect(result.categoryScores).toEqual({});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('getAvailableModels', () => {
|
|
344
|
+
it('should return all Claude models', async () => {
|
|
345
|
+
const models = await provider.getAvailableModels();
|
|
346
|
+
|
|
347
|
+
expect(models).toHaveLength(3);
|
|
348
|
+
expect(models.map(m => m.name)).toContain('Claude 3 Opus');
|
|
349
|
+
expect(models.map(m => m.name)).toContain('Claude 3 Sonnet');
|
|
350
|
+
expect(models.map(m => m.name)).toContain('Claude 3 Haiku');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('checkHealth', () => {
|
|
355
|
+
it('should return healthy status on success', async () => {
|
|
356
|
+
const mockResponse = {
|
|
357
|
+
id: 'msg_123',
|
|
358
|
+
type: 'message',
|
|
359
|
+
role: 'assistant',
|
|
360
|
+
model: 'claude-3-haiku-20240307',
|
|
361
|
+
content: [{ type: 'text', text: 'Hi' }],
|
|
362
|
+
stop_reason: 'end_turn',
|
|
363
|
+
stop_sequence: null,
|
|
364
|
+
usage: { input_tokens: 5, output_tokens: 5 }
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
368
|
+
|
|
369
|
+
const health = await provider.checkHealth();
|
|
370
|
+
|
|
371
|
+
expect(health.status).toBe('healthy');
|
|
372
|
+
expect(health.latency).toBeGreaterThanOrEqual(0);
|
|
373
|
+
expect(health.lastChecked).toBeInstanceOf(Date);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should return unavailable on failure', async () => {
|
|
377
|
+
mockCreate.mockRejectedValue(new Error('Network error'));
|
|
378
|
+
|
|
379
|
+
const health = await provider.checkHealth();
|
|
380
|
+
|
|
381
|
+
expect(health.status).toBe('unavailable');
|
|
382
|
+
expect(health.details?.error).toContain('Network error');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('analyzeImage', () => {
|
|
387
|
+
it('should analyze image from buffer', async () => {
|
|
388
|
+
const mockResponse = {
|
|
389
|
+
id: 'msg_123',
|
|
390
|
+
type: 'message',
|
|
391
|
+
role: 'assistant',
|
|
392
|
+
model: 'claude-3-opus-20240229',
|
|
393
|
+
content: [{ type: 'text', text: 'Image analysis result' }],
|
|
394
|
+
stop_reason: 'end_turn',
|
|
395
|
+
stop_sequence: null,
|
|
396
|
+
usage: { input_tokens: 100, output_tokens: 50 }
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
400
|
+
|
|
401
|
+
const imageBuffer = Buffer.from('fake-image-data');
|
|
402
|
+
const result = await provider.analyzeImage(imageBuffer, 'Describe this image');
|
|
403
|
+
|
|
404
|
+
expect(result.success).toBe(true);
|
|
405
|
+
expect(result.content).toBe('Image analysis result');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should analyze image from base64 string', async () => {
|
|
409
|
+
const mockResponse = {
|
|
410
|
+
id: 'msg_123',
|
|
411
|
+
type: 'message',
|
|
412
|
+
role: 'assistant',
|
|
413
|
+
model: 'claude-3-opus-20240229',
|
|
414
|
+
content: [{ type: 'text', text: 'Image description' }],
|
|
415
|
+
stop_reason: 'end_turn',
|
|
416
|
+
stop_sequence: null,
|
|
417
|
+
usage: { input_tokens: 100, output_tokens: 50 }
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
421
|
+
|
|
422
|
+
const result = await provider.analyzeImage('base64data', 'What is this?');
|
|
423
|
+
|
|
424
|
+
expect(result.success).toBe(true);
|
|
425
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
426
|
+
expect.objectContaining({
|
|
427
|
+
messages: expect.arrayContaining([
|
|
428
|
+
expect.objectContaining({
|
|
429
|
+
content: expect.arrayContaining([
|
|
430
|
+
expect.objectContaining({
|
|
431
|
+
type: 'image',
|
|
432
|
+
source: expect.objectContaining({
|
|
433
|
+
type: 'base64',
|
|
434
|
+
data: 'base64data'
|
|
435
|
+
})
|
|
436
|
+
}),
|
|
437
|
+
expect.objectContaining({
|
|
438
|
+
type: 'text',
|
|
439
|
+
text: 'What is this?'
|
|
440
|
+
})
|
|
441
|
+
])
|
|
442
|
+
})
|
|
443
|
+
])
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should use custom model and parameters', async () => {
|
|
449
|
+
const mockResponse = {
|
|
450
|
+
id: 'msg_123',
|
|
451
|
+
type: 'message',
|
|
452
|
+
role: 'assistant',
|
|
453
|
+
model: 'claude-3-sonnet-20240229',
|
|
454
|
+
content: [{ type: 'text', text: 'Analysis' }],
|
|
455
|
+
stop_reason: 'end_turn',
|
|
456
|
+
stop_sequence: null,
|
|
457
|
+
usage: { input_tokens: 100, output_tokens: 50 }
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
461
|
+
|
|
462
|
+
await provider.analyzeImage(
|
|
463
|
+
'imagedata',
|
|
464
|
+
'Analyze',
|
|
465
|
+
'claude-3-sonnet-20240229',
|
|
466
|
+
2000,
|
|
467
|
+
0.5
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
471
|
+
expect.objectContaining({
|
|
472
|
+
model: 'claude-3-sonnet-20240229',
|
|
473
|
+
max_tokens: 2000,
|
|
474
|
+
temperature: 0.5
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('extendedContextCompletion', () => {
|
|
481
|
+
it('should handle extended context requests', async () => {
|
|
482
|
+
const mockResponse = {
|
|
483
|
+
id: 'msg_123',
|
|
484
|
+
type: 'message',
|
|
485
|
+
role: 'assistant',
|
|
486
|
+
model: 'claude-3-opus-20240229',
|
|
487
|
+
content: [{ type: 'text', text: 'Summary of long document' }],
|
|
488
|
+
stop_reason: 'end_turn',
|
|
489
|
+
stop_sequence: null,
|
|
490
|
+
usage: { input_tokens: 50000, output_tokens: 500 }
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
494
|
+
|
|
495
|
+
const result = await provider.extendedContextCompletion({
|
|
496
|
+
messages: [{ role: 'user', content: 'Long document...' }],
|
|
497
|
+
enableExtendedContext: true
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(result.success).toBe(true);
|
|
501
|
+
expect(result.usage?.promptTokens).toBe(50000);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('estimateCost', () => {
|
|
506
|
+
it('should estimate cost for Opus', () => {
|
|
507
|
+
const cost = provider.estimateCost({
|
|
508
|
+
messages: [{ role: 'user', content: 'Hello world!' }],
|
|
509
|
+
model: 'claude-3-opus-20240229',
|
|
510
|
+
maxTokens: 100
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(cost.estimatedCostUSD).toBeGreaterThan(0);
|
|
514
|
+
expect(cost.currency).toBe('USD');
|
|
515
|
+
expect(cost.inputTokens).toBeGreaterThan(0);
|
|
516
|
+
expect(cost.outputTokens).toBe(100);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should use default model if not specified', () => {
|
|
520
|
+
const cost = provider.estimateCost({
|
|
521
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
522
|
+
maxTokens: 100
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(cost.estimatedCostUSD).toBeGreaterThan(0);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should calculate different costs for different models', () => {
|
|
529
|
+
const opusCost = provider.estimateCost({
|
|
530
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
531
|
+
model: 'claude-3-opus-20240229',
|
|
532
|
+
maxTokens: 100
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const haikuCost = provider.estimateCost({
|
|
536
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
537
|
+
model: 'claude-3-haiku-20240307',
|
|
538
|
+
maxTokens: 100
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Opus should be more expensive
|
|
542
|
+
expect(opusCost.estimatedCostUSD).toBeGreaterThan(haikuCost.estimatedCostUSD);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
describe('isAvailable', () => {
|
|
547
|
+
it('should return true when healthy', async () => {
|
|
548
|
+
const mockResponse = {
|
|
549
|
+
id: 'msg_123',
|
|
550
|
+
type: 'message',
|
|
551
|
+
role: 'assistant',
|
|
552
|
+
model: 'claude-3-haiku-20240307',
|
|
553
|
+
content: [{ type: 'text', text: 'Hi' }],
|
|
554
|
+
stop_reason: 'end_turn',
|
|
555
|
+
stop_sequence: null,
|
|
556
|
+
usage: { input_tokens: 5, output_tokens: 5 }
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
560
|
+
|
|
561
|
+
const available = await provider.isAvailable();
|
|
562
|
+
expect(available).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should return false when unavailable', async () => {
|
|
566
|
+
mockCreate.mockRejectedValue(new Error('Network error'));
|
|
567
|
+
|
|
568
|
+
const available = await provider.isAvailable();
|
|
569
|
+
expect(available).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe('edge cases', () => {
|
|
574
|
+
it('should handle undefined stop reason', async () => {
|
|
575
|
+
const mockResponse = {
|
|
576
|
+
id: 'msg_123',
|
|
577
|
+
type: 'message',
|
|
578
|
+
role: 'assistant',
|
|
579
|
+
model: 'claude-3-opus-20240229',
|
|
580
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
581
|
+
stop_reason: null,
|
|
582
|
+
stop_sequence: null,
|
|
583
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
587
|
+
|
|
588
|
+
const result = await provider.complete({
|
|
589
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(result.finishReason).toBe('stop');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('should handle stop_sequence stop reason', async () => {
|
|
596
|
+
const mockResponse = {
|
|
597
|
+
id: 'msg_123',
|
|
598
|
+
type: 'message',
|
|
599
|
+
role: 'assistant',
|
|
600
|
+
model: 'claude-3-opus-20240229',
|
|
601
|
+
content: [{ type: 'text', text: 'Response STOP' }],
|
|
602
|
+
stop_reason: 'stop_sequence',
|
|
603
|
+
stop_sequence: 'STOP',
|
|
604
|
+
usage: { input_tokens: 10, output_tokens: 10 }
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
608
|
+
|
|
609
|
+
const result = await provider.complete({
|
|
610
|
+
messages: [{ role: 'user', content: 'Test' }],
|
|
611
|
+
stop: ['STOP']
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(result.finishReason).toBe('stop');
|
|
615
|
+
expect(result.metadata?.stopSequence).toBe('STOP');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should filter out non-text content blocks', async () => {
|
|
619
|
+
const mockResponse = {
|
|
620
|
+
id: 'msg_123',
|
|
621
|
+
type: 'message',
|
|
622
|
+
role: 'assistant',
|
|
623
|
+
model: 'claude-3-opus-20240229',
|
|
624
|
+
content: [
|
|
625
|
+
{ type: 'text', text: 'Text content' },
|
|
626
|
+
{ type: 'other', data: 'should be ignored' }
|
|
627
|
+
],
|
|
628
|
+
stop_reason: 'end_turn',
|
|
629
|
+
stop_sequence: null,
|
|
630
|
+
usage: { input_tokens: 10, output_tokens: 10 }
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
mockCreate.mockResolvedValue(mockResponse as unknown as Anthropic.Message);
|
|
634
|
+
|
|
635
|
+
const result = await provider.complete({
|
|
636
|
+
messages: [{ role: 'user', content: 'Test' }]
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
expect(result.content).toBe('Text content');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('should handle analyzeImage error', async () => {
|
|
643
|
+
const error = new Error('Vision API error');
|
|
644
|
+
mockCreate.mockRejectedValue(error);
|
|
645
|
+
|
|
646
|
+
const result = await provider.analyzeImage(
|
|
647
|
+
'imagedata',
|
|
648
|
+
'Analyze this'
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(result.success).toBe(false);
|
|
652
|
+
expect(result.error).toContain('Vision API error');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
});
|