genai-lite 0.2.1 → 0.3.1

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 (49) hide show
  1. package/README.md +382 -49
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.js +4 -3
  4. package/dist/llm/LLMService.createMessages.test.d.ts +4 -0
  5. package/dist/llm/LLMService.createMessages.test.js +364 -0
  6. package/dist/llm/LLMService.d.ts +48 -83
  7. package/dist/llm/LLMService.js +176 -480
  8. package/dist/llm/LLMService.original.d.ts +147 -0
  9. package/dist/llm/LLMService.original.js +656 -0
  10. package/dist/llm/LLMService.test.js +187 -0
  11. package/dist/llm/clients/AnthropicClientAdapter.test.js +4 -0
  12. package/dist/llm/clients/GeminiClientAdapter.test.js +4 -0
  13. package/dist/llm/clients/MockClientAdapter.js +29 -13
  14. package/dist/llm/clients/MockClientAdapter.test.js +4 -0
  15. package/dist/llm/clients/OpenAIClientAdapter.test.js +4 -0
  16. package/dist/llm/config.js +5 -0
  17. package/dist/llm/services/AdapterRegistry.d.ts +59 -0
  18. package/dist/llm/services/AdapterRegistry.js +113 -0
  19. package/dist/llm/services/AdapterRegistry.test.d.ts +1 -0
  20. package/dist/llm/services/AdapterRegistry.test.js +239 -0
  21. package/dist/llm/services/ModelResolver.d.ts +35 -0
  22. package/dist/llm/services/ModelResolver.js +116 -0
  23. package/dist/llm/services/ModelResolver.test.d.ts +1 -0
  24. package/dist/llm/services/ModelResolver.test.js +158 -0
  25. package/dist/llm/services/PresetManager.d.ts +27 -0
  26. package/dist/llm/services/PresetManager.js +50 -0
  27. package/dist/llm/services/PresetManager.test.d.ts +1 -0
  28. package/dist/llm/services/PresetManager.test.js +210 -0
  29. package/dist/llm/services/RequestValidator.d.ts +31 -0
  30. package/dist/llm/services/RequestValidator.js +122 -0
  31. package/dist/llm/services/RequestValidator.test.d.ts +1 -0
  32. package/dist/llm/services/RequestValidator.test.js +159 -0
  33. package/dist/llm/services/SettingsManager.d.ts +32 -0
  34. package/dist/llm/services/SettingsManager.js +223 -0
  35. package/dist/llm/services/SettingsManager.test.d.ts +1 -0
  36. package/dist/llm/services/SettingsManager.test.js +266 -0
  37. package/dist/llm/types.d.ts +29 -28
  38. package/dist/prompting/builder.d.ts +4 -0
  39. package/dist/prompting/builder.js +12 -61
  40. package/dist/prompting/content.js +3 -9
  41. package/dist/prompting/index.d.ts +2 -3
  42. package/dist/prompting/index.js +4 -5
  43. package/dist/prompting/parser.d.ts +80 -0
  44. package/dist/prompting/parser.js +133 -0
  45. package/dist/prompting/parser.test.js +348 -0
  46. package/dist/prompting/template.d.ts +8 -0
  47. package/dist/prompting/template.js +89 -6
  48. package/dist/prompting/template.test.js +116 -0
  49. package/package.json +1 -1
@@ -359,4 +359,191 @@ describe('LLMService', () => {
359
359
  expect(gpt4.maxTokens).toBeGreaterThan(0);
360
360
  });
361
361
  });
