genai-lite 0.1.1 → 0.1.4

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.
Files changed (36) hide show
  1. package/README.md +202 -1
  2. package/dist/config/presets.json +222 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +4 -1
  5. package/dist/llm/LLMService.d.ts +25 -1
  6. package/dist/llm/LLMService.js +34 -1
  7. package/dist/llm/LLMService.presets.test.d.ts +1 -0
  8. package/dist/llm/LLMService.presets.test.js +210 -0
  9. package/dist/llm/LLMService.test.d.ts +1 -0
  10. package/dist/llm/LLMService.test.js +279 -0
  11. package/dist/llm/clients/AnthropicClientAdapter.test.d.ts +1 -0
  12. package/dist/llm/clients/AnthropicClientAdapter.test.js +263 -0
  13. package/dist/llm/clients/GeminiClientAdapter.test.d.ts +1 -0
  14. package/dist/llm/clients/GeminiClientAdapter.test.js +281 -0
  15. package/dist/llm/clients/MockClientAdapter.test.d.ts +1 -0
  16. package/dist/llm/clients/MockClientAdapter.test.js +240 -0
  17. package/dist/llm/clients/OpenAIClientAdapter.test.d.ts +1 -0
  18. package/dist/llm/clients/OpenAIClientAdapter.test.js +248 -0
  19. package/dist/llm/clients/adapterErrorUtils.test.d.ts +1 -0
  20. package/dist/llm/clients/adapterErrorUtils.test.js +123 -0
  21. package/dist/llm/config.test.d.ts +1 -0
  22. package/dist/llm/config.test.js +159 -0
  23. package/dist/providers/fromEnvironment.test.d.ts +1 -0
  24. package/dist/providers/fromEnvironment.test.js +46 -0
  25. package/dist/types/presets.d.ts +19 -0
  26. package/dist/types/presets.js +2 -0
  27. package/dist/utils/index.d.ts +1 -0
  28. package/dist/utils/index.js +1 -0
  29. package/dist/utils/prompt.test.d.ts +1 -0
  30. package/dist/utils/prompt.test.js +115 -0
  31. package/dist/utils/templateEngine.d.ts +15 -0
  32. package/dist/utils/templateEngine.js +194 -0
  33. package/dist/utils/templateEngine.test.d.ts +1 -0
  34. package/dist/utils/templateEngine.test.js +134 -0
  35. package/package.json +9 -4
  36. package/src/config/presets.json +222 -0
