genai-lite 0.4.0 → 0.4.2
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 +47 -37
- package/dist/llm/LLMService.d.ts +29 -2
- package/dist/llm/LLMService.js +80 -36
- package/dist/llm/config.js +4 -4
- package/dist/llm/services/SettingsManager.js +17 -11
- package/dist/llm/types.d.ts +81 -22
- package/dist/prompting/parser.d.ts +2 -2
- package/dist/prompting/parser.js +2 -2
- package/package.json +1 -1
- package/dist/llm/LLMService.createMessages.test.d.ts +0 -4
- package/dist/llm/LLMService.createMessages.test.js +0 -364
- package/dist/llm/LLMService.original.d.ts +0 -147
- package/dist/llm/LLMService.original.js +0 -656
- package/dist/llm/LLMService.prepareMessage.test.d.ts +0 -1
- package/dist/llm/LLMService.prepareMessage.test.js +0 -303
- package/dist/llm/LLMService.presets.test.d.ts +0 -1
- package/dist/llm/LLMService.presets.test.js +0 -210
- package/dist/llm/LLMService.sendMessage.preset.test.d.ts +0 -1
- package/dist/llm/LLMService.sendMessage.preset.test.js +0 -153
- package/dist/llm/LLMService.test.d.ts +0 -1
- package/dist/llm/LLMService.test.js +0 -639
- package/dist/llm/clients/AnthropicClientAdapter.test.d.ts +0 -1
- package/dist/llm/clients/AnthropicClientAdapter.test.js +0 -273
- package/dist/llm/clients/GeminiClientAdapter.test.d.ts +0 -1
- package/dist/llm/clients/GeminiClientAdapter.test.js +0 -405
- package/dist/llm/clients/LlamaCppClientAdapter.test.d.ts +0 -1
- package/dist/llm/clients/LlamaCppClientAdapter.test.js +0 -447
- package/dist/llm/clients/LlamaCppServerClient.test.d.ts +0 -1
- package/dist/llm/clients/LlamaCppServerClient.test.js +0 -294
- package/dist/llm/clients/MockClientAdapter.test.d.ts +0 -1
- package/dist/llm/clients/MockClientAdapter.test.js +0 -250
- package/dist/llm/clients/OpenAIClientAdapter.test.d.ts +0 -1
- package/dist/llm/clients/OpenAIClientAdapter.test.js +0 -258
- package/dist/llm/clients/adapterErrorUtils.test.d.ts +0 -1
- package/dist/llm/clients/adapterErrorUtils.test.js +0 -123
- package/dist/llm/config.test.d.ts +0 -1
- package/dist/llm/config.test.js +0 -176
- package/dist/llm/services/AdapterRegistry.test.d.ts +0 -1
- package/dist/llm/services/AdapterRegistry.test.js +0 -239
- package/dist/llm/services/ModelResolver.test.d.ts +0 -1
- package/dist/llm/services/ModelResolver.test.js +0 -179
- package/dist/llm/services/PresetManager.test.d.ts +0 -1
- package/dist/llm/services/PresetManager.test.js +0 -210
- package/dist/llm/services/RequestValidator.test.d.ts +0 -1
- package/dist/llm/services/RequestValidator.test.js +0 -159
- package/dist/llm/services/SettingsManager.test.d.ts +0 -1
- package/dist/llm/services/SettingsManager.test.js +0 -266
- package/dist/prompting/builder.d.ts +0 -38
- package/dist/prompting/builder.js +0 -63
- package/dist/prompting/builder.test.d.ts +0 -4
- package/dist/prompting/builder.test.js +0 -109
- package/dist/prompting/content.test.d.ts +0 -4
- package/dist/prompting/content.test.js +0 -212
- package/dist/prompting/parser.test.d.ts +0 -4
- package/dist/prompting/parser.test.js +0 -464
- package/dist/prompting/template.test.d.ts +0 -1
- package/dist/prompting/template.test.js +0 -250
- package/dist/providers/fromEnvironment.test.d.ts +0 -1
- package/dist/providers/fromEnvironment.test.js +0 -59
|
@@ -1,639 +0,0 @@
|
|
|
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 succeed with fallback for unknown model', async () => {
|
|
48
|
-
const request = {
|
|
49
|
-
providerId: 'mock', // Use mock provider to avoid real API calls
|
|
50
|
-
modelId: 'unsupported-model',
|
|
51
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
52
|
-
};
|
|
53
|
-
const response = await service.sendMessage(request);
|
|
54
|
-
// Should succeed with mock response (not error) even for unknown model
|
|
55
|
-
expect(response.object).toBe('chat.completion');
|
|
56
|
-
});
|
|
57
|
-
it('should silently work with flexible providers unknown models (no warning)', async () => {
|
|
58
|
-
const warnings = [];
|
|
59
|
-
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((msg) => {
|
|
60
|
-
warnings.push(msg);
|
|
61
|
-
});
|
|
62
|
-
// Test with mock provider (which has allowUnknownModels: true)
|
|
63
|
-
const request = {
|
|
64
|
-
providerId: 'mock',
|
|
65
|
-
modelId: 'totally-unknown-model-xyz',
|
|
66
|
-
messages: [{ role: 'user', content: 'Testing flexible provider' }]
|
|
67
|
-
};
|
|
68
|
-
const response = await service.sendMessage(request);
|
|
69
|
-
// Should succeed with mock response
|
|
70
|
-
expect(response.object).toBe('chat.completion');
|
|
71
|
-
// Should NOT warn about unknown model (filter out adapter constructor warnings)
|
|
72
|
-
const unknownModelWarnings = warnings.filter(w => !w.includes('No adapter constructor'));
|
|
73
|
-
expect(unknownModelWarnings.length).toBe(0); // No warnings for flexible providers
|
|
74
|
-
consoleWarnSpy.mockRestore();
|
|
75
|
-
});
|
|
76
|
-
it('should return validation error for empty messages', async () => {
|
|
77
|
-
const request = {
|
|
78
|
-
providerId: 'openai',
|
|
79
|
-
modelId: 'gpt-4.1',
|
|
80
|
-
messages: []
|
|
81
|
-
};
|
|
82
|
-
const response = await service.sendMessage(request);
|
|
83
|
-
expect(response.object).toBe('error');
|
|
84
|
-
const errorResponse = response;
|
|
85
|
-
expect(errorResponse.error.code).toBe('INVALID_REQUEST');
|
|
86
|
-
expect(errorResponse.error.message).toContain('Request must contain at least one message');
|
|
87
|
-
});
|
|
88
|
-
it('should return validation error for invalid message role', async () => {
|
|
89
|
-
const request = {
|
|
90
|
-
providerId: 'openai',
|
|
91
|
-
modelId: 'gpt-4.1',
|
|
92
|
-
messages: [{ role: 'invalid', content: 'Hello' }]
|
|
93
|
-
};
|
|
94
|
-
const response = await service.sendMessage(request);
|
|
95
|
-
expect(response.object).toBe('error');
|
|
96
|
-
const errorResponse = response;
|
|
97
|
-
expect(errorResponse.error.code).toBe('INVALID_MESSAGE_ROLE');
|
|
98
|
-
expect(errorResponse.error.message).toContain('Invalid message role');
|
|
99
|
-
});
|
|
100
|
-
it('should return validation error for empty message content', async () => {
|
|
101
|
-
const request = {
|
|
102
|
-
providerId: 'openai',
|
|
103
|
-
modelId: 'gpt-4.1',
|
|
104
|
-
messages: [{ role: 'user', content: '' }]
|
|
105
|
-
};
|
|
106
|
-
const response = await service.sendMessage(request);
|
|
107
|
-
expect(response.object).toBe('error');
|
|
108
|
-
const errorResponse = response;
|
|
109
|
-
expect(errorResponse.error.code).toBe('INVALID_MESSAGE');
|
|
110
|
-
expect(errorResponse.error.message).toContain('Message at index 0 must have both');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
describe('API key handling', () => {
|
|
114
|
-
it('should return error when API key provider returns null', async () => {
|
|
115
|
-
mockApiKeyProvider.mockResolvedValueOnce(null);
|
|
116
|
-
const request = {
|
|
117
|
-
providerId: 'openai',
|
|
118
|
-
modelId: 'gpt-4.1',
|
|
119
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
120
|
-
};
|
|
121
|
-
const response = await service.sendMessage(request);
|
|
122
|
-
expect(response.object).toBe('error');
|
|
123
|
-
const errorResponse = response;
|
|
124
|
-
expect(errorResponse.error.code).toBe('API_KEY_ERROR');
|
|
125
|
-
expect(errorResponse.error.message).toContain('API key for provider');
|
|
126
|
-
});
|
|
127
|
-
it('should return error when API key provider throws', async () => {
|
|
128
|
-
mockApiKeyProvider.mockRejectedValueOnce(new Error('Key provider error'));
|
|
129
|
-
const request = {
|
|
130
|
-
providerId: 'openai',
|
|
131
|
-
modelId: 'gpt-4.1',
|
|
132
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
133
|
-
};
|
|
134
|
-
const response = await service.sendMessage(request);
|
|
135
|
-
expect(response.object).toBe('error');
|
|
136
|
-
const errorResponse = response;
|
|
137
|
-
expect(errorResponse.error.code).toBe('PROVIDER_ERROR');
|
|
138
|
-
expect(errorResponse.error.message).toContain('Key provider error');
|
|
139
|
-
});
|
|
140
|
-
it('should return error for invalid API key format', async () => {
|
|
141
|
-
mockApiKeyProvider.mockResolvedValueOnce('invalid-key');
|
|
142
|
-
const request = {
|
|
143
|
-
providerId: 'openai',
|
|
144
|
-
modelId: 'gpt-4.1',
|
|
145
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
146
|
-
};
|
|
147
|
-
const response = await service.sendMessage(request);
|
|
148
|
-
// OpenAI adapter expects keys starting with 'sk-'
|
|
149
|
-
expect(response.object).toBe('error');
|
|
150
|
-
const errorResponse = response;
|
|
151
|
-
expect(errorResponse.error.code).toBe('INVALID_API_KEY');
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
describe('adapter routing', () => {
|
|
155
|
-
it('should route request to correct adapter based on provider', async () => {
|
|
156
|
-
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
157
|
-
const request = {
|
|
158
|
-
providerId: 'openai',
|
|
159
|
-
modelId: 'gpt-4.1',
|
|
160
|
-
messages: [{ role: 'user', content: 'Test routing' }]
|
|
161
|
-
};
|
|
162
|
-
const response = await service.sendMessage(request);
|
|
163
|
-
// This will fail with a network error since we're not mocking the actual API
|
|
164
|
-
expect(response.object).toBe('error');
|
|
165
|
-
const errorResponse = response;
|
|
166
|
-
// We should get a network error or similar since we're not mocking the HTTP request
|
|
167
|
-
expect(errorResponse.provider).toBe('openai');
|
|
168
|
-
});
|
|
169
|
-
it('should reuse existing adapter for same provider', async () => {
|
|
170
|
-
const request = {
|
|
171
|
-
providerId: 'mock',
|
|
172
|
-
modelId: 'mock-model',
|
|
173
|
-
messages: [{ role: 'user', content: 'First request' }]
|
|
174
|
-
};
|
|
175
|
-
// First request
|
|
176
|
-
await service.sendMessage(request);
|
|
177
|
-
// Second request to same provider
|
|
178
|
-
request.messages = [{ role: 'user', content: 'Second request' }];
|
|
179
|
-
await service.sendMessage(request);
|
|
180
|
-
// API key provider should be called once per unique provider (mock provider now registered)
|
|
181
|
-
expect(mockApiKeyProvider).toHaveBeenCalledTimes(2);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe('settings management', () => {
|
|
185
|
-
it('should apply default settings when none provided', async () => {
|
|
186
|
-
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
187
|
-
const request = {
|
|
188
|
-
providerId: 'openai',
|
|
189
|
-
modelId: 'gpt-4.1',
|
|
190
|
-
messages: [{ role: 'user', content: 'Hello' }]
|
|
191
|
-
};
|
|
192
|
-
const response = await service.sendMessage(request);
|
|
193
|
-
// We'll get a network error but can still verify the request was attempted
|
|
194
|
-
expect(response.object).toBe('error');
|
|
195
|
-
expect(mockApiKeyProvider).toHaveBeenCalledWith('openai');
|
|
196
|
-
});
|
|
197
|
-
it('should merge user settings with defaults', async () => {
|
|
198
|
-
mockApiKeyProvider.mockResolvedValueOnce('sk-test-key-12345678901234567890');
|
|
199
|
-
const request = {
|
|
200
|
-
providerId: 'openai',
|
|
201
|
-
modelId: 'gpt-4.1',
|
|
202
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
203
|
-
settings: {
|
|
204
|
-
temperature: 0.9,
|
|
205
|
-
maxTokens: 500
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
const response = await service.sendMessage(request);
|
|
209
|
-
// We'll get a network error but the settings should still be validated
|
|
210
|
-
expect(response.object).toBe('error');
|
|
211
|
-
});
|
|
212
|
-
it('should validate temperature setting', async () => {
|
|
213
|
-
const request = {
|
|
214
|
-
providerId: 'openai',
|
|
215
|
-
modelId: 'gpt-4.1',
|
|
216
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
217
|
-
settings: {
|
|
218
|
-
temperature: 2.5 // Out of range
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
const response = await service.sendMessage(request);
|
|
222
|
-
expect(response.object).toBe('error');
|
|
223
|
-
const errorResponse = response;
|
|
224
|
-
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
225
|
-
expect(errorResponse.error.message).toContain('temperature must be a number between');
|
|
226
|
-
});
|
|
227
|
-
it('should validate maxTokens setting', async () => {
|
|
228
|
-
const request = {
|
|
229
|
-
providerId: 'openai',
|
|
230
|
-
modelId: 'gpt-4.1',
|
|
231
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
232
|
-
settings: {
|
|
233
|
-
maxTokens: 0 // Invalid
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
const response = await service.sendMessage(request);
|
|
237
|
-
expect(response.object).toBe('error');
|
|
238
|
-
const errorResponse = response;
|
|
239
|
-
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
240
|
-
expect(errorResponse.error.message).toContain('maxTokens must be an integer between');
|
|
241
|
-
});
|
|
242
|
-
it('should validate topP setting', async () => {
|
|
243
|
-
const request = {
|
|
244
|
-
providerId: 'openai',
|
|
245
|
-
modelId: 'gpt-4.1',
|
|
246
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
247
|
-
settings: {
|
|
248
|
-
topP: -0.1 // Out of range
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
const response = await service.sendMessage(request);
|
|
252
|
-
expect(response.object).toBe('error');
|
|
253
|
-
const errorResponse = response;
|
|
254
|
-
expect(errorResponse.error.code).toBe('INVALID_SETTINGS');
|
|
255
|
-
expect(errorResponse.error.message).toContain('topP must be a number between 0 and 1');
|
|
256
|
-
});
|
|
257
|
-
it('should reject reasoning settings for non-reasoning models', async () => {
|
|
258
|
-
const request = {
|
|
259
|
-
providerId: 'openai',
|
|
260
|
-
modelId: 'gpt-4.1', // This model doesn't support reasoning
|
|
261
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
262
|
-
settings: {
|
|
263
|
-
reasoning: {
|
|
264
|
-
enabled: true
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
const response = await service.sendMessage(request);
|
|
269
|
-
expect(response.object).toBe('error');
|
|
270
|
-
const errorResponse = response;
|
|
271
|
-
expect(errorResponse.error.code).toBe('reasoning_not_supported');
|
|
272
|
-
expect(errorResponse.error.message).toContain('does not support reasoning/thinking');
|
|
273
|
-
});
|
|
274
|
-
it('should reject reasoning with effort for non-reasoning models', async () => {
|
|
275
|
-
const request = {
|
|
276
|
-
providerId: 'openai',
|
|
277
|
-
modelId: 'gpt-4.1',
|
|
278
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
279
|
-
settings: {
|
|
280
|
-
reasoning: {
|
|
281
|
-
effort: 'high'
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
const response = await service.sendMessage(request);
|
|
286
|
-
expect(response.object).toBe('error');
|
|
287
|
-
const errorResponse = response;
|
|
288
|
-
expect(errorResponse.error.code).toBe('reasoning_not_supported');
|
|
289
|
-
});
|
|
290
|
-
it('should reject reasoning with maxTokens for non-reasoning models', async () => {
|
|
291
|
-
const request = {
|
|
292
|
-
providerId: 'openai',
|
|
293
|
-
modelId: 'gpt-4.1',
|
|
294
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
295
|
-
settings: {
|
|
296
|
-
reasoning: {
|
|
297
|
-
maxTokens: 5000
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
const response = await service.sendMessage(request);
|
|
302
|
-
expect(response.object).toBe('error');
|
|
303
|
-
const errorResponse = response;
|
|
304
|
-
expect(errorResponse.error.code).toBe('reasoning_not_supported');
|
|
305
|
-
});
|
|
306
|
-
it('should allow disabled reasoning for non-reasoning models', async () => {
|
|
307
|
-
const request = {
|
|
308
|
-
providerId: 'openai',
|
|
309
|
-
modelId: 'gpt-4.1',
|
|
310
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
311
|
-
settings: {
|
|
312
|
-
reasoning: {
|
|
313
|
-
enabled: false
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
// This should pass validation but will fail at the adapter level since we don't have a real API key
|
|
318
|
-
const response = await service.sendMessage(request);
|
|
319
|
-
// Should not be a reasoning validation error
|
|
320
|
-
const errorResponse = response;
|
|
321
|
-
expect(errorResponse.error.code).not.toBe('reasoning_not_supported');
|
|
322
|
-
});
|
|
323
|
-
it('should allow reasoning with exclude=true for non-reasoning models', async () => {
|
|
324
|
-
const request = {
|
|
325
|
-
providerId: 'openai',
|
|
326
|
-
modelId: 'gpt-4.1',
|
|
327
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
328
|
-
settings: {
|
|
329
|
-
reasoning: {
|
|
330
|
-
exclude: true
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
// This should pass validation
|
|
335
|
-
const response = await service.sendMessage(request);
|
|
336
|
-
// Should not be a reasoning validation error
|
|
337
|
-
const errorResponse = response;
|
|
338
|
-
expect(errorResponse.error.code).not.toBe('reasoning_not_supported');
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
describe('getProviders', () => {
|
|
343
|
-
it('should return all supported providers', async () => {
|
|
344
|
-
const providers = await service.getProviders();
|
|
345
|
-
expect(providers).toHaveLength(6);
|
|
346
|
-
expect(providers.find(p => p.id === 'openai')).toBeDefined();
|
|
347
|
-
expect(providers.find(p => p.id === 'anthropic')).toBeDefined();
|
|
348
|
-
expect(providers.find(p => p.id === 'gemini')).toBeDefined();
|
|
349
|
-
expect(providers.find(p => p.id === 'mistral')).toBeDefined();
|
|
350
|
-
expect(providers.find(p => p.id === 'llamacpp')).toBeDefined();
|
|
351
|
-
expect(providers.find(p => p.id === 'mock')).toBeDefined();
|
|
352
|
-
});
|
|
353
|
-
it('should include provider metadata', async () => {
|
|
354
|
-
const providers = await service.getProviders();
|
|
355
|
-
const openai = providers.find(p => p.id === 'openai');
|
|
356
|
-
expect(openai).toMatchObject({
|
|
357
|
-
id: 'openai',
|
|
358
|
-
name: 'OpenAI'
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
describe('getModels', () => {
|
|
363
|
-
it('should return all models for a provider', async () => {
|
|
364
|
-
const models = await service.getModels('openai');
|
|
365
|
-
expect(models.length).toBeGreaterThan(0);
|
|
366
|
-
expect(models.some(m => m.id.includes('gpt-4'))).toBe(true);
|
|
367
|
-
expect(models.some(m => m.id.includes('o4-mini'))).toBe(true);
|
|
368
|
-
});
|
|
369
|
-
it('should return empty array for invalid provider', async () => {
|
|
370
|
-
const models = await service.getModels('invalid-provider');
|
|
371
|
-
expect(models).toEqual([]);
|
|
372
|
-
});
|
|
373
|
-
it('should include model metadata', async () => {
|
|
374
|
-
const models = await service.getModels('openai');
|
|
375
|
-
const gpt4 = models.find(m => m.id === 'gpt-4.1');
|
|
376
|
-
expect(gpt4).toBeDefined();
|
|
377
|
-
expect(gpt4.contextWindow).toBeGreaterThan(0);
|
|
378
|
-
expect(gpt4.maxTokens).toBeGreaterThan(0);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
describe('thinking extraction', () => {
|
|
382
|
-
it('should extract thinking tag from response when enabled', async () => {
|
|
383
|
-
// Use mistral provider which doesn't have an adapter, so MockClientAdapter will be used
|
|
384
|
-
const request = {
|
|
385
|
-
providerId: 'mistral',
|
|
386
|
-
modelId: 'codestral-2501',
|
|
387
|
-
messages: [{ role: 'user', content: 'test_thinking:<thinking>I am thinking about this problem.</thinking>Here is the answer.' }],
|
|
388
|
-
settings: {
|
|
389
|
-
thinkingExtraction: {
|
|
390
|
-
enabled: true,
|
|
391
|
-
tag: 'thinking'
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
const response = await service.sendMessage(request);
|
|
396
|
-
expect(response.object).toBe('chat.completion');
|
|
397
|
-
const successResponse = response;
|
|
398
|
-
expect(successResponse.choices[0].reasoning).toBe('I am thinking about this problem.');
|
|
399
|
-
expect(successResponse.choices[0].message.content).toBe('Here is the answer.');
|
|
400
|
-
});
|
|
401
|
-
it('should not extract thinking tag when disabled', async () => {
|
|
402
|
-
const request = {
|
|
403
|
-
providerId: 'mistral',
|
|
404
|
-
modelId: 'codestral-2501',
|
|
405
|
-
messages: [{ role: 'user', content: 'test_thinking:<thinking>I am thinking about this problem.</thinking>Here is the answer.' }],
|
|
406
|
-
settings: {
|
|
407
|
-
thinkingExtraction: {
|
|
408
|
-
enabled: false,
|
|
409
|
-
tag: 'thinking'
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
const response = await service.sendMessage(request);
|
|
414
|
-
expect(response.object).toBe('chat.completion');
|
|
415
|
-
const successResponse = response;
|
|
416
|
-
expect(successResponse.choices[0].reasoning).toBeUndefined();
|
|
417
|
-
expect(successResponse.choices[0].message.content).toBe('<thinking>I am thinking about this problem.</thinking>Here is the answer.');
|
|
418
|
-
});
|
|
419
|
-
it('should use custom tag name', async () => {
|
|
420
|
-
const request = {
|
|
421
|
-
providerId: 'mistral',
|
|
422
|
-
modelId: 'codestral-2501',
|
|
423
|
-
messages: [{ role: 'user', content: 'test_thinking:<scratchpad>Working through the logic...</scratchpad>Final answer is 42.' }],
|
|
424
|
-
settings: {
|
|
425
|
-
thinkingExtraction: {
|
|
426
|
-
enabled: true,
|
|
427
|
-
tag: 'scratchpad'
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
const response = await service.sendMessage(request);
|
|
432
|
-
expect(response.object).toBe('chat.completion');
|
|
433
|
-
const successResponse = response;
|
|
434
|
-
expect(successResponse.choices[0].reasoning).toBe('Working through the logic...');
|
|
435
|
-
expect(successResponse.choices[0].message.content).toBe('Final answer is 42.');
|
|
436
|
-
});
|
|
437
|
-
it('should append to existing reasoning', async () => {
|
|
438
|
-
// Use test_reasoning to get a response with existing reasoning, then test extraction appends to it
|
|
439
|
-
const request = {
|
|
440
|
-
providerId: 'mistral',
|
|
441
|
-
modelId: 'codestral-2501',
|
|
442
|
-
messages: [{ role: 'user', content: 'test_reasoning:<thinking>Additional thoughts here.</thinking>The analysis is complete.' }],
|
|
443
|
-
settings: {
|
|
444
|
-
thinkingExtraction: {
|
|
445
|
-
enabled: true,
|
|
446
|
-
tag: 'thinking'
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
const response = await service.sendMessage(request);
|
|
451
|
-
expect(response.object).toBe('chat.completion');
|
|
452
|
-
const successResponse = response;
|
|
453
|
-
// Should contain both the initial reasoning and the extracted thinking with separator
|
|
454
|
-
expect(successResponse.choices[0].reasoning).toBe('Initial model reasoning from native capabilities.\n\n#### Additional Reasoning\n\nAdditional thoughts here.');
|
|
455
|
-
expect(successResponse.choices[0].message.content).toBe('The analysis is complete.');
|
|
456
|
-
});
|
|
457
|
-
it('should handle missing tag with explicit ignore', async () => {
|
|
458
|
-
const request = {
|
|
459
|
-
providerId: 'mistral',
|
|
460
|
-
modelId: 'codestral-2501',
|
|
461
|
-
messages: [{ role: 'user', content: 'test_thinking:This response has no thinking tag.' }],
|
|
462
|
-
settings: {
|
|
463
|
-
thinkingExtraction: {
|
|
464
|
-
enabled: true,
|
|
465
|
-
tag: 'thinking',
|
|
466
|
-
onMissing: 'ignore' // Explicitly set to ignore
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
const response = await service.sendMessage(request);
|
|
471
|
-
expect(response.object).toBe('chat.completion');
|
|
472
|
-
const successResponse = response;
|
|
473
|
-
expect(successResponse.choices[0].reasoning).toBeUndefined();
|
|
474
|
-
expect(successResponse.choices[0].message.content).toBe('This response has no thinking tag.');
|
|
475
|
-
});
|
|
476
|
-
it('should use default settings when not specified', async () => {
|
|
477
|
-
// Default is now disabled, needs explicit opt-in
|
|
478
|
-
const request = {
|
|
479
|
-
providerId: 'mistral',
|
|
480
|
-
modelId: 'codestral-2501',
|
|
481
|
-
messages: [{ role: 'user', content: 'test_thinking:<thinking>Default extraction test.</thinking>Result here.' }]
|
|
482
|
-
};
|
|
483
|
-
const response = await service.sendMessage(request);
|
|
484
|
-
expect(response.object).toBe('chat.completion');
|
|
485
|
-
const successResponse = response;
|
|
486
|
-
// With default settings (enabled: false), no extraction should occur
|
|
487
|
-
expect(successResponse.choices[0].reasoning).toBeUndefined();
|
|
488
|
-
expect(successResponse.choices[0].message.content).toBe('<thinking>Default extraction test.</thinking>Result here.');
|
|
489
|
-
});
|
|
490
|
-
describe('onMissing behavior', () => {
|
|
491
|
-
it('should use auto mode by default with error for non-native models', async () => {
|
|
492
|
-
const request = {
|
|
493
|
-
providerId: 'mistral',
|
|
494
|
-
modelId: 'codestral-2501', // Non-native reasoning model (using mock)
|
|
495
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
496
|
-
settings: {
|
|
497
|
-
thinkingExtraction: {
|
|
498
|
-
enabled: true,
|
|
499
|
-
// onMissing defaults to 'auto'
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
const response = await service.sendMessage(request);
|
|
504
|
-
expect(response.object).toBe('error');
|
|
505
|
-
const errorResponse = response;
|
|
506
|
-
expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
|
|
507
|
-
expect(errorResponse.error.type).toBe('validation_error');
|
|
508
|
-
expect(errorResponse.error.message).toContain('response was expected to start with a <thinking> tag');
|
|
509
|
-
expect(errorResponse.error.message).toContain('does not have native reasoning active');
|
|
510
|
-
// Check that partial response is included
|
|
511
|
-
expect(errorResponse.partialResponse).toBeDefined();
|
|
512
|
-
expect(errorResponse.partialResponse.choices[0].message.content).toBe('Response without thinking tag.');
|
|
513
|
-
});
|
|
514
|
-
it('should handle missing tag for non-reasoning model with warn', async () => {
|
|
515
|
-
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
516
|
-
const request = {
|
|
517
|
-
providerId: 'mistral',
|
|
518
|
-
modelId: 'codestral-2501',
|
|
519
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
520
|
-
settings: {
|
|
521
|
-
thinkingExtraction: {
|
|
522
|
-
enabled: true,
|
|
523
|
-
onMissing: 'warn'
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
const response = await service.sendMessage(request);
|
|
528
|
-
expect(response.object).toBe('chat.completion');
|
|
529
|
-
const successResponse = response;
|
|
530
|
-
expect(successResponse.choices[0].message.content).toBe('Response without thinking tag.');
|
|
531
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Expected <thinking> tag was not found'));
|
|
532
|
-
consoleSpy.mockRestore();
|
|
533
|
-
});
|
|
534
|
-
it('should handle missing tag with explicit error mode', async () => {
|
|
535
|
-
const request = {
|
|
536
|
-
providerId: 'mistral',
|
|
537
|
-
modelId: 'codestral-2501',
|
|
538
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
539
|
-
settings: {
|
|
540
|
-
thinkingExtraction: {
|
|
541
|
-
enabled: true,
|
|
542
|
-
onMissing: 'error' // Explicitly set to error
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
const response = await service.sendMessage(request);
|
|
547
|
-
expect(response.object).toBe('error');
|
|
548
|
-
const errorResponse = response;
|
|
549
|
-
expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
|
|
550
|
-
expect(errorResponse.error.message).toContain('response was expected to start with a <thinking> tag');
|
|
551
|
-
// Check that partial response is included
|
|
552
|
-
expect(errorResponse.partialResponse).toBeDefined();
|
|
553
|
-
expect(errorResponse.partialResponse.choices[0].message.content).toBe('Response without thinking tag.');
|
|
554
|
-
});
|
|
555
|
-
it('should handle missing tag for non-reasoning model with ignore', async () => {
|
|
556
|
-
const request = {
|
|
557
|
-
providerId: 'mistral',
|
|
558
|
-
modelId: 'codestral-2501',
|
|
559
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
560
|
-
settings: {
|
|
561
|
-
thinkingExtraction: {
|
|
562
|
-
enabled: true,
|
|
563
|
-
onMissing: 'ignore'
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
const response = await service.sendMessage(request);
|
|
568
|
-
expect(response.object).toBe('chat.completion');
|
|
569
|
-
const successResponse = response;
|
|
570
|
-
expect(successResponse.choices[0].message.content).toBe('Response without thinking tag.');
|
|
571
|
-
});
|
|
572
|
-
it('should work with custom tag names in error messages', async () => {
|
|
573
|
-
const request = {
|
|
574
|
-
providerId: 'mistral',
|
|
575
|
-
modelId: 'codestral-2501',
|
|
576
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without custom tag.' }],
|
|
577
|
-
settings: {
|
|
578
|
-
thinkingExtraction: {
|
|
579
|
-
enabled: true,
|
|
580
|
-
tag: 'reasoning',
|
|
581
|
-
onMissing: 'error'
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
const response = await service.sendMessage(request);
|
|
586
|
-
expect(response.object).toBe('error');
|
|
587
|
-
const errorResponse = response;
|
|
588
|
-
expect(errorResponse.error.message).toContain('expected to start with a <reasoning> tag');
|
|
589
|
-
expect(errorResponse.partialResponse).toBeDefined();
|
|
590
|
-
expect(errorResponse.partialResponse.choices[0].message.content).toBe('Response without custom tag.');
|
|
591
|
-
});
|
|
592
|
-
describe('auto mode with native reasoning detection', () => {
|
|
593
|
-
it('should enforce thinking tags for non-reasoning models by default', async () => {
|
|
594
|
-
// Mistral model doesn't have reasoning support
|
|
595
|
-
const request = {
|
|
596
|
-
providerId: 'mistral',
|
|
597
|
-
modelId: 'codestral-2501',
|
|
598
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
599
|
-
settings: {
|
|
600
|
-
thinkingExtraction: {
|
|
601
|
-
enabled: true,
|
|
602
|
-
onMissing: 'auto'
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
const response = await service.sendMessage(request);
|
|
607
|
-
// Should error because model doesn't have native reasoning
|
|
608
|
-
expect(response.object).toBe('error');
|
|
609
|
-
const errorResponse = response;
|
|
610
|
-
expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
|
|
611
|
-
expect(errorResponse.error.message).toContain('does not have native reasoning active');
|
|
612
|
-
expect(errorResponse.partialResponse).toBeDefined();
|
|
613
|
-
expect(errorResponse.partialResponse.choices[0].message.content).toBe('Response without thinking tag.');
|
|
614
|
-
});
|
|
615
|
-
it('should respect explicit reasoning.enabled: false even for models with enabledByDefault', async () => {
|
|
616
|
-
// This is the key test for the fix
|
|
617
|
-
const request = {
|
|
618
|
-
providerId: 'mistral',
|
|
619
|
-
modelId: 'codestral-2501',
|
|
620
|
-
messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
|
|
621
|
-
settings: {
|
|
622
|
-
reasoning: { enabled: false }, // Explicitly disabled
|
|
623
|
-
thinkingExtraction: {
|
|
624
|
-
enabled: true,
|
|
625
|
-
onMissing: 'auto'
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
const response = await service.sendMessage(request);
|
|
630
|
-
// Should error because reasoning is explicitly disabled
|
|
631
|
-
expect(response.object).toBe('error');
|
|
632
|
-
const errorResponse = response;
|
|
633
|
-
expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
|
|
634
|
-
expect(errorResponse.partialResponse).toBeDefined();
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|