362
+ describe('thinking extraction', () => {
363
+ it('should extract thinking tag from response when enabled', async () => {
364
+ // Use mistral provider which doesn't have an adapter, so MockClientAdapter will be used
365
+ const request = {
366
+ providerId: 'mistral',
367
+ modelId: 'codestral-2501',
368
+ messages: [{ role: 'user', content: 'test_thinking:<thinking>I am thinking about this problem.</thinking>Here is the answer.' }],
369
+ settings: {
370
+ thinkingExtraction: {
371
+ enabled: true,
372
+ tag: 'thinking'
373
+ }
374
+ }
375
+ };
376
+ const response = await service.sendMessage(request);
377
+ expect(response.object).toBe('chat.completion');
378
+ const successResponse = response;
379
+ expect(successResponse.choices[0].reasoning).toBe('I am thinking about this problem.');
380
+ expect(successResponse.choices[0].message.content).toBe('Here is the answer.');
381
+ });
382
+ it('should not extract thinking tag when disabled', async () => {
383
+ const request = {
384
+ providerId: 'mistral',
385
+ modelId: 'codestral-2501',
386
+ messages: [{ role: 'user', content: 'test_thinking:<thinking>I am thinking about this problem.</thinking>Here is the answer.' }],
387
+ settings: {
388
+ thinkingExtraction: {
389
+ enabled: false,
390
+ tag: 'thinking'
391
+ }
392
+ }
393
+ };
394
+ const response = await service.sendMessage(request);
395
+ expect(response.object).toBe('chat.completion');
396
+ const successResponse = response;
397
+ expect(successResponse.choices[0].reasoning).toBeUndefined();
398
+ expect(successResponse.choices[0].message.content).toBe('<thinking>I am thinking about this problem.</thinking>Here is the answer.');
399
+ });
400
+ it('should use custom tag name', async () => {
401
+ const request = {
402
+ providerId: 'mistral',
403
+ modelId: 'codestral-2501',
404
+ messages: [{ role: 'user', content: 'test_thinking:<scratchpad>Working through the logic...</scratchpad>Final answer is 42.' }],
405
+ settings: {
406
+ thinkingExtraction: {
407
+ enabled: true,
408
+ tag: 'scratchpad'
409
+ }
410
+ }
411
+ };
412
+ const response = await service.sendMessage(request);
413
+ expect(response.object).toBe('chat.completion');
414
+ const successResponse = response;
415
+ expect(successResponse.choices[0].reasoning).toBe('Working through the logic...');
416
+ expect(successResponse.choices[0].message.content).toBe('Final answer is 42.');
417
+ });
418
+ it('should append to existing reasoning', async () => {
419
+ // Use test_reasoning to get a response with existing reasoning, then test extraction appends to it
420
+ const request = {
421
+ providerId: 'mistral',
422
+ modelId: 'codestral-2501',
423
+ messages: [{ role: 'user', content: 'test_reasoning:<thinking>Additional thoughts here.</thinking>The analysis is complete.' }],
424
+ settings: {
425
+ thinkingExtraction: {
426
+ enabled: true,
427
+ tag: 'thinking'
428
+ }
429
+ }
430
+ };
431
+ const response = await service.sendMessage(request);
432
+ expect(response.object).toBe('chat.completion');
433
+ const successResponse = response;
434
+ // Should contain both the initial reasoning and the extracted thinking with separator
435
+ expect(successResponse.choices[0].reasoning).toBe('Initial model reasoning from native capabilities.\n\n#### Additional Reasoning\n\nAdditional thoughts here.');
436
+ expect(successResponse.choices[0].message.content).toBe('The analysis is complete.');
437
+ });
438
+ it('should handle missing tag with explicit ignore', async () => {
439
+ const request = {
440
+ providerId: 'mistral',
441
+ modelId: 'codestral-2501',
442
+ messages: [{ role: 'user', content: 'test_thinking:This response has no thinking tag.' }],
443
+ settings: {
444
+ thinkingExtraction: {
445
+ enabled: true,
446
+ tag: 'thinking',
447
+ onMissing: 'ignore' // Explicitly set to ignore
448
+ }
449
+ }
450
+ };
451
+ const response = await service.sendMessage(request);
452
+ expect(response.object).toBe('chat.completion');
453
+ const successResponse = response;
454
+ expect(successResponse.choices[0].reasoning).toBeUndefined();
455
+ expect(successResponse.choices[0].message.content).toBe('This response has no thinking tag.');
456
+ });
457
+ it('should use default settings when not specified', async () => {
458
+ // Default is now disabled, needs explicit opt-in
459
+ const request = {
460
+ providerId: 'mistral',
461
+ modelId: 'codestral-2501',
462
+ messages: [{ role: 'user', content: 'test_thinking:<thinking>Default extraction test.</thinking>Result here.' }]
463
+ };
464
+ const response = await service.sendMessage(request);
465
+ expect(response.object).toBe('chat.completion');
466
+ const successResponse = response;
467
+ // With default settings (enabled: false), no extraction should occur
468
+ expect(successResponse.choices[0].reasoning).toBeUndefined();
469
+ expect(successResponse.choices[0].message.content).toBe('<thinking>Default extraction test.</thinking>Result here.');
470
+ });
471
+ describe('onMissing behavior', () => {
472
+ it('should use auto mode by default with error for non-native models', async () => {
473
+ const request = {
474
+ providerId: 'mistral',
475
+ modelId: 'codestral-2501', // Non-native reasoning model (using mock)
476
+ messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
477
+ settings: {
478
+ thinkingExtraction: {
479
+ enabled: true,
480
+ // onMissing defaults to 'auto'
481
+ }
482
+ }
483
+ };
484
+ const response = await service.sendMessage(request);
485
+ expect(response.object).toBe('error');
486
+ const errorResponse = response;
487
+ expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
488
+ expect(errorResponse.error.type).toBe('validation_error');
489
+ expect(errorResponse.error.message).toContain('response was expected to start with a <thinking> tag');
490
+ expect(errorResponse.error.message).toContain('does not have native reasoning active');
491
+ });
492
+ it('should handle missing tag for non-reasoning model with warn', async () => {
493
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
494
+ const request = {
495
+ providerId: 'mistral',
496
+ modelId: 'codestral-2501',
497
+ messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
498
+ settings: {
499
+ thinkingExtraction: {
500
+ enabled: true,
501
+ onMissing: 'warn'
502
+ }
503
+ }
504
+ };
505
+ const response = await service.sendMessage(request);
506
+ expect(response.object).toBe('chat.completion');
507
+ const successResponse = response;
508
+ expect(successResponse.choices[0].message.content).toBe('Response without thinking tag.');
509
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Expected <thinking> tag was not found'));
510
+ consoleSpy.mockRestore();
511
+ });
512
+ it('should handle missing tag for non-reasoning model with ignore', async () => {
513
+ const request = {
514
+ providerId: 'mistral',
515
+ modelId: 'codestral-2501',
516
+ messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
517
+ settings: {
518
+ thinkingExtraction: {
519
+ enabled: true,
520
+ onMissing: 'ignore'
521
+ }
522
+ }
523
+ };
524
+ const response = await service.sendMessage(request);
525
+ expect(response.object).toBe('chat.completion');
526
+ const successResponse = response;
527
+ expect(successResponse.choices[0].message.content).toBe('Response without thinking tag.');
528
+ });
529
+ it('should work with custom tag names in error messages', async () => {
530
+ const request = {
531
+ providerId: 'mistral',
532
+ modelId: 'codestral-2501',
533
+ messages: [{ role: 'user', content: 'test_thinking:Response without custom tag.' }],
534
+ settings: {
535
+ thinkingExtraction: {
536
+ enabled: true,
537
+ tag: 'reasoning',
538
+ onMissing: 'error'
539
+ }
540
+ }
541
+ };
542
+ const response = await service.sendMessage(request);
543
+ expect(response.object).toBe('error');
544
+ const errorResponse = response;
545
+ expect(errorResponse.error.message).toContain('expected to start with a <reasoning> tag');
546
+ });
547
+ });
548
+ });
362
549
  });
