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
package/README.md CHANGED
@@ -295,9 +295,11 @@ if (response.object === 'chat.completion' && response.choices[0].reasoning) {
295
295
  - Not all models support reasoning - check the [supported models](#models-with-reasoning-support) list
296
296
  - The `reasoning` field in the response contains the model's thought process (when available)
297
297
 
298
- ### Automatic Thinking Extraction
298
+ ### Thinking Extraction and Enforcement
299
299
 
300
- genai-lite can capture reasoning from any model by automatically extracting content wrapped in XML tags. When models output their thinking process in tags like `<thinking>`, the library automatically moves this content to the standardized `reasoning` field. This works with all models, providing a consistent interface for accessing model reasoning:
300
+ For models without native reasoning, you can prompt them to output reasoning in XML tags like `<thinking>`. The library then extracts these tags and moves the content to the standardized `reasoning` field, providing a consistent interface across all models.
301
+
302
+ **Key point:** The library doesn't make models think automatically—you must explicitly instruct non-reasoning models to use thinking tags in your prompt. The library then enforces that these tags are present (for non-reasoning models) or accepts native reasoning (for reasoning models).
301
303
 
302
304
  ```typescript
303
305
  // Prompt the model to think step-by-step in a <thinking> tag
@@ -312,7 +314,7 @@ const response = await llmService.sendMessage({
312
314
  content: 'Please think through this problem step by step before answering: What is 15% of 240?'
313
315
  }],
314
316
  settings: {
315
- thinkingExtraction: { enabled: true } // Must explicitly enable
317
+ thinkingTagFallback: { enabled: true } // Must explicitly enable
316
318
  }
317
319
  });
318
320
 
@@ -334,25 +336,25 @@ const response = await llmService.sendMessage({
334
336
  modelId: 'claude-3-5-haiku-20241022',
335
337
  messages: [{ role: 'user', content: 'Solve this step by step...' }],
336
338
  settings: {
337
- thinkingExtraction: {
339
+ thinkingTagFallback: {
338
340
  enabled: true, // Must explicitly enable (default: false)
339
- tag: 'scratchpad', // Custom tag name (default: 'thinking')
340
- onMissing: 'auto' // Smart enforcement (see below)
341
+ tagName: 'scratchpad', // Custom tag name (default: 'thinking')
342
+ enforce: true // Smart enforcement (see below)
341
343
  }
342
344
  }
343
345
  });
344
346
  ```
345
347
 
346
- **The `onMissing` Property:**
348
+ **The `enforce` Property:**
349
+
350
+ The `enforce` boolean controls whether thinking tags are required when native reasoning is not active:
347
351
 
348
- The `onMissing` property controls what happens when the expected thinking tag is not found:
352
+ - `enforce: true` - Error if tags missing AND native reasoning not active (smart enforcement)
353
+ - `enforce: false` (default) - Extract tags if present, never error
349
354
 
350
- - `'ignore'`: Silently continue without the tag
351
- - `'warn'`: Log a warning but continue processing
352
- - `'error'`: Return an error response with the original response preserved in `partialResponse`
353
- - `'auto'` (default): Intelligently decide based on the model's native reasoning capabilities
355
+ The enforcement is **always smart** - it automatically checks if native reasoning is active and only enforces when the model needs tags as a fallback.
354
356
 
355
- **How `'auto'` Mode Works:**
357
+ **How Smart Enforcement Works:**
356
358
 
357
359
  ```typescript
358
360
  // With non-native reasoning models (e.g., GPT-4)
@@ -367,10 +369,10 @@ const response = await llmService.sendMessage({
367
369
  content: 'What is 15% of 240?'
368
370
  }],
369
371
  settings: {
370
- thinkingExtraction: { enabled: true } // onMissing: 'auto' is default
372
+ thinkingTagFallback: { enabled: true, enforce: true }
371
373
  }
372
374
  });
373
- // Result: ERROR if <thinking> tag is missing (strict enforcement)
375
+ // Result: ERROR if <thinking> tag is missing (native reasoning not active)
374
376
  // The response is still accessible via errorResponse.partialResponse
375
377
 
376
378
  // With native reasoning models (e.g., Claude with reasoning enabled)
@@ -380,10 +382,10 @@ const response = await llmService.sendMessage({
380
382
  messages: [/* same prompt */],
381
383
  settings: {
382
384
  reasoning: { enabled: true },
383
- thinkingExtraction: { enabled: true }
385
+ thinkingTagFallback: { enabled: true, enforce: true }
384
386
  }
385
387
  });
386
- // Result: SUCCESS even if <thinking> tag is missing (lenient for native reasoning)
388
+ // Result: SUCCESS even if <thinking> tag is missing (native reasoning is active)
387
389
  ```
388
390
 
389
391
  This intelligent enforcement ensures that:
@@ -510,13 +512,10 @@ The library provides a powerful `createMessages` method that combines template r
510
512
  // Basic example: Create model-aware messages
511
513
  const { messages, modelContext } = await llmService.createMessages({
512
514
  template: `
513
- <SYSTEM>
514
- You are a {{ thinking_enabled ? "thoughtful" : "helpful" }} assistant.
515
- {{ thinking_available && !thinking_enabled ? "Note: Reasoning mode is available for complex problems." : "" }}
516
- </SYSTEM>
515
+ <SYSTEM>You are a helpful assistant.</SYSTEM>
517
516
  <USER>{{ question }}</USER>
518
517
  `,
519
- variables: {
518
+ variables: {
520
519
  question: 'What is the optimal algorithm for finding the shortest path in a weighted graph?'
521
520
  },
522
521
  presetId: 'anthropic-claude-3-7-sonnet-20250219-thinking'
@@ -560,14 +559,26 @@ The method provides:
560
559
  - **Template Rendering**: Full support for conditionals and variable substitution
561
560
  - **Role Tag Parsing**: Converts `<SYSTEM>`, `<USER>`, and `<ASSISTANT>` tags to messages
562
561
 
563
- Available model context variables:
564
- - `thinking_enabled`: Whether reasoning/thinking is enabled for this request
565
- - `thinking_available`: Whether the model supports reasoning/thinking
562
+ **Available model context variables:**
563
+
564
+ - `native_reasoning_active`: Whether native reasoning is **currently active** for this request
565
+ - `true`: The model is using built-in reasoning (e.g., Claude 4, o4-mini, Gemini 2.5 Pro with reasoning enabled)
566
+ - `false`: No native reasoning is active (either because the model doesn't support it, or it's been disabled)
567
+ - `native_reasoning_capable`: Whether the model **has the capability** to use native reasoning
568
+ - `true`: Model supports native reasoning (may or may not be enabled)
569
+ - `false`: Model does not support native reasoning
566
570
  - `model_id`: The resolved model ID
567
571
  - `provider_id`: The resolved provider ID
568
572
  - `reasoning_effort`: The reasoning effort level if specified
569
573
  - `reasoning_max_tokens`: The reasoning token budget if specified
570
574
 
575
+ **Best Practice for Templates:**
576
+ When adding thinking tag instructions to your templates, **always use `requires_tags_for_thinking`** (the NOT operator). This ensures:
577
+ - Models with active native reasoning get clean, direct prompts
578
+ - Models without native reasoning get explicit instructions to use `<thinking>` tags
579
+
580
+ Example: `{{ requires_tags_for_thinking ? ' Write your reasoning in <thinking> tags first.' : '' }}`
581
+
571
582
  #### Advanced Features
572
583
 
573
584
  **Dynamic Role Injection:**
@@ -617,7 +628,7 @@ const response = await llmService.sendMessage({
617
628
  modelId: 'gpt-4.1',
618
629
  messages,
619
630
  settings: {
620
- thinkingExtraction: { enabled: true } // Default, but shown for clarity
631
+ thinkingTagFallback: { enabled: true } // Default, but shown for clarity
621
632
  }
622
633
  });
623
634
 
@@ -640,7 +651,7 @@ const creativeWritingTemplate = `
640
651
  "settings": {
641
652
  "temperature": 0.9,
642
653
  "maxTokens": 3000,
643
- "thinkingExtraction": { "enabled": true, "tag": "reasoning" }
654
+ "thinkingTagFallback": { "enabled": true, "tagName": "reasoning" }
644
655
  }
645
656
  }
646
657
  </META>
@@ -1045,14 +1056,14 @@ const llmService = new LLMService(electronKeyProvider);
1045
1056
  genai-lite is written in TypeScript and provides comprehensive type definitions:
1046
1057
 
1047
1058
  ```typescript
1048
- import type {
1059
+ import type {
1049
1060
  LLMChatRequest,
1050
1061
  LLMChatRequestWithPreset,
1051
1062
  LLMResponse,
1052
1063
  LLMFailureResponse,
1053
1064
  LLMSettings,
1054
1065
  LLMReasoningSettings,
1055
- LLMThinkingExtractionSettings,
1066
+ LLMThinkingTagFallbackSettings,
1056
1067
  ApiKeyProvider,
1057
1068
  ModelPreset,
1058
1069
  LLMServiceOptions,
@@ -1324,24 +1335,23 @@ const { messages } = await llmService.createMessages({
1324
1335
  presetId: 'openai-gpt-4.1-default' // Optional: adds model context
1325
1336
  });
1326
1337
 
1327
- // Advanced: Leverage model context for adaptive prompts
1338
+ // Advanced: Adaptive prompts based on model capabilities
1328
1339
  const { messages, modelContext } = await llmService.createMessages({
1329
1340
  template: `
1330
1341
  <SYSTEM>
1331
- You are a {{ thinking_enabled ? 'analytical problem solver' : 'quick helper' }}.
1332
- {{ model_id.includes('claude') ? 'Use your advanced reasoning capabilities.' : '' }}
1342
+ You are a problem-solving assistant.
1343
+ {{ requires_tags_for_thinking ? ' For complex problems, write your reasoning in <thinking> tags before answering.' : '' }}
1333
1344
  </SYSTEM>
1334
- <USER>
1335
- {{ thinking_enabled ? 'Please solve this step-by-step:' : 'Please answer:' }}
1336
- {{ question }}
1337
- </USER>
1345
+ <USER>{{ question }}</USER>
1338
1346
  `,
1347
+ // Note: Use requires_tags_for_thinking (NOT operator) - only instruct models that don't have active native reasoning
1339
1348
  variables: { question: 'What causes the seasons on Earth?' },
1340
1349
  presetId: 'anthropic-claude-3-7-sonnet-20250219-thinking'
1341
1350
  });
1342
1351
 
1343
1352
  console.log('Model context:', modelContext);
1344
- // Output: { thinking_enabled: true, thinking_available: true, model_id: 'claude-3-7-sonnet-20250219', ... }
1353
+ // Output: { native_reasoning_active: true, native_reasoning_capable: true, model_id: 'claude-3-7-sonnet-20250219', ... }
1354
+ // Note: With a reasoning model, the system prompt won't include thinking tag instructions
1345
1355
  ```
1346
1356
 
1347
1357
  **Low-Level Utilities:**
@@ -75,19 +75,45 @@ export declare class LLMService {
75
75
  * injection, and role tag parsing into a single, intuitive API. It replaces the need
76
76
  * to chain prepareMessage and buildMessagesFromTemplate for model-aware multi-turn prompts.
77
77
  *
78
+ * **Model Context Injection:**
79
+ * When a presetId or providerId/modelId is provided, this method automatically injects
80
+ * model context variables into your templates:
81
+ * - `native_reasoning_active`: Whether native reasoning is currently active
82
+ * - `native_reasoning_capable`: Whether the model supports native reasoning
83
+ * - `requires_tags_for_thinking`: Whether thinking tags are needed (true when native reasoning not active)
84
+ * - `model_id`, `provider_id`, `reasoning_effort`, `reasoning_max_tokens`
85
+ *
86
+ * **Best Practice for Thinking Tags:**
87
+ * When adding thinking tag instructions, use requires_tags_for_thinking:
88
+ * `{{ requires_tags_for_thinking ? 'Write your reasoning in <thinking> tags first.' : '' }}`
89
+ *
78
90
  * @param options Options for creating messages
79
- * @returns Promise resolving to parsed messages and model context
91
+ * @returns Promise resolving to parsed messages, model context, and template settings
80
92
  *
81
93
  * @example
82
94
  * ```typescript
95
+ * // Basic usage
83
96
  * const { messages } = await llm.createMessages({
84
97
  * template: `
85
- * <SYSTEM>You are a {{ thinking_enabled ? "thoughtful" : "helpful" }} assistant.</SYSTEM>
98
+ * <SYSTEM>You are a helpful assistant.</SYSTEM>
86
99
  * <USER>Help me with {{ task }}</USER>
87
100
  * `,
88
101
  * variables: { task: 'understanding async/await' },
89
102
  * presetId: 'openai-gpt-4.1-default'
90
103
  * });
104
+ *
105
+ * // Model-aware template with thinking tags
106
+ * const { messages, modelContext } = await llm.createMessages({
107
+ * template: `
108
+ * <SYSTEM>
109
+ * You are a problem-solving assistant.
110
+ * {{ requires_tags_for_thinking ? 'For complex problems, write your reasoning in <thinking> tags first.' : '' }}
111
+ * </SYSTEM>
112
+ * <USER>{{ question }}</USER>
113
+ * `,
114
+ * variables: { question: 'Explain recursion' },
115
+ * presetId: 'anthropic-claude-3-7-sonnet-20250219-thinking'
116
+ * });
91
117
  * ```
92
118
  */
93
119
  createMessages(options: {
@@ -96,6 +122,7 @@ export declare class LLMService {
96
122
  presetId?: string;
97
123
  providerId?: string;
98
124
  modelId?: string;
125
+ settings?: Partial<LLMSettings>;
99
126
  }): Promise<CreateMessagesResult>;
100
127
  /**
101
128
  * Gets information about registered adapters
@@ -143,32 +143,42 @@ class LLMService {
143
143
  object: "error",
144
144
  };
145
145
  }
146
+ // Validate API key format if adapter supports it
147
+ if (clientAdapter.validateApiKey && !clientAdapter.validateApiKey(apiKey)) {
148
+ return {
149
+ provider: providerId,
150
+ model: modelId,
151
+ error: {
152
+ message: `Invalid API key format for provider '${providerId}'. Please check your API key.`,
153
+ code: "INVALID_API_KEY",
154
+ type: "authentication_error",
155
+ },
156
+ object: "error",
157
+ };
158
+ }
146
159
  console.log(`Making LLM request with ${clientAdapter.constructor.name} for provider: ${providerId}`);
147
160
  const result = await clientAdapter.sendMessage(internalRequest, apiKey);
148
- // Post-process for thinking extraction
149
- if (result.object === 'chat.completion' && internalRequest.settings.thinkingExtraction?.enabled) {
150
- const settings = internalRequest.settings.thinkingExtraction;
151
- const tagName = settings.tag || 'thinking';
152
- // Step 1: Resolve the effective onMissing strategy
153
- let effectiveOnMissing = settings.onMissing || 'auto';
154
- if (effectiveOnMissing === 'auto') {
155
- // Check if native reasoning is active
156
- const isNativeReasoningActive = modelInfo.reasoning?.supported === true &&
157
- (internalRequest.settings.reasoning?.enabled === true ||
158
- (modelInfo.reasoning?.enabledByDefault === true &&
159
- internalRequest.settings.reasoning?.enabled !== false) || // Only if not explicitly disabled
160
- modelInfo.reasoning?.canDisable === false); // Always-on models
161
- effectiveOnMissing = isNativeReasoningActive ? 'ignore' : 'error';
162
- }
163
- // Step 2: Process the response
161
+ // Post-process for thinking tag fallback
162
+ // This feature extracts reasoning from XML tags when native reasoning is not active.
163
+ // It's a fallback mechanism for models without native reasoning or when native is disabled.
164
+ const fallbackSettings = internalRequest.settings.thinkingTagFallback;
165
+ if (result.object === 'chat.completion' && fallbackSettings && fallbackSettings.enabled !== false) {
166
+ const tagName = fallbackSettings.tagName || 'thinking';
167
+ // Check if native reasoning is active for this request
168
+ const isNativeReasoningActive = modelInfo.reasoning?.supported === true &&
169
+ (internalRequest.settings.reasoning?.enabled === true ||
170
+ (modelInfo.reasoning?.enabledByDefault === true &&
171
+ internalRequest.settings.reasoning?.enabled !== false) ||
172
+ modelInfo.reasoning?.canDisable === false);
173
+ // Process the response - extract thinking tags if present
164
174
  const choice = result.choices[0];
165
175
  if (choice?.message?.content) {
166
176
  const { extracted, remaining } = (0, parser_1.extractInitialTaggedContent)(choice.message.content, tagName);
167
177
  if (extracted !== null) {
178
+ // Success: thinking tag found
168
179
  console.log(`Extracted <${tagName}> block from response.`);
169
- // Handle the edge case: append to existing reasoning if present.
180
+ // Handle the edge case: append to existing reasoning if present (e.g., native reasoning + thinking tags)
170
181
  const existingReasoning = choice.reasoning || '';
171
- // Only add a separator when appending to existing reasoning
172
182
  if (existingReasoning) {
173
183
  // Use a neutral markdown header that works for any consumer (human or AI)
174
184
  choice.reasoning = `${existingReasoning}\n\n#### Additional Reasoning\n\n${extracted}`;
@@ -180,17 +190,24 @@ class LLMService {
180
190
  choice.message.content = remaining;
181
191
  }
182
192
  else {
183
- // Tag was not found, enforce the effective strategy
184
- if (effectiveOnMissing === 'error') {
193
+ // Tag was not found
194
+ // Enforce only if: (1) enforce: true AND (2) native reasoning is NOT active
195
+ if (fallbackSettings.enforce === true && !isNativeReasoningActive) {
196
+ const nativeReasoningCapable = modelInfo.reasoning?.supported === true;
185
197
  return {
186
198
  provider: providerId,
187
199
  model: modelId,
188
200
  error: {
189
- message: `The model (${modelId}) response was expected to start with a <${tagName}> tag but it was not found. ` +
190
- `This is enforced because the model does not have native reasoning active. ` +
191
- `Either ensure your prompt instructs the model to use <${tagName}> tags, or enable native reasoning if supported.`,
192
- code: "MISSING_EXPECTED_TAG",
201
+ message: `Model response missing required <${tagName}> tags.`,
202
+ code: "THINKING_TAGS_MISSING",
193
203
  type: "validation_error",
204
+ param: nativeReasoningCapable && !isNativeReasoningActive
205
+ ? `You disabled native reasoning for this model (${modelId}). ` +
206
+ `To see its reasoning, you must prompt it to use <${tagName}> tags. ` +
207
+ `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`
208
+ : `This model (${modelId}) does not support native reasoning. ` +
209
+ `To get reasoning, you must prompt it to use <${tagName}> tags. ` +
210
+ `Example: "Write your step-by-step reasoning in <${tagName}> tags before answering."`,
194
211
  },
195
212
  object: "error",
196
213
  partialResponse: {
@@ -203,10 +220,7 @@ class LLMService {
203
220
  }
204
221
  };
205
222
  }
206
- else if (effectiveOnMissing === 'warn') {
207
- console.warn(`Expected <${tagName}> tag was not found in the response from model ${modelId}.`);
208
- }
209
- // If 'ignore', do nothing
223
+ // If enforce: false or native reasoning is active, do nothing
210
224
  }
211
225
  }
212
226
  }
@@ -262,19 +276,45 @@ class LLMService {
262
276
  * injection, and role tag parsing into a single, intuitive API. It replaces the need
263
277
  * to chain prepareMessage and buildMessagesFromTemplate for model-aware multi-turn prompts.
264
278
  *
279
+ * **Model Context Injection:**
280
+ * When a presetId or providerId/modelId is provided, this method automatically injects
281
+ * model context variables into your templates:
282
+ * - `native_reasoning_active`: Whether native reasoning is currently active
283
+ * - `native_reasoning_capable`: Whether the model supports native reasoning
284
+ * - `requires_tags_for_thinking`: Whether thinking tags are needed (true when native reasoning not active)
285
+ * - `model_id`, `provider_id`, `reasoning_effort`, `reasoning_max_tokens`
286
+ *
287
+ * **Best Practice for Thinking Tags:**
288
+ * When adding thinking tag instructions, use requires_tags_for_thinking:
289
+ * `{{ requires_tags_for_thinking ? 'Write your reasoning in <thinking> tags first.' : '' }}`
290
+ *
265
291
  * @param options Options for creating messages
266
- * @returns Promise resolving to parsed messages and model context
292
+ * @returns Promise resolving to parsed messages, model context, and template settings
267
293
  *
268
294
  * @example
269
295
  * ```typescript
296
+ * // Basic usage
270
297
  * const { messages } = await llm.createMessages({
271
298
  * template: `
272
- * <SYSTEM>You are a {{ thinking_enabled ? "thoughtful" : "helpful" }} assistant.</SYSTEM>
299
+ * <SYSTEM>You are a helpful assistant.</SYSTEM>
273
300
  * <USER>Help me with {{ task }}</USER>
274
301
  * `,
275
302
  * variables: { task: 'understanding async/await' },
276
303
  * presetId: 'openai-gpt-4.1-default'
277
304
  * });
305
+ *
306
+ * // Model-aware template with thinking tags
307
+ * const { messages, modelContext } = await llm.createMessages({
308
+ * template: `
309
+ * <SYSTEM>
310
+ * You are a problem-solving assistant.
311
+ * {{ requires_tags_for_thinking ? 'For complex problems, write your reasoning in <thinking> tags first.' : '' }}
312
+ * </SYSTEM>
313
+ * <USER>{{ question }}</USER>
314
+ * `,
315
+ * variables: { question: 'Explain recursion' },
316
+ * presetId: 'anthropic-claude-3-7-sonnet-20250219-thinking'
317
+ * });
278
318
  * ```
279
319
  */
280
320
  async createMessages(options) {
@@ -290,7 +330,8 @@ class LLMService {
290
330
  const resolved = this.modelResolver.resolve({
291
331
  presetId: options.presetId,
292
332
  providerId: options.providerId,
293
- modelId: options.modelId
333
+ modelId: options.modelId,
334
+ settings: options.settings
294
335
  });
295
336
  if (resolved.error) {
296
337
  // If resolution fails, proceed without model context
@@ -300,12 +341,15 @@ class LLMService {
300
341
  const { providerId, modelId, modelInfo, settings } = resolved;
301
342
  // Merge settings with model defaults
302
343
  const mergedSettings = this.settingsManager.mergeSettingsForModel(modelId, providerId, settings || {});
303
- // Create model context
344
+ // Calculate native reasoning status
345
+ const nativeReasoningActive = !!(modelInfo.reasoning?.supported &&
346
+ (mergedSettings.reasoning?.enabled === true ||
347
+ (modelInfo.reasoning?.enabledByDefault && mergedSettings.reasoning?.enabled !== false)));
348
+ // Create model context with new property names
304
349
  modelContext = {
305
- thinking_enabled: !!(modelInfo.reasoning?.supported &&
306
- (mergedSettings.reasoning?.enabled === true ||
307
- (modelInfo.reasoning?.enabledByDefault && mergedSettings.reasoning?.enabled !== false))),
308
- thinking_available: !!modelInfo.reasoning?.supported,
350
+ native_reasoning_active: nativeReasoningActive,
351
+ native_reasoning_capable: !!modelInfo.reasoning?.supported,
352
+ requires_tags_for_thinking: !nativeReasoningActive,
309
353
  model_id: modelId,
310
354
  provider_id: providerId,
311
355
  reasoning_effort: mergedSettings.reasoning?.effort,
@@ -69,10 +69,10 @@ exports.DEFAULT_LLM_SETTINGS = {
69
69
  maxTokens: undefined,
70
70
  exclude: false,
71
71
  },
72
- thinkingExtraction: {
73
- enabled: false, // Now requires explicit opt-in, works with onMissing: 'auto'
74
- tag: 'thinking',
75
- onMissing: 'auto' // Smart enforcement based on native reasoning status
72
+ thinkingTagFallback: {
73
+ enabled: false,
74
+ tagName: 'thinking',
75
+ enforce: false
76
76
  },
77
77
  };
78
78
  /**
@@ -34,9 +34,9 @@ class SettingsManager {
34
34
  ...modelDefaults.reasoning,
35
35
  ...requestSettings?.reasoning,
36
36
  },
37
- thinkingExtraction: {
38
- ...modelDefaults.thinkingExtraction,
39
- ...requestSettings?.thinkingExtraction,
37
+ thinkingTagFallback: {
38
+ ...modelDefaults.thinkingTagFallback,
39
+ ...requestSettings?.thinkingTagFallback,
40
40
  },
41
41
  };
42
42
  // Log the final settings for debugging
@@ -115,7 +115,7 @@ class SettingsManager {
115
115
  'supportsSystemMessage',
116
116
  'geminiSafetySettings',
117
117
  'reasoning',
118
- 'thinkingExtraction'
118
+ 'thinkingTagFallback'
119
119
  ];
120
120
  // Check each setting field
121
121
  for (const [key, value] of Object.entries(settings)) {
@@ -195,22 +195,28 @@ class SettingsManager {
195
195
  }
196
196
  continue;
197
197
  }
198
- if (key === 'thinkingExtraction' && typeof value === 'object' && value !== null) {
198
+ if (key === 'thinkingTagFallback' && typeof value === 'object' && value !== null) {
199
199
  const thinkingValidated = {};
200
200
  if ('enabled' in value && typeof value.enabled !== 'boolean') {
201
- console.warn(`Invalid thinkingExtraction.enabled value in template. Must be a boolean.`);
201
+ console.warn(`Invalid thinkingTagFallback.enabled value in template. Must be a boolean.`);
202
202
  }
203
203
  else if ('enabled' in value) {
204
204
  thinkingValidated.enabled = value.enabled;
205
205
  }
206
- if ('tag' in value && typeof value.tag !== 'string') {
207
- console.warn(`Invalid thinkingExtraction.tag value in template. Must be a string.`);
206
+ if ('tagName' in value && typeof value.tagName !== 'string') {
207
+ console.warn(`Invalid thinkingTagFallback.tagName value in template. Must be a string.`);
208
208
  }
209
- else if ('tag' in value) {
210
- thinkingValidated.tag = value.tag;
209
+ else if ('tagName' in value) {
210
+ thinkingValidated.tagName = value.tagName;
211
+ }
212
+ if ('enforce' in value && typeof value.enforce !== 'boolean') {
213
+ console.warn(`Invalid thinkingTagFallback.enforce value in template. Must be a boolean.`);
214
+ }
215
+ else if ('enforce' in value) {
216
+ thinkingValidated.enforce = value.enforce;
211
217
  }
212
218
  if (Object.keys(thinkingValidated).length > 0) {
213
- validated.thinkingExtraction = thinkingValidated;
219
+ validated.thinkingTagFallback = thinkingValidated;
214
220
  }
215
221
  continue;
216
222
  }