@@ -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 {};
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const genai_1 = require("@google/genai");
4
+ const GeminiClientAdapter_1 = require("./GeminiClientAdapter");
5
+ const types_1 = require("./types");
6
+ // Mock the entire '@google/genai' module
7
+ jest.mock('@google/genai');
8
+ // Cast the mocked module to allow setting up mock implementations
9
+ const MockGoogleGenAI = genai_1.GoogleGenAI;
10
+ describe('GeminiClientAdapter', () => {
11
+ let adapter;
12
+ let mockGenerateContent;
13
+ let mockGetGenerativeModel;
14
+ let mockModel;
15
+ let basicRequest;
16
+ beforeEach(() => {
17
+ // Reset mocks before each test
18
+ MockGoogleGenAI.mockClear();
19
+ mockGenerateContent = jest.fn();
20
+ // Mock the models.generateContent method
21
+ MockGoogleGenAI.mockImplementation(() => ({
22
+ models: {
23
+ generateContent: mockGenerateContent
24
+ }
25
+ }));
26
+ adapter = new GeminiClientAdapter_1.GeminiClientAdapter();
27
+ basicRequest = {
28
+ providerId: 'gemini',
29
+ modelId: 'gemini-2.5-pro',
30
+ messages: [{ role: 'user', content: 'Hello' }],
31
+ settings: {
32
+ temperature: 0.7,
33
+ maxTokens: 100,
34
+ topP: 1,
35
+ frequencyPenalty: 0,
36
+ presencePenalty: 0,
37
+ stopSequences: [],
38
+ user: 'test-user',
39
+ geminiSafetySettings: [],
40
+ supportsSystemMessage: true
41
+ }
42
+ };
43
+ });
44
+ describe('sendMessage', () => {
45
+ it('should format the request correctly and call the Gemini API', async () => {
46
+ // Setup mock response - Gemini API returns the raw response without nesting
47
+ mockGenerateContent.mockResolvedValueOnce({
48
+ text: () => 'Hello! How can I help you today?',
49
+ candidates: [{
50
+ finishReason: 'STOP',
51
+ content: {
52
+ parts: [{ text: 'Hello! How can I help you today?' }],
53
+ role: 'model'
54
+ }
55
+ }],
56
+ usageMetadata: {
57
+ promptTokenCount: 10,
58
+ candidatesTokenCount: 20,
59
+ totalTokenCount: 30
60
+ }
61
+ });
62
+ const response = await adapter.sendMessage(basicRequest, 'test-api-key');
63
+ // Verify GoogleGenAI was instantiated with the API key
64
+ expect(MockGoogleGenAI).toHaveBeenCalledWith({ apiKey: 'test-api-key' });
65
+ // Verify generateContent was called
66
+ expect(mockGenerateContent).toHaveBeenCalledTimes(1);
67
+ const callArgs = mockGenerateContent.mock.calls[0][0];
68
+ expect(callArgs.model).toBe('gemini-2.5-pro');
69
+ expect(callArgs.contents).toHaveLength(1);
70
+ expect(callArgs.contents[0].role).toBe('user');
71
+ // Verify the response
72
+ expect(response.object).toBe('chat.completion');
73
+ const successResponse = response;
74
+ expect(successResponse.provider).toBe('gemini');
75
+ expect(successResponse.model).toBe('gemini-2.5-pro');
76
+ expect(successResponse.choices[0].message.content).toBe('Hello! How can I help you today?');
77
+ expect(successResponse.usage?.total_tokens).toBe(30);
78
+ });
79
+ it('should handle system messages correctly', async () => {
80
+ basicRequest.messages = [
81
+ { role: 'system', content: 'You are a helpful assistant.' },
82
+ { role: 'user', content: 'Hello' }
83
+ ];
84
+ mockGenerateContent.mockResolvedValueOnce({
85
+ text: () => 'Hello!',
86
+ candidates: [{
87
+ finishReason: 'STOP',
88
+ content: { parts: [{ text: 'Hello!' }], role: 'model' }
89
+ }],
90
+ usageMetadata: { promptTokenCount: 15, candidatesTokenCount: 5, totalTokenCount: 20 }
91
+ });
92
+ await adapter.sendMessage(basicRequest, 'test-api-key');
93
+ // System message should be passed as systemInstruction
94
+ expect(mockGenerateContent).toHaveBeenCalledWith({
95
+ model: 'gemini-2.5-pro',
96
+ contents: [{
97
+ role: 'user',
98
+ parts: [{ text: 'Hello' }]
99
+ }],
100
+ config: {
101
+ temperature: 0.7,
102
+ maxOutputTokens: 100,
103
+ topP: 1,
104
+ safetySettings: [],
105
+ systemInstruction: 'You are a helpful assistant.'
106
+ }
107
+ });
108
+ });
109
+ it('should handle multi-turn conversations with role mapping', async () => {
110
+ basicRequest.messages = [
111
+ { role: 'user', content: 'Hello' },
112
+ { role: 'assistant', content: 'Hi there!' },
113
+ { role: 'user', content: 'How are you?' }
114
+ ];
115
+ mockGenerateContent.mockResolvedValueOnce({
116
+ text: () => "I'm doing well!",
117
+ candidates: [{
118
+ finishReason: 'STOP',
119
+ content: { parts: [{ text: "I'm doing well!" }], role: 'model' }
120
+ }],
121
+ usageMetadata: { promptTokenCount: 20, candidatesTokenCount: 10, totalTokenCount: 30 }
122
+ });
123
+ await adapter.sendMessage(basicRequest, 'test-api-key');
124
+ // Verify role mapping: assistant -> model
125
+ expect(mockGenerateContent).toHaveBeenCalledWith({
126
+ model: 'gemini-2.5-pro',
127
+ contents: [
128
+ { role: 'user', parts: [{ text: 'Hello' }] },
129
+ { role: 'model', parts: [{ text: 'Hi there!' }] },
130
+ { role: 'user', parts: [{ text: 'How are you?' }] }
131
+ ],
132
+ config: {
133
+ temperature: 0.7,
134
+ maxOutputTokens: 100,
135
+ topP: 1,
136
+ safetySettings: []
137
+ }
138
+ });
139
+ });
140
+ it('should handle stop sequences', async () => {
141
+ basicRequest.settings.stopSequences = ['END', 'STOP'];
142
+ mockGenerateContent.mockResolvedValueOnce({
143
+ text: () => 'Response',
144
+ candidates: [{
145
+ finishReason: 'STOP',
146
+ content: { parts: [{ text: 'Response' }], role: 'model' }
147
+ }]
148
+ });
149
+ await adapter.sendMessage(basicRequest, 'test-api-key');
150
+ expect(mockGenerateContent).toHaveBeenCalledWith(expect.objectContaining({
151
+ config: expect.objectContaining({
152
+ stopSequences: ['END', 'STOP']
153
+ })
154
+ }));
155
+ });
156
+ it('should handle safety settings', async () => {
157
+ basicRequest.settings.geminiSafetySettings = [
158
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' }
159
+ ];
160
+ mockGenerateContent.mockResolvedValueOnce({
161
+ text: () => 'Response',
162
+ candidates: [{
163
+ finishReason: 'STOP',
164
+ content: { parts: [{ text: 'Response' }], role: 'model' }
165
+ }]
166
+ });
167
+ await adapter.sendMessage(basicRequest, 'test-api-key');
168
+ expect(mockGenerateContent).toHaveBeenCalledWith(expect.objectContaining({
169
+ config: expect.objectContaining({
170
+ safetySettings: [
171
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' }
172
+ ]
173
+ })
174
+ }));
175
+ });
176
+ it('should map finish reasons correctly', async () => {
177
+ const finishReasons = [
178
+ { gemini: 'STOP', expected: 'stop' },
179
+ { gemini: 'MAX_TOKENS', expected: 'length' },
180
+ { gemini: 'SAFETY', expected: 'content_filter' },
181
+ { gemini: 'RECITATION', expected: 'content_filter' },
182
+ { gemini: 'OTHER', expected: 'other' },
183
+ { gemini: 'UNKNOWN', expected: 'other' }
184
+ ];
185
+ for (const { gemini, expected } of finishReasons) {
186
+ mockGenerateContent.mockResolvedValueOnce({
187
+ text: () => 'Response',
188
+ candidates: [{
189
+ finishReason: gemini,
190
+ content: { parts: [{ text: 'Response' }], role: 'model' }
191
+ }]
192
+ });
193
+ const response = await adapter.sendMessage(basicRequest, 'test-api-key');
194
+ const successResponse = response;
195
+ expect(successResponse.choices[0].finish_reason).toBe(expected);
196
+ }
197
+ });
198
+ describe('error handling', () => {
199
+ it('should handle API key errors', async () => {
200
+ const apiError = new Error('API key not valid');
201
+ mockGenerateContent.mockRejectedValueOnce(apiError);
202
+ const response = await adapter.sendMessage(basicRequest, 'invalid-key');
203
+ const errorResponse = response;
204
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.INVALID_API_KEY);
205
+ expect(errorResponse.error.type).toBe('authentication_error');
206
+ });
207
+ it('should handle safety/content filter errors', async () => {
208
+ const apiError = new Error('Response was blocked due to safety reasons');
209
+ mockGenerateContent.mockRejectedValueOnce(apiError);
210
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
211
+ const errorResponse = response;
212
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.CONTENT_FILTER);
213
+ expect(errorResponse.error.type).toBe('content_filter_error');
214
+ });
215
+ it('should handle quota exceeded errors', async () => {
216
+ const apiError = new Error('API rate limit exceeded');
217
+ mockGenerateContent.mockRejectedValueOnce(apiError);
218
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
219
+ const errorResponse = response;
220
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.RATE_LIMIT_EXCEEDED);
221
+ expect(errorResponse.error.type).toBe('rate_limit_error');
222
+ });
223
+ it('should handle model not found errors', async () => {
224
+ const apiError = new Error('Model not found');
225
+ apiError.status = 404;
226
+ mockGenerateContent.mockRejectedValueOnce(apiError);
227
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
228
+ const errorResponse = response;
229
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.MODEL_NOT_FOUND);
230
+ expect(errorResponse.error.type).toBe('invalid_request_error');
231
+ });
232
+ it('should handle permission errors', async () => {
233
+ const apiError = new Error('Invalid API key provided');
234
+ mockGenerateContent.mockRejectedValueOnce(apiError);
235
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
236
+ const errorResponse = response;
237
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.INVALID_API_KEY);
238
+ expect(errorResponse.error.type).toBe('authentication_error');
239
+ });
240
+ it('should handle generic errors', async () => {
241
+ const apiError = new Error('Unknown error');
242
+ mockGenerateContent.mockRejectedValueOnce(apiError);
243
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
244
+ const errorResponse = response;
245
+ expect(errorResponse.error.code).toBe(types_1.ADAPTER_ERROR_CODES.UNKNOWN_ERROR);
246
+ expect(errorResponse.error.message).toContain('Unknown error');
247
+ });
248
+ it('should handle empty response as success with empty content', async () => {
249
+ mockGenerateContent.mockResolvedValueOnce({
250
+ text: () => '',
251
+ candidates: []
252
+ });
253
+ const response = await adapter.sendMessage(basicRequest, 'test-key');
254
+ // Empty responses are returned as success with empty content
255
+ const successResponse = response;
256
+ expect(successResponse.object).toBe('chat.completion');
257
+ expect(successResponse.choices[0].message.content).toBe('');
258
+ });
259
+ });
260
+ });
261
+ describe('validateApiKey', () => {
262
+ it('should validate API key format', () => {
263
+ // Gemini API keys must start with 'AIza' and be at least 35 chars
264
+ expect(adapter.validateApiKey('AIzaSyABCDEFGHIJKLMNOPQRSTUVWXYZ123456')).toBe(true);
265
+ expect(adapter.validateApiKey('AIzaABCDEFGHIJKLMNOPQRSTUVWXYZ12345')).toBe(true);
266
+ // Invalid formats
267
+ expect(adapter.validateApiKey('')).toBe(false);
268
+ expect(adapter.validateApiKey('short')).toBe(false); // Too short
269
+ expect(adapter.validateApiKey('abcdef123456')).toBe(false); // Wrong prefix
270
+ });
271
+ });
272
+ describe('getAdapterInfo', () => {
273
+ it('should return correct adapter information', () => {
274
+ const info = adapter.getAdapterInfo();
275
+ expect(info.providerId).toBe('gemini');
276
+ expect(info.name).toBe('Gemini Client Adapter');
277
+ expect(info.version).toBeDefined();
278
+ // supportedModels is not part of the interface
279
+ });
280
+ });
281
+ });
@@ -0,0 +1 @@
1
+ export {};