@@ -42,6 +42,10 @@ describe('AnthropicClientAdapter', () => {
42
42
  effort: undefined,
43
43
  maxTokens: undefined,
44
44
  exclude: false
45
+ },
46
+ thinkingExtraction: {
47
+ enabled: true,
48
+ tag: 'thinking'
45
49
  }
46
50
  }
47
51
  };
@@ -43,6 +43,10 @@ describe('GeminiClientAdapter', () => {
43
43
  effort: undefined,
44
44
  maxTokens: undefined,
45
45
  exclude: false
46
+ },
47
+ thinkingExtraction: {
48
+ enabled: true,
49
+ tag: 'thinking'
46
50
  }
47
51
  }
48
52
  };
@@ -31,7 +31,8 @@ class MockClientAdapter {
31
31
  try {
32
32
  // Check for special test patterns in the last user message
33
33
  const lastMessage = request.messages[request.messages.length - 1];
34
- const content = lastMessage?.content?.toLowerCase() || "";
34
+ const originalContent = lastMessage?.content || "";
35
+ const content = originalContent.toLowerCase();
35
36
  // Simulate various error conditions based on message content
36
37
  if (content.includes("error_invalid_key")) {
37
38
  return this.createErrorResponse("Invalid API key provided", types_1.ADAPTER_ERROR_CODES.INVALID_API_KEY, 401, request);
@@ -58,7 +59,7 @@ class MockClientAdapter {
58
59
  return this.createErrorResponse("Generic provider error", types_1.ADAPTER_ERROR_CODES.PROVIDER_ERROR, 500, request);
59
60
  }
60
61
  // Generate successful mock response
61
- return this.createSuccessResponse(request, content);
62
+ return this.createSuccessResponse(request, content, originalContent);
62
63
  }
63
64
  catch (error) {
64
65
  return this.createErrorResponse(`Mock adapter error: ${error instanceof Error ? error.message : "Unknown error"}`, types_1.ADAPTER_ERROR_CODES.UNKNOWN_ERROR, 500, request);
@@ -84,7 +85,7 @@ class MockClientAdapter {
84
85
  /**
85
86
  * Creates a successful mock response
86
87
  */
87
- createSuccessResponse(request, userContent) {
88
+ createSuccessResponse(request, userContent, originalContent) {
88
89
  // Generate response content based on user input and settings
89
90
  let responseContent;
90
91
  // Check for settings-based test patterns
@@ -94,6 +95,16 @@ class MockClientAdapter {
94
95
  else if (userContent.includes("test_settings")) {
95
96
  responseContent = this.generateSettingsTestResponse(request.settings);
96
97
  }
98
+ else if (userContent.includes("test_thinking:")) {
99
+ // Extract content after "test_thinking:" for testing thinking extraction
100
+ const startIndex = originalContent.indexOf("test_thinking:") + "test_thinking:".length;
101
+ responseContent = originalContent.substring(startIndex).trim();
102
+ }
103
+ else if (userContent.includes("test_reasoning:")) {
104
+ // Extract content after "test_reasoning:" and return it as both content and reasoning
105
+ const startIndex = originalContent.indexOf("test_reasoning:") + "test_reasoning:".length;
106
+ responseContent = originalContent.substring(startIndex).trim();
107
+ }
97
108
  else if (userContent.includes("hello") || userContent.includes("hi")) {
98
109
  responseContent =
99
110
  "Hello! I'm a mock LLM assistant. How can I help you today?";
@@ -147,21 +158,26 @@ class MockClientAdapter {
147
158
  else if (request.settings.stopSequences.some((seq) => responseContent.includes(seq))) {
148
159
  finishReason = "stop";
149
160
  }
161
+ // Check if we need to add reasoning to the response
162
+ const isReasoningTest = userContent.includes("test_reasoning:");
163
+ const choice = {
164
+ message: {
165
+ role: "assistant",
166
+ content: responseContent,
167
+ },
168
+ finish_reason: finishReason,
169
+ index: 0,
170
+ };
171
+ // Add reasoning field for test_reasoning pattern
172
+ if (isReasoningTest) {
173
+ choice.reasoning = "Initial model reasoning from native capabilities.";
174
+ }
150
175
  return {
151
176
  id: `mock-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
152
177
  provider: request.providerId,
153
178
  model: request.modelId,
154
179
  created: Math.floor(Date.now() / 1000),
155
- choices: [
156
- {
157
- message: {
158
- role: "assistant",
159
- content: responseContent,
160
- },
161
- finish_reason: finishReason,
162
- index: 0,
163
- },
164
- ],
180
+ choices: [choice],
165
181
  usage: {
166
182
  prompt_tokens: promptTokenCount,
167
183
  completion_tokens: mockTokenCount,
@@ -26,6 +26,10 @@ describe('MockClientAdapter', () => {
26
26
  effort: undefined,
27
27
  maxTokens: undefined,
28
28
  exclude: false
29
+ },
30
+ thinkingExtraction: {
31
+ enabled: true,
32
+ tag: 'thinking'
29
33
  }
30
34
  }
31
35
  };
@@ -44,6 +44,10 @@ describe('OpenAIClientAdapter', () => {
44
44
  effort: undefined,
45
45
  maxTokens: undefined,
46
46
  exclude: false
47
+ },
48
+ thinkingExtraction: {
49
+ enabled: true,
50
+ tag: 'thinking'
47
51
  }
48
52
  }
49
53
  };
@@ -63,6 +63,11 @@ exports.DEFAULT_LLM_SETTINGS = {
63
63
  maxTokens: undefined,
64
64
  exclude: false,
65
65
  },
66
+ thinkingExtraction: {
67
+ enabled: false, // Now requires explicit opt-in, works with onMissing: 'auto'
68
+ tag: 'thinking',
69
+ onMissing: 'auto' // Smart enforcement based on native reasoning status
70
+ },
66
71
  };
67
72
  /**
68
73
  * Per-provider default setting overrides
@@ -0,0 +1,59 @@
1
+ import type { ApiProviderId } from "../types";
2
+ import type { ILLMClientAdapter } from "../clients/types";
3
+ /**
4
+ * Information about a registered adapter
5
+ */
6
+ export interface AdapterInfo {
7
+ providerId: ApiProviderId;
8
+ hasAdapter: boolean;
9
+ adapterInfo: {
10
+ name: string;
11
+ };
12
+ }
13
+ /**
14
+ * Summary of provider availability
15
+ */
16
+ export interface ProviderSummary {
17
+ totalProviders: number;
18
+ providersWithAdapters: number;
19
+ availableProviders: string[];
20
+ unavailableProviders: string[];
21
+ }
22
+ /**
23
+ * Registry for managing LLM client adapters
24
+ */
25
+ export declare class AdapterRegistry {
26
+ private clientAdapters;
27
+ private mockClientAdapter;
28
+ constructor();
29
+ /**
30
+ * Initializes adapters for all supported providers
31
+ */
32
+ private initializeAdapters;
33
+ /**
34
+ * Registers a client adapter for a specific provider
35
+ *
36
+ * @param providerId - The provider ID
37
+ * @param adapter - The client adapter implementation
38
+ */
39
+ registerAdapter(providerId: ApiProviderId, adapter: ILLMClientAdapter): void;
40
+ /**
41
+ * Gets the appropriate client adapter for a provider
42
+ *
43
+ * @param providerId - The provider ID
44
+ * @returns The client adapter to use
45
+ */
46
+ getAdapter(providerId: ApiProviderId): ILLMClientAdapter;
47
+ /**
48
+ * Gets information about registered adapters
49
+ *
50
+ * @returns Map of provider IDs to adapter info
51
+ */
52
+ getRegisteredAdapters(): Map<ApiProviderId, AdapterInfo>;
53
+ /**
54
+ * Gets a summary of available providers and their adapter status
55
+ *
56
+ * @returns Summary of provider availability
57
+ */
58
+ getProviderSummary(): ProviderSummary;
59
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdapterRegistry = void 0;
4
+ const MockClientAdapter_1 = require("../clients/MockClientAdapter");
5
+ const config_1 = require("../config");
6
+ /**
7
+ * Registry for managing LLM client adapters
8
+ */
9
+ class AdapterRegistry {
10
+ constructor() {
11
+ this.clientAdapters = new Map();
12
+ this.mockClientAdapter = new MockClientAdapter_1.MockClientAdapter();
13
+ this.initializeAdapters();
14
+ }
15
+ /**
16
+ * Initializes adapters for all supported providers
17
+ */
18
+ initializeAdapters() {
19
+ let registeredCount = 0;
20
+ const successfullyRegisteredProviders = [];
21
+ for (const provider of config_1.SUPPORTED_PROVIDERS) {
22
+ const AdapterClass = config_1.ADAPTER_CONSTRUCTORS[provider.id];
23
+ if (AdapterClass) {
24
+ try {
25
+ const adapterConfig = config_1.ADAPTER_CONFIGS[provider.id];
26
+ const adapterInstance = new AdapterClass(adapterConfig);
27
+ this.registerAdapter(provider.id, adapterInstance);
28
+ registeredCount++;
29
+ successfullyRegisteredProviders.push(provider.id);
30
+ }
31
+ catch (error) {
32
+ console.error(`LLMService: Failed to instantiate adapter for provider '${provider.id}'. This provider will use the mock adapter. Error:`, error);
33
+ }
34
+ }
35
+ else {
36
+ console.warn(`LLMService: No adapter constructor found for supported provider '${provider.id}'. This provider will use the mock adapter as a fallback.`);
37
+ }
38
+ }
39
+ if (registeredCount > 0) {
40
+ console.log(`LLMService: Initialized with ${registeredCount} dynamically registered adapter(s) for: ${successfullyRegisteredProviders.join(", ")}.`);
41
+ }
42
+ else {
43
+ console.log(`LLMService: No real adapters were dynamically registered. All providers will use the mock adapter.`);
44
+ }
45
+ }
46
+ /**
47
+ * Registers a client adapter for a specific provider
48
+ *
49
+ * @param providerId - The provider ID
50
+ * @param adapter - The client adapter implementation
51
+ */
52
+ registerAdapter(providerId, adapter) {
53
+ this.clientAdapters.set(providerId, adapter);
54
+ console.log(`Registered client adapter for provider: ${providerId}`);
55
+ }
56
+ /**
57
+ * Gets the appropriate client adapter for a provider
58
+ *
59
+ * @param providerId - The provider ID
60
+ * @returns The client adapter to use
61
+ */
62
+ getAdapter(providerId) {
63
+ // Check for registered real adapters first
64
+ const registeredAdapter = this.clientAdapters.get(providerId);
65
+ if (registeredAdapter) {
66
+ console.log(`Using registered adapter for provider: ${providerId}`);
67
+ return registeredAdapter;
68
+ }
69
+ // Fall back to mock adapter for unsupported providers
70
+ console.log(`No real adapter found for ${providerId}, using mock adapter`);
71
+ return this.mockClientAdapter;
72
+ }
73
+ /**
74
+ * Gets information about registered adapters
75
+ *
76
+ * @returns Map of provider IDs to adapter info
77
+ */
78
+ getRegisteredAdapters() {
79
+ const adapterInfo = new Map();
80
+ for (const [providerId, adapter] of this.clientAdapters.entries()) {
81
+ adapterInfo.set(providerId, {
82
+ providerId,
83
+ hasAdapter: true,
84
+ adapterInfo: adapter.getAdapterInfo?.() || { name: "Unknown Adapter" },
85
+ });
86
+ }
87
+ return adapterInfo;
88
+ }
89
+ /**
90
+ * Gets a summary of available providers and their adapter status
91
+ *
92
+ * @returns Summary of provider availability
93
+ */
94
+ getProviderSummary() {
95
+ const availableProviders = [];
96
+ const unavailableProviders = [];
97
+ for (const provider of config_1.SUPPORTED_PROVIDERS) {
98
+ if (this.clientAdapters.has(provider.id)) {
99
+ availableProviders.push(provider.id);
100
+ }
101
+ else {
102
+ unavailableProviders.push(provider.id);
103
+ }
104
+ }
105
+ return {
106
+ totalProviders: config_1.SUPPORTED_PROVIDERS.length,
107
+ providersWithAdapters: availableProviders.length,
108
+ availableProviders,
109
+ unavailableProviders,
110
+ };
111
+ }
112
+ }
113
+ exports.AdapterRegistry = AdapterRegistry;
@@ -0,0 +1 @@
1
+ export {};