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.
Files changed (59) hide show
  1. package/README.md +47 -37
  2. package/dist/llm/LLMService.d.ts +29 -2
  3. package/dist/llm/LLMService.js +80 -36
  4. package/dist/llm/config.js +4 -4
  5. package/dist/llm/services/SettingsManager.js +17 -11
  6. package/dist/llm/types.d.ts +81 -22
  7. package/dist/prompting/parser.d.ts +2 -2
  8. package/dist/prompting/parser.js +2 -2
  9. package/package.json +1 -1
  10. package/dist/llm/LLMService.createMessages.test.d.ts +0 -4
  11. package/dist/llm/LLMService.createMessages.test.js +0 -364
  12. package/dist/llm/LLMService.original.d.ts +0 -147
  13. package/dist/llm/LLMService.original.js +0 -656
  14. package/dist/llm/LLMService.prepareMessage.test.d.ts +0 -1
  15. package/dist/llm/LLMService.prepareMessage.test.js +0 -303
  16. package/dist/llm/LLMService.presets.test.d.ts +0 -1
  17. package/dist/llm/LLMService.presets.test.js +0 -210
  18. package/dist/llm/LLMService.sendMessage.preset.test.d.ts +0 -1
  19. package/dist/llm/LLMService.sendMessage.preset.test.js +0 -153
  20. package/dist/llm/LLMService.test.d.ts +0 -1
  21. package/dist/llm/LLMService.test.js +0 -639
  22. package/dist/llm/clients/AnthropicClientAdapter.test.d.ts +0 -1
  23. package/dist/llm/clients/AnthropicClientAdapter.test.js +0 -273
  24. package/dist/llm/clients/GeminiClientAdapter.test.d.ts +0 -1
  25. package/dist/llm/clients/GeminiClientAdapter.test.js +0 -405
  26. package/dist/llm/clients/LlamaCppClientAdapter.test.d.ts +0 -1
  27. package/dist/llm/clients/LlamaCppClientAdapter.test.js +0 -447
  28. package/dist/llm/clients/LlamaCppServerClient.test.d.ts +0 -1
  29. package/dist/llm/clients/LlamaCppServerClient.test.js +0 -294
  30. package/dist/llm/clients/MockClientAdapter.test.d.ts +0 -1
  31. package/dist/llm/clients/MockClientAdapter.test.js +0 -250
  32. package/dist/llm/clients/OpenAIClientAdapter.test.d.ts +0 -1
  33. package/dist/llm/clients/OpenAIClientAdapter.test.js +0 -258
  34. package/dist/llm/clients/adapterErrorUtils.test.d.ts +0 -1
  35. package/dist/llm/clients/adapterErrorUtils.test.js +0 -123
  36. package/dist/llm/config.test.d.ts +0 -1
  37. package/dist/llm/config.test.js +0 -176
  38. package/dist/llm/services/AdapterRegistry.test.d.ts +0 -1
  39. package/dist/llm/services/AdapterRegistry.test.js +0 -239
  40. package/dist/llm/services/ModelResolver.test.d.ts +0 -1
  41. package/dist/llm/services/ModelResolver.test.js +0 -179
  42. package/dist/llm/services/PresetManager.test.d.ts +0 -1
  43. package/dist/llm/services/PresetManager.test.js +0 -210
  44. package/dist/llm/services/RequestValidator.test.d.ts +0 -1
  45. package/dist/llm/services/RequestValidator.test.js +0 -159
  46. package/dist/llm/services/SettingsManager.test.d.ts +0 -1
  47. package/dist/llm/services/SettingsManager.test.js +0 -266
  48. package/dist/prompting/builder.d.ts +0 -38
  49. package/dist/prompting/builder.js +0 -63
  50. package/dist/prompting/builder.test.d.ts +0 -4
  51. package/dist/prompting/builder.test.js +0 -109
  52. package/dist/prompting/content.test.d.ts +0 -4
  53. package/dist/prompting/content.test.js +0 -212
  54. package/dist/prompting/parser.test.d.ts +0 -4
  55. package/dist/prompting/parser.test.js +0 -464
  56. package/dist/prompting/template.test.d.ts +0 -1
  57. package/dist/prompting/template.test.js +0 -250
  58. package/dist/providers/fromEnvironment.test.d.ts +0 -1
  59. 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 {};