genai-lite 0.1.1 → 0.1.3
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/README.md +99 -1
- package/dist/config/presets.json +222 -0
- package/dist/index.d.ts +2 -0
- package/dist/llm/LLMService.d.ts +25 -1
- package/dist/llm/LLMService.js +34 -1
- package/dist/llm/LLMService.presets.test.d.ts +1 -0
- package/dist/llm/LLMService.presets.test.js +210 -0
- package/dist/llm/LLMService.test.d.ts +1 -0
- package/dist/llm/LLMService.test.js +279 -0
- package/dist/llm/clients/AnthropicClientAdapter.test.d.ts +1 -0
- package/dist/llm/clients/AnthropicClientAdapter.test.js +263 -0
- package/dist/llm/clients/GeminiClientAdapter.test.d.ts +1 -0
- package/dist/llm/clients/GeminiClientAdapter.test.js +281 -0
- package/dist/llm/clients/MockClientAdapter.test.d.ts +1 -0
- package/dist/llm/clients/MockClientAdapter.test.js +240 -0
- package/dist/llm/clients/OpenAIClientAdapter.test.d.ts +1 -0
- package/dist/llm/clients/OpenAIClientAdapter.test.js +248 -0
- package/dist/llm/clients/adapterErrorUtils.test.d.ts +1 -0
- package/dist/llm/clients/adapterErrorUtils.test.js +123 -0
- package/dist/llm/config.test.d.ts +1 -0
- package/dist/llm/config.test.js +159 -0
- package/dist/providers/fromEnvironment.test.d.ts +1 -0
- package/dist/providers/fromEnvironment.test.js +46 -0
- package/dist/types/presets.d.ts +19 -0
- package/dist/types/presets.js +2 -0
- package/dist/utils/prompt.test.d.ts +1 -0
- package/dist/utils/prompt.test.js +115 -0
- package/package.json +9 -4
- package/src/config/presets.json +222 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const LLMService_1 = require("./LLMService");
|
|
4
|
+
describe('LLMService', () => {
|
|
5
|
+
let service;
|
|
6
|
+
let mockApiKeyProvider;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Reset all mocks
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
// Create mock API key provider
|
|
11
|
+
mockApiKeyProvider = jest.fn(async (providerId) => `mock-key-for-${providerId}`);
|
|
12
|
+
// Create service instance
|
|
13
|
+
service = new LLMService_1.LLMService(mockApiKeyProvider);
|
|
14
|
+
});
|
|
15
|
+
describe('constructor and initialization', () => {
|
|
16
|
+
it('should initialize with the provided API key provider', () => {
|
|
17
|
+
expect(service).toBeDefined();
|
|
18
|
+
// The service should be ready to use
|
|
19
|
+
});
|
|
20
|
+
it('should lazy-load client adapters on first use', async () => {
|
|
21
|
+
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
22
|
+
// First request should create the adapter
|
|
23
|
+
const request = {
|
|
24
|
+
providerId: 'openai',
|
|
25
|
+
modelId: 'gpt-4.1',
|
|
26
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
27
|
+
};
|
|
28
|
+
await service.sendMessage(request);
|
|
29
|
+
// Verify API key provider was called
|
|
30
|
+
expect(mockApiKeyProvider).toHaveBeenCalledWith('openai');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('sendMessage', () => {
|
|
34
|
+
describe('request validation', () => {
|
|
35
|
+
it('should return validation error for unsupported provider', async () => {
|
|
36
|
+
const request = {
|
|
37
|
+
providerId: 'unsupported-provider',
|
|
38
|
+
modelId: 'some-model',
|
|
39
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
40
|
+
};
|
|
41
|
+
const response = await service.sendMessage(request);
|
|
42
|
+
expect(response.object).toBe('error');
|
|
43
|
+
const errorResponse = response;
|
|
44
|
+
expect(errorResponse.error.code).toBe('UNSUPPORTED_PROVIDER');
|
|
45
|
+
expect(errorResponse.error.message).toContain('Unsupported provider');
|
|
46
|
+
});
|
|
47
|
+
it('should return validation error for unsupported model', async () => {
|
|
48
|
+
const request = {
|
|
49
|
+
providerId: 'openai',
|
|
50
|
+
modelId: 'unsupported-model',
|
|
51
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
52
|
+
};
|
|
53
|
+
const response = await service.sendMessage(request);
|
|
54
|
+
expect(response.object).toBe('error');
|
|
55
|
+
const errorResponse = response;
|
|
56
|
+
expect(errorResponse.error.code).toBe('UNSUPPORTED_MODEL');
|
|
57
|
+
expect(errorResponse.error.message).toContain('Unsupported model');
|
|
58
|
+
});
|
|
59
|
+
it('should return validation error for empty messages', async () => {
|
|
60
|
+
const request = {
|
|
61
|
+
providerId: 'openai',
|
|
62
|
+
modelId: 'gpt-4.1',
|
|
63
|
+
messages: []
|
|
64
|
+
};
|
|
65
|
+
const response = await service.sendMessage(request);
|
|
66
|
+
expect(response.object).toBe('error');
|
|
67
|
+
const errorResponse = response;
|
|
68
|
+
expect(errorResponse.error.code).toBe('INVALID_REQUEST');
|
|
69
|
+
expect(errorResponse.error.message).toContain('Request must contain at least one message');
|
|
70
|
+
});
|
|
71
|
+
it('should return validation error for invalid message role', async () => {
|
|
72
|
+
const request = {
|
|
73
|
+
providerId: 'openai',
|
|
74
|
+
modelId: 'gpt-4.1',
|
|
75
|
+
messages: [{ role: 'invalid', content: 'Hello' }]
|
|
76
|
+
};
|
|
77
|
+
const response = await service.sendMessage(request);
|
|
78
|
+
expect(response.object).toBe('error');
|
|
79
|
+
const errorResponse = response;
|
|
80
|
+
expect(errorResponse.error.code).toBe('INVALID_MESSAGE_ROLE');
|
|
81
|
+
expect(errorResponse.error.message).toContain('Invalid message role');
|
|
82
|
+
});
|
|
83
|
+
it('should return validation error for empty message content', async () => {
|
|
84
|
+
const request = {
|
|
85
|
+
providerId: 'openai',
|
|
86
|
+
modelId: 'gpt-4.1',
|
|
87
|
+
messages: [{ role: 'user', content: '' }]
|
|
88
|
+
};
|
|
89
|
+
const response = await service.sendMessage(request);
|
|
90
|
+
expect(response.object).toBe('error');
|
|
91
|
+
const errorResponse = response;
|
|
92
|
+
expect(errorResponse.error.code).toBe('INVALID_MESSAGE');
|
|
93
|
+
expect(errorResponse.error.message).toContain('Message at index 0 must have both');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('API key handling', () => {
|
|
97
|
+
it('should return error when API key provider returns null', async () => {
|
|
98
|
+
mockApiKeyProvider.mockResolvedValueOnce(null);
|
|
99
|
+
const request = {
|
|
100
|
+
providerId: 'openai',
|
|
101
|
+
modelId: 'gpt-4.1',
|
|
102
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
103
|
+
};
|
|
104
|
+
const response = await service.sendMessage(request);
|
|
105
|
+
expect(response.object).toBe('error');
|
|
106
|
+
const errorResponse = response;
|
|
107
|
+
expect(errorResponse.error.code).toBe('API_KEY_ERROR');
|
|
108
|
+
expect(errorResponse.error.message).toContain('API key for provider');
|
|
109
|
+
});
|
|
110
|
+
it('should return error when API key provider throws', async () => {
|
|
111
|
+
mockApiKeyProvider.mockRejectedValueOnce(new Error('Key provider error'));
|
|
112
|
+
const request = {
|
|
113
|
+
providerId: 'openai',
|
|
114
|
+
modelId: 'gpt-4.1',
|
|
115
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
116
|
+
};
|
|
117
|
+
const response = await service.sendMessage(request);
|
|
118
|
+
expect(response.object).toBe('error');
|
|
119
|
+
const errorResponse = response;
|
|
120
|
+
expect(errorResponse.error.code).toBe('PROVIDER_ERROR');
|
|
121
|
+
expect(errorResponse.error.message).toContain('Key provider error');
|
|
122
|
+
});
|
|
123
|
+
it('should return error for invalid API key format', async () => {
|
|
124
|
+
mockApiKeyProvider.mockResolvedValueOnce('invalid-key');
|
|
125
|
+
const request = {
|
|
126
|
+
providerId: 'openai',
|
|
127
|
+
modelId: 'gpt-4.1',
|
|
128
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
129
|
+
};
|
|
130
|
+
const response = await service.sendMessage(request);
|
|
131
|
+
// OpenAI adapter expects keys starting with 'sk-'
|
|
132
|
+
expect(response.object).toBe('error');
|
|
133
|
+
const errorResponse = response;
|
|
134
|
+
expect(errorResponse.error.code).toBe('INVALID_API_KEY');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('adapter routing', () => {
|
|
138
|
+
it('should route request to correct adapter based on provider', async () => {
|
|
139
|
+
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
140
|
+
const request = {
|
|
141
|
+
providerId: 'openai',
|
|
142
|
+
modelId: 'gpt-4.1',
|
|
143
|
+
messages: [{ role: 'user', content: 'Test routing' }]
|
|
144
|
+
};
|
|
145
|
+
const response = await service.sendMessage(request);
|
|
146
|
+
// This will fail with a network error since we're not mocking the actual API
|
|
147
|
+
expect(response.object).toBe('error');
|
|
148
|
+
const errorResponse = response;
|
|
149
|
+
// We should get a network error or similar since we're not mocking the HTTP request
|
|
150
|
+
expect(errorResponse.provider).toBe('openai');
|
|
151
|
+
});
|
|
152
|
+
it('should reuse existing adapter for same provider', async () => {
|
|
153
|
+
const request = {
|
|
154
|
+
providerId: 'mock',
|
|
155
|
+
modelId: 'mock-model',
|
|
156
|
+
messages: [{ role: 'user', content: 'First request' }]
|
|
157
|
+
};
|
|
158
|
+
// First request
|
|
159
|
+
await service.sendMessage(request);
|
|
160
|
+
// Second request to same provider
|
|
161
|
+
request.messages = [{ role: 'user', content: 'Second request' }];
|
|
162
|
+
await service.sendMessage(request);
|
|
163
|
+
// API key provider should be called for each request with mock provider
|
|
164
|
+
expect(mockApiKeyProvider).toHaveBeenCalledTimes(0); // Mock provider doesn't need API keys
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('settings management', () => {
|
|
168
|
+
it('should apply default settings when none provided', async () => {
|
|
169
|
+
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
170
|
+
const request = {
|
|
171
|
+
providerId: 'openai',
|
|
172
|
+
modelId: 'gpt-4.1',
|
|
173
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
174
|
+
};
|
|
175
|
+
const response = await service.sendMessage(request);
|
|
176
|
+
// We'll get a network error but can still verify the request was attempted
|
|
177
|
+
expect(response.object).toBe('error');
|
|
178
|
+
expect(mockApiKeyProvider).toHaveBeenCalledWith('openai');
|
|
179
|
+
});
|
|
180
|
+
it('should merge user settings with defaults', async () => {
|
|
181
|
+
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
182
|
+
const request = {
|
|
183
|
+
providerId: 'openai',
|
|
184
|
+
modelId: 'gpt-4.1',
|
|
185
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
186
|
+
settings: {
|
|
187
|
+
temperature: 0.9,
|
|
188
|
+
maxTokens: 500
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const response = await service.sendMessage(request);
|
|
192
|
+
// We'll get a network error but the settings should still be validated
|
|
193
|
+
expect(response.object).toBe('error');
|
|
194
|
+
});
|
|
195
|
+
it('should validate temperature setting', async () => {
|
|
196
|
+
const request = {
|
|
197
|
+
providerId: 'openai',
|
|
198
|
+
modelId: 'gpt-4.1',
|
|
199
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
200
|
+
settings: {
|
|
201
|
+
temperature: 2.5 // Out of range
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const response = await service.sendMessage(request);
|
|
205
|
+
expect(response.object).toBe('error');
|
|
206
|
+
const errorResponse = response;
|
|
207
|
+
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
208
|
+
expect(errorResponse.error.message).toContain('temperature must be a number between');
|
|
209
|
+
});
|
|
210
|
+
it('should validate maxTokens setting', async () => {
|
|
211
|
+
const request = {
|
|
212
|
+
providerId: 'openai',
|
|
213
|
+
modelId: 'gpt-4.1',
|
|
214
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
215
|
+
settings: {
|
|
216
|
+
maxTokens: 0 // Invalid
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const response = await service.sendMessage(request);
|
|
220
|
+
expect(response.object).toBe('error');
|
|
221
|
+
const errorResponse = response;
|
|
222
|
+
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
223
|
+
expect(errorResponse.error.message).toContain('maxTokens must be an integer between');
|
|
224
|
+
});
|
|
225
|
+
it('should validate topP setting', async () => {
|
|
226
|
+
const request = {
|
|
227
|
+
providerId: 'openai',
|
|
228
|
+
modelId: 'gpt-4.1',
|
|
229
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
230
|
+
settings: {
|
|
231
|
+
topP: -0.1 // Out of range
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const response = await service.sendMessage(request);
|
|
235
|
+
expect(response.object).toBe('error');
|
|
236
|
+
const errorResponse = response;
|
|
237
|
+
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
238
|
+
expect(errorResponse.error.message).toContain('topP must be a number between 0 and 1');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('getProviders', () => {
|
|
243
|
+
it('should return all supported providers', async () => {
|
|
244
|
+
const providers = await service.getProviders();
|
|
245
|
+
expect(providers).toHaveLength(4);
|
|
246
|
+
expect(providers.find(p => p.id === 'openai')).toBeDefined();
|
|
247
|
+
expect(providers.find(p => p.id === 'anthropic')).toBeDefined();
|
|
248
|
+
expect(providers.find(p => p.id === 'gemini')).toBeDefined();
|
|
249
|
+
expect(providers.find(p => p.id === 'mistral')).toBeDefined();
|
|
250
|
+
});
|
|
251
|
+
it('should include provider metadata', async () => {
|
|
252
|
+
const providers = await service.getProviders();
|
|
253
|
+
const openai = providers.find(p => p.id === 'openai');
|
|
254
|
+
expect(openai).toMatchObject({
|
|
255
|
+
id: 'openai',
|
|
256
|
+
name: 'OpenAI'
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('getModels', () => {
|
|
261
|
+
it('should return all models for a provider', async () => {
|
|
262
|
+
const models = await service.getModels('openai');
|
|
263
|
+
expect(models.length).toBeGreaterThan(0);
|
|
264
|
+
expect(models.some(m => m.id.includes('gpt-4'))).toBe(true);
|
|
265
|
+
expect(models.some(m => m.id.includes('o4-mini'))).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
it('should return empty array for invalid provider', async () => {
|
|
268
|
+
const models = await service.getModels('invalid-provider');
|
|
269
|
+
expect(models).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
it('should include model metadata', async () => {
|
|
272
|
+
const models = await service.getModels('openai');
|
|
273
|
+
const gpt4 = models.find(m => m.id === 'gpt-4.1');
|
|
274
|
+
expect(gpt4).toBeDefined();
|
|
275
|
+
expect(gpt4.contextWindow).toBeGreaterThan(0);
|
|
276
|
+
expect(gpt4.maxTokens).toBeGreaterThan(0);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
7
|
+
const AnthropicClientAdapter_1 = require("./AnthropicClientAdapter");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
// Mock the entire '@anthropic-ai/sdk' module
|
|
10
|
+
jest.mock('@anthropic-ai/sdk');
|
|
11
|
+
// Cast the mocked module to allow setting up mock implementations
|
|
12
|
+
const MockAnthropic = sdk_1.default;
|
|
13
|
+
describe('AnthropicClientAdapter', () => {
|
|
14
|
+
let adapter;
|
|
15
|
+
let mockCreate;
|
|
16
|
+
let basicRequest;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Reset mocks before each test
|
|
19
|
+
MockAnthropic.mockClear();
|
|
20
|
+
mockCreate = jest.fn();
|
|
21
|
+
// Mock the messages.create method
|
|
22
|
+
MockAnthropic.prototype.messages = {
|
|
23
|
+
create: mockCreate,
|
|
24
|
+
};
|
|
25
|
+
adapter = new AnthropicClientAdapter_1.AnthropicClientAdapter();
|
|
26
|
+
basicRequest = {
|
|
27
|
+
providerId: 'anthropic',
|
|
28
|
+
modelId: 'claude-3-5-sonnet-20241022',
|
|
29
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
30
|
+
settings: {
|
|
31
|
+
temperature: 0.7,
|
|
32
|
+
maxTokens: 100,
|
|
33
|
+
topP: 1,
|
|
34
|
+
frequencyPenalty: 0,
|
|
35
|
+
presencePenalty: 0,
|
|
36
|
+
stopSequences: [],
|
|
37
|
+
user: 'test-user',
|
|
38
|
+
geminiSafetySettings: [],
|
|
39
|
+
supportsSystemMessage: true
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
describe('sendMessage', () => {
|
|
44
|
+
it('should format the request correctly and call the Anthropic API', async () => {
|
|
45
|
+
// Setup mock response
|
|
46
|
+
mockCreate.mockResolvedValueOnce({
|
|
47
|
+
id: 'msg_123',
|
|
48
|
+
type: 'message',
|
|
49
|
+
role: 'assistant',
|
|
50
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
51
|
+
content: [{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: 'Hello! How can I help you today?'
|
|
54
|
+
}],
|
|
55
|
+
stop_reason: 'end_turn',
|
|
56
|
+
stop_sequence: null,
|
|
57
|
+
usage: {
|
|
58
|
+
input_tokens: 10,
|
|
59
|
+
output_tokens: 20
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const response = await adapter.sendMessage(basicRequest, 'test-api-key');
|
|
63
|
+
// Verify Anthropic was instantiated with the API key
|
|
64
|
+
expect(MockAnthropic).toHaveBeenCalledWith({
|
|
65
|
+
apiKey: 'test-api-key',
|
|
66
|
+
baseURL: undefined
|
|
67
|
+
});
|
|
68
|
+
// Verify the create method was called with correct parameters
|
|
69
|
+
expect(mockCreate).toHaveBeenCalledWith({
|
|
70
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
71
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
72
|
+
max_tokens: 100,
|
|
73
|
+
temperature: 0.7,
|
|
74
|
+
top_p: 1
|
|
75
|
+
});
|
|
76
|
+
// Verify the response
|
|
77
|
+
expect(response.object).toBe('chat.completion');
|
|
78
|
+
const successResponse = response;
|
|
79
|
+
expect(successResponse.id).toBe('msg_123');
|
|
80
|
+
expect(successResponse.provider).toBe('anthropic');
|
|
81
|
+
expect(successResponse.model).toBe('claude-3-5-sonnet-20241022');
|
|
82
|
+
expect(successResponse.choices[0].message.content).toBe('Hello! How can I help you today?');
|
|
83
|
+
expect(successResponse.usage?.total_tokens).toBe(30);
|
|
84
|
+
});
|
|
85
|
+
it('should handle system messages by merging into first user message', async () => {
|
|
86
|
+
basicRequest.messages = [
|
|
87
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
88
|
+
{ role: 'user', content: 'Hello' }
|
|
89
|
+
];
|
|
90
|
+
mockCreate.mockResolvedValueOnce({
|
|
91
|
+
id: 'msg_123',
|
|
92
|
+
type: 'message',
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
95
|
+
content: [{ type: 'text', text: 'Hello!' }],
|
|
96
|
+
stop_reason: 'end_turn',
|
|
97
|
+
usage: { input_tokens: 15, output_tokens: 5 }
|
|
98
|
+
});
|
|
99
|
+
await adapter.sendMessage(basicRequest, 'test-api-key');
|
|
100
|
+
// System message should be sent as separate system parameter
|
|
101
|
+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
|
+
system: 'You are a helpful assistant.',
|
|
103
|
+
messages: [{
|
|
104
|
+
role: 'user',
|
|
105
|
+
content: 'Hello'
|
|
106
|
+
}]
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
it('should handle stop sequences correctly', async () => {
|
|
110
|
+
basicRequest.settings.stopSequences = ['END', 'STOP'];
|
|
111
|
+
mockCreate.mockResolvedValueOnce({
|
|
112
|
+
id: 'msg_123',
|
|
113
|
+
type: 'message',
|
|
114
|
+
role: 'assistant',
|
|
115
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
116
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
117
|
+
stop_reason: 'end_turn',
|
|
118
|
+
usage: { input_tokens: 10, output_tokens: 10 }
|
|
119
|
+
});
|
|
120
|
+
await adapter.sendMessage(basicRequest, 'test-api-key');
|
|
121
|
+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
122
|
+
stop_sequences: ['END', 'STOP']
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
it('should handle multi-turn conversations', async () => {
|
|
126
|
+
basicRequest.messages = [
|
|
127
|
+
{ role: 'user', content: 'Hello' },
|
|
128
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
129
|
+
{ role: 'user', content: 'How are you?' }
|
|
130
|
+
];
|
|
131
|
+
mockCreate.mockResolvedValueOnce({
|
|
132
|
+
id: 'msg_123',
|
|
133
|
+
type: 'message',
|
|
134
|
+
role: 'assistant',
|
|
135
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
136
|
+
content: [{ type: 'text', text: "I'm doing well, thanks!" }],
|
|
137
|
+
stop_reason: 'end_turn',
|
|
138
|
+
usage: { input_tokens: 20, output_tokens: 10 }
|
|
139
|
+
});
|
|
140
|
+
await adapter.sendMessage(basicRequest, 'test-api-key');
|
|
141
|
+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
142
|
+
messages: [
|
|
143
|
+
{ role: 'user', content: 'Hello' },
|
|
144
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
145
|
+
{ role: 'user', content: 'How are you?' }
|
|
146
|
+
]
|
|
147
|
+
}));
|
|
148
|
+
});
|
|
149
|
+
it('should map stop_reason correctly', async () => {
|
|
150
|
+
const stopReasons = [
|
|
151
|
+
{ anthropic: 'end_turn', expected: 'stop' },
|
|
152
|
+
{ anthropic: 'max_tokens', expected: 'length' },
|
|
153
|
+
{ anthropic: 'stop_sequence', expected: 'stop' },
|
|
154
|
+
{ anthropic: 'unknown_reason', expected: 'other' }
|
|
155
|
+
];
|
|
156
|
+
for (const { anthropic, expected } of stopReasons) {
|
|
157
|
+
mockCreate.mockResolvedValueOnce({
|
|
158
|
+
id: 'msg_123',
|
|
159
|
+
type: 'message',
|
|
160
|
+
role: 'assistant',
|
|
161
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
162
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
163
|
+
stop_reason: anthropic,
|
|
164
|
+
usage: { input_tokens: 10, output_tokens: 10 }
|
|
165
|
+
});
|
|
166
|
+
const response = await adapter.sendMessage(basicRequest, 'test-api-key');
|
|
167
|
+
const successResponse = response;
|
|
168
|
+
expect(successResponse.choices[0].finish_reason).toBe(expected);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
describe('error handling', () => {
|
|
172
|
+
it('should handle authentication errors (401)', async () => {
|
|
173
|
+
const apiError = new Error('Invalid API key');
|
|
174
|
+
apiError.status = 401;
|
|
175
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
176
|
+
const response = await adapter.sendMessage(basicRequest, 'invalid-key');
|
|
177
|
+
expect(response.object).toBe('error');
|
|
178
|
+
const errorResponse = response;
|
|
179
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.INVALID_API_KEY);
|
|
180
|
+
expect(errorResponse.error.type).toBe('authentication_error');
|
|
181
|
+
});
|
|
182
|
+
it('should handle rate limit errors (429)', async () => {
|
|
183
|
+
const apiError = new Error('Rate limit exceeded');
|
|
184
|
+
apiError.status = 429;
|
|
185
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
186
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
187
|
+
const errorResponse = response;
|
|
188
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED);
|
|
189
|
+
expect(errorResponse.error.type).toBe('rate_limit_error');
|
|
190
|
+
});
|
|
191
|
+
it('should handle context length errors', async () => {
|
|
192
|
+
// Create a mock error that simulates Anthropic.APIError
|
|
193
|
+
const apiError = Object.assign(new Error('Message is too long'), {
|
|
194
|
+
status: 400,
|
|
195
|
+
constructor: { name: 'APIError' }
|
|
196
|
+
});
|
|
197
|
+
Object.setPrototypeOf(apiError, sdk_1.default.APIError.prototype);
|
|
198
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
199
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
200
|
+
const errorResponse = response;
|
|
201
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.CONTEXT_LENGTH_EXCEEDED);
|
|
202
|
+
expect(errorResponse.error.type).toBe('invalid_request_error');
|
|
203
|
+
});
|
|
204
|
+
it('should handle invalid model errors', async () => {
|
|
205
|
+
const apiError = new Error('Model not found');
|
|
206
|
+
apiError.status = 404;
|
|
207
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
208
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
209
|
+
const errorResponse = response;
|
|
210
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.MODEL_NOT_FOUND);
|
|
211
|
+
expect(errorResponse.error.type).toBe('invalid_request_error');
|
|
212
|
+
});
|
|
213
|
+
it('should handle credit errors', async () => {
|
|
214
|
+
const apiError = new Error('Insufficient credits');
|
|
215
|
+
apiError.status = 402;
|
|
216
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
217
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
218
|
+
const errorResponse = response;
|
|
219
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.INSUFFICIENT_CREDITS);
|
|
220
|
+
expect(errorResponse.error.type).toBe('rate_limit_error');
|
|
221
|
+
});
|
|
222
|
+
it('should handle server errors (500)', async () => {
|
|
223
|
+
const apiError = new Error('Internal server error');
|
|
224
|
+
apiError.status = 500;
|
|
225
|
+
mockCreate.mockRejectedValueOnce(apiError);
|
|
226
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
227
|
+
const errorResponse = response;
|
|
228
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR);
|
|
229
|
+
expect(errorResponse.error.type).toBe('server_error');
|
|
230
|
+
});
|
|
231
|
+
it('should handle network errors', async () => {
|
|
232
|
+
const networkError = new Error('Network error');
|
|
233
|
+
networkError.code = 'ENOTFOUND';
|
|
234
|
+
mockCreate.mockRejectedValueOnce(networkError);
|
|
235
|
+
const response = await adapter.sendMessage(basicRequest, 'test-key');
|
|
236
|
+
const errorResponse = response;
|
|
237
|
+
expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.NETWORK_ERROR);
|
|
238
|
+
expect(errorResponse.error.type).toBe('connection_error');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('validateApiKey', () => {
|
|
243
|
+
it('should validate API key format', () => {
|
|
244
|
+
// Valid Anthropic API key format - must start with 'sk-ant-' and be at least 30 chars
|
|
245
|
+
expect(adapter.validateApiKey('sk-ant-api01-test123456789012345')).toBe(true);
|
|
246
|
+
expect(adapter.validateApiKey('sk-ant-api03-test123456789012345')).toBe(true);
|
|
247
|
+
// Invalid formats
|
|
248
|
+
expect(adapter.validateApiKey('invalid')).toBe(false);
|
|
249
|
+
expect(adapter.validateApiKey('')).toBe(false);
|
|
250
|
+
expect(adapter.validateApiKey('sk-test')).toBe(false); // OpenAI format
|
|
251
|
+
expect(adapter.validateApiKey('sk-ant-test123')).toBe(false); // Too short
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('getAdapterInfo', () => {
|
|
255
|
+
it('should return correct adapter information', () => {
|
|
256
|
+
const info = adapter.getAdapterInfo();
|
|
257
|
+
expect(info.providerId).toBe('anthropic');
|
|
258
|
+
expect(info.name).toBe('Anthropic Client Adapter');
|
|
259
|
+
expect(info.version).toBeDefined();
|
|
260
|
+
// supportedModels is not part of the interface
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|