genai-lite 0.3.0 → 0.3.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 CHANGED
@@ -240,7 +240,7 @@ const response = await llmService.sendMessage({
240
240
  //
241
241
  // The response will have:
242
242
  // - response.choices[0].message.content = "The answer is 36."
243
- // - response.choices[0].reasoning = "<!-- Extracted by genai-lite from <thinking> tag -->\n15% means 15/100 = 0.15. So 15% of 240 = 0.15 × 240 = 36."
243
+ // - response.choices[0].reasoning = "15% means 15/100 = 0.15. So 15% of 240 = 0.15 × 240 = 36."
244
244
 
245
245
  // If the model doesn't include the <thinking> tag, you'll get an error (with default 'auto' mode)
246
246
  ```
@@ -155,7 +155,8 @@ class LLMService {
155
155
  // Check if native reasoning is active
156
156
  const isNativeReasoningActive = modelInfo.reasoning?.supported === true &&
157
157
  (internalRequest.settings.reasoning?.enabled === true ||
158
- modelInfo.reasoning?.enabledByDefault === true ||
158
+ (modelInfo.reasoning?.enabledByDefault === true &&
159
+ internalRequest.settings.reasoning?.enabled !== false) || // Only if not explicitly disabled
159
160
  modelInfo.reasoning?.canDisable === false); // Always-on models
160
161
  effectiveOnMissing = isNativeReasoningActive ? 'ignore' : 'error';
161
162
  }
@@ -167,11 +168,15 @@ class LLMService {
167
168
  console.log(`Extracted <${tagName}> block from response.`);
168
169
  // Handle the edge case: append to existing reasoning if present.
169
170
  const existingReasoning = choice.reasoning || '';
170
- const separator = existingReasoning ? '\n\n' : '';
171
- // Add a comment to indicate the source of this reasoning block.
172
- const newReasoning = `<!-- Extracted by genai-lite from <${tagName}> tag -->\n${extracted}`;
173
- // Update the choice object
174
- choice.reasoning = `${existingReasoning}${separator}${newReasoning}`;
171
+ // Only add a separator when appending to existing reasoning
172
+ if (existingReasoning) {
173
+ // Use a neutral markdown header that works for any consumer (human or AI)
174
+ choice.reasoning = `${existingReasoning}\n\n#### Additional Reasoning\n\n${extracted}`;
175
+ }
176
+ else {
177
+ // No existing reasoning, just use the extracted content directly
178
+ choice.reasoning = extracted;
179
+ }
175
180
  choice.message.content = remaining;
176
181
  }
177
182
  else {
@@ -376,8 +376,7 @@ describe('LLMService', () => {
376
376
  const response = await service.sendMessage(request);
377
377
  expect(response.object).toBe('chat.completion');
378
378
  const successResponse = response;
379
- expect(successResponse.choices[0].reasoning).toContain('I am thinking about this problem.');
380
- expect(successResponse.choices[0].reasoning).toContain('<!-- Extracted by genai-lite from <thinking> tag -->');
379
+ expect(successResponse.choices[0].reasoning).toBe('I am thinking about this problem.');
381
380
  expect(successResponse.choices[0].message.content).toBe('Here is the answer.');
382
381
  });
383
382
  it('should not extract thinking tag when disabled', async () => {
@@ -413,19 +412,15 @@ describe('LLMService', () => {
413
412
  const response = await service.sendMessage(request);
414
413
  expect(response.object).toBe('chat.completion');
415
414
  const successResponse = response;
416
- expect(successResponse.choices[0].reasoning).toContain('Working through the logic...');
417
- expect(successResponse.choices[0].reasoning).toContain('<!-- Extracted by genai-lite from <scratchpad> tag -->');
415
+ expect(successResponse.choices[0].reasoning).toBe('Working through the logic...');
418
416
  expect(successResponse.choices[0].message.content).toBe('Final answer is 42.');
419
417
  });
420
418
  it('should append to existing reasoning', async () => {
421
- // For this test, we first create a response with reasoning by using a reasoning-enabled model
422
- // Then test that thinking extraction appends to it
423
- // Since MockClientAdapter doesn't generate reasoning, we'll skip this complex test
424
- // and just test the simple case
419
+ // Use test_reasoning to get a response with existing reasoning, then test extraction appends to it
425
420
  const request = {
426
421
  providerId: 'mistral',
427
422
  modelId: 'codestral-2501',
428
- messages: [{ role: 'user', content: 'test_thinking:<thinking>Additional thoughts here.</thinking>The analysis is complete.' }],
423
+ messages: [{ role: 'user', content: 'test_reasoning:<thinking>Additional thoughts here.</thinking>The analysis is complete.' }],
429
424
  settings: {
430
425
  thinkingExtraction: {
431
426
  enabled: true,
@@ -436,8 +431,8 @@ describe('LLMService', () => {
436
431
  const response = await service.sendMessage(request);
437
432
  expect(response.object).toBe('chat.completion');
438
433
  const successResponse = response;
439
- expect(successResponse.choices[0].reasoning).toContain('<!-- Extracted by genai-lite from <thinking> tag -->');
440
- expect(successResponse.choices[0].reasoning).toContain('Additional thoughts here.');
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.');
441
436
  expect(successResponse.choices[0].message.content).toBe('The analysis is complete.');
442
437
  });
443
438
  it('should handle missing tag with explicit ignore', async () => {
@@ -549,6 +544,48 @@ describe('LLMService', () => {
549
544
  const errorResponse = response;
550
545
  expect(errorResponse.error.message).toContain('expected to start with a <reasoning> tag');
551
546
  });
547
+ describe('auto mode with native reasoning detection', () => {
548
+ it('should enforce thinking tags for non-reasoning models by default', async () => {
549
+ // Mistral model doesn't have reasoning support
550
+ const request = {
551
+ providerId: 'mistral',
552
+ modelId: 'codestral-2501',
553
+ messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
554
+ settings: {
555
+ thinkingExtraction: {
556
+ enabled: true,
557
+ onMissing: 'auto'
558
+ }
559
+ }
560
+ };
561
+ const response = await service.sendMessage(request);
562
+ // Should error because model doesn't have native reasoning
563
+ expect(response.object).toBe('error');
564
+ const errorResponse = response;
565
+ expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
566
+ expect(errorResponse.error.message).toContain('does not have native reasoning active');
567
+ });
568
+ it('should respect explicit reasoning.enabled: false even for models with enabledByDefault', async () => {
569
+ // This is the key test for the fix
570
+ const request = {
571
+ providerId: 'mistral',
572
+ modelId: 'codestral-2501',
573
+ messages: [{ role: 'user', content: 'test_thinking:Response without thinking tag.' }],
574
+ settings: {
575
+ reasoning: { enabled: false }, // Explicitly disabled
576
+ thinkingExtraction: {
577
+ enabled: true,
578
+ onMissing: 'auto'
579
+ }
580
+ }
581
+ };
582
+ const response = await service.sendMessage(request);
583
+ // Should error because reasoning is explicitly disabled
584
+ expect(response.object).toBe('error');
585
+ const errorResponse = response;
586
+ expect(errorResponse.error.code).toBe('MISSING_EXPECTED_TAG');
587
+ });
588
+ });
552
589
  });
553
590
  });
554
591
  });
@@ -100,6 +100,11 @@ class MockClientAdapter {
100
100
  const startIndex = originalContent.indexOf("test_thinking:") + "test_thinking:".length;
101
101
  responseContent = originalContent.substring(startIndex).trim();
102
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
+ }
103
108
  else if (userContent.includes("hello") || userContent.includes("hi")) {
104
109
  responseContent =
105
110
  "Hello! I'm a mock LLM assistant. How can I help you today?";
@@ -153,21 +158,26 @@ class MockClientAdapter {
153
158
  else if (request.settings.stopSequences.some((seq) => responseContent.includes(seq))) {
154
159
  finishReason = "stop";
155
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
+ }
156
175
  return {
157
176
  id: `mock-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
158
177
  provider: request.providerId,
159
178
  model: request.modelId,
160
179
  created: Math.floor(Date.now() / 1000),
161
- choices: [
162
- {
163
- message: {
164
- role: "assistant",
165
- content: responseContent,
166
- },
167
- finish_reason: finishReason,
168
- index: 0,
169
- },
170
- ],
180
+ choices: [choice],
171
181
  usage: {
172
182
  prompt_tokens: promptTokenCount,
173
183
  completion_tokens: mockTokenCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genai-lite",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A lightweight, portable toolkit for interacting with various Generative AI APIs.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",