genai-lite 0.2.0 → 0.3.0

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 +508 -30
  2. package/dist/config/presets.json +121 -17
  3. package/dist/index.d.ts +3 -3
  4. package/dist/index.js +4 -3
  5. package/dist/llm/LLMService.createMessages.test.d.ts +4 -0
  6. package/dist/llm/LLMService.createMessages.test.js +364 -0
  7. package/dist/llm/LLMService.d.ts +49 -47
  8. package/dist/llm/LLMService.js +208 -303
  9. package/dist/llm/LLMService.original.d.ts +147 -0
  10. package/dist/llm/LLMService.original.js +656 -0
  11. package/dist/llm/LLMService.prepareMessage.test.d.ts +1 -0
  12. package/dist/llm/LLMService.prepareMessage.test.js +303 -0
  13. package/dist/llm/LLMService.sendMessage.preset.test.d.ts +1 -0
  14. package/dist/llm/LLMService.sendMessage.preset.test.js +153 -0
  15. package/dist/llm/LLMService.test.js +275 -0
  16. package/dist/llm/clients/AnthropicClientAdapter.js +64 -10
  17. package/dist/llm/clients/AnthropicClientAdapter.test.js +11 -1
  18. package/dist/llm/clients/GeminiClientAdapter.js +70 -11
  19. package/dist/llm/clients/GeminiClientAdapter.test.js +125 -1
  20. package/dist/llm/clients/MockClientAdapter.js +9 -3
  21. package/dist/llm/clients/MockClientAdapter.test.js +11 -1
  22. package/dist/llm/clients/OpenAIClientAdapter.js +26 -10
  23. package/dist/llm/clients/OpenAIClientAdapter.test.js +11 -1
  24. package/dist/llm/config.js +117 -2
  25. package/dist/llm/config.test.js +17 -0
  26. package/dist/llm/services/AdapterRegistry.d.ts +59 -0
  27. package/dist/llm/services/AdapterRegistry.js +113 -0
  28. package/dist/llm/services/AdapterRegistry.test.d.ts +1 -0
  29. package/dist/llm/services/AdapterRegistry.test.js +239 -0
  30. package/dist/llm/services/ModelResolver.d.ts +35 -0
  31. package/dist/llm/services/ModelResolver.js +116 -0
  32. package/dist/llm/services/ModelResolver.test.d.ts +1 -0
  33. package/dist/llm/services/ModelResolver.test.js +158 -0
  34. package/dist/llm/services/PresetManager.d.ts +27 -0
  35. package/dist/llm/services/PresetManager.js +50 -0
  36. package/dist/llm/services/PresetManager.test.d.ts +1 -0
  37. package/dist/llm/services/PresetManager.test.js +210 -0
  38. package/dist/llm/services/RequestValidator.d.ts +31 -0
  39. package/dist/llm/services/RequestValidator.js +122 -0
  40. package/dist/llm/services/RequestValidator.test.d.ts +1 -0
  41. package/dist/llm/services/RequestValidator.test.js +159 -0
  42. package/dist/llm/services/SettingsManager.d.ts +32 -0
  43. package/dist/llm/services/SettingsManager.js +223 -0
  44. package/dist/llm/services/SettingsManager.test.d.ts +1 -0
  45. package/dist/llm/services/SettingsManager.test.js +266 -0
  46. package/dist/llm/types.d.ts +107 -0
  47. package/dist/prompting/builder.d.ts +4 -0
  48. package/dist/prompting/builder.js +12 -61
  49. package/dist/prompting/content.js +3 -9
  50. package/dist/prompting/index.d.ts +2 -3
  51. package/dist/prompting/index.js +4 -5
  52. package/dist/prompting/parser.d.ts +80 -0
  53. package/dist/prompting/parser.js +133 -0
  54. package/dist/prompting/parser.test.js +348 -0
  55. package/dist/prompting/template.d.ts +8 -0
  56. package/dist/prompting/template.js +89 -6
  57. package/dist/prompting/template.test.js +116 -0
  58. package/package.json +3 -2
  59. package/src/config/presets.json +122 -17
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SettingsManager = void 0;
4
+ const config_1 = require("../config");
5
+ /**
6
+ * Manages LLM settings including merging with defaults and filtering unsupported parameters
7
+ */
8
+ class SettingsManager {
9
+ /**
10
+ * Merges request settings with model-specific and global defaults
11
+ *
12
+ * @param modelId - The model ID to get defaults for
13
+ * @param providerId - The provider ID to get defaults for
14
+ * @param requestSettings - Settings from the request
15
+ * @returns Complete settings object with all required fields
16
+ */
17
+ mergeSettingsForModel(modelId, providerId, requestSettings) {
18
+ // Get model-specific defaults
19
+ const modelDefaults = (0, config_1.getDefaultSettingsForModel)(modelId, providerId);
20
+ // Merge with user-provided settings (user settings take precedence)
21
+ const mergedSettings = {
22
+ temperature: requestSettings?.temperature ?? modelDefaults.temperature,
23
+ maxTokens: requestSettings?.maxTokens ?? modelDefaults.maxTokens,
24
+ topP: requestSettings?.topP ?? modelDefaults.topP,
25
+ stopSequences: requestSettings?.stopSequences ?? modelDefaults.stopSequences,
26
+ frequencyPenalty: requestSettings?.frequencyPenalty ?? modelDefaults.frequencyPenalty,
27
+ presencePenalty: requestSettings?.presencePenalty ?? modelDefaults.presencePenalty,
28
+ user: requestSettings?.user ?? modelDefaults.user,
29
+ supportsSystemMessage: requestSettings?.supportsSystemMessage ??
30
+ modelDefaults.supportsSystemMessage,
31
+ geminiSafetySettings: requestSettings?.geminiSafetySettings ??
32
+ modelDefaults.geminiSafetySettings,
33
+ reasoning: {
34
+ ...modelDefaults.reasoning,
35
+ ...requestSettings?.reasoning,
36
+ },
37
+ thinkingExtraction: {
38
+ ...modelDefaults.thinkingExtraction,
39
+ ...requestSettings?.thinkingExtraction,
40
+ },
41
+ };
42
+ // Log the final settings for debugging
43
+ console.log(`Merged settings for ${providerId}/${modelId}:`, {
44
+ temperature: mergedSettings.temperature,
45
+ maxTokens: mergedSettings.maxTokens,
46
+ topP: mergedSettings.topP,
47
+ hasStopSequences: mergedSettings.stopSequences.length > 0,
48
+ frequencyPenalty: mergedSettings.frequencyPenalty,
49
+ presencePenalty: mergedSettings.presencePenalty,
50
+ hasUser: !!mergedSettings.user,
51
+ geminiSafetySettingsCount: mergedSettings.geminiSafetySettings.length,
52
+ reasoning: mergedSettings.reasoning,
53
+ });
54
+ return mergedSettings;
55
+ }
56
+ /**
57
+ * Filters out unsupported parameters based on model and provider configuration
58
+ *
59
+ * @param settings - The settings to filter
60
+ * @param modelInfo - Model information including unsupported parameters
61
+ * @param providerInfo - Provider information including unsupported parameters
62
+ * @returns Filtered settings object
63
+ */
64
+ filterUnsupportedParameters(settings, modelInfo, providerInfo) {
65
+ // Create a mutable copy
66
+ const filteredSettings = { ...settings };
67
+ const paramsToExclude = new Set();
68
+ // Add provider-level exclusions
69
+ if (providerInfo.unsupportedParameters) {
70
+ providerInfo.unsupportedParameters.forEach((param) => paramsToExclude.add(param));
71
+ }
72
+ // Add model-level exclusions (these will be added to any provider-level ones)
73
+ if (modelInfo.unsupportedParameters) {
74
+ modelInfo.unsupportedParameters.forEach((param) => paramsToExclude.add(param));
75
+ }
76
+ if (paramsToExclude.size > 0) {
77
+ console.log(`LLMService: Potential parameters to exclude for provider '${providerInfo.id}', model '${modelInfo.id}':`, Array.from(paramsToExclude));
78
+ }
79
+ paramsToExclude.forEach((param) => {
80
+ // Check if the parameter key actually exists in filteredSettings before trying to delete
81
+ if (param in filteredSettings) {
82
+ console.log(`LLMService: Removing excluded parameter '${String(param)}' for provider '${providerInfo.id}', model '${modelInfo.id}'. Value was:`, filteredSettings[param]);
83
+ delete filteredSettings[param]; // Cast to allow deletion
84
+ }
85
+ else {
86
+ // This case should ideally not happen if settings truly is Required<LLMSettings>
87
+ console.log(`LLMService: Parameter '${String(param)}' marked for exclusion was not found in settings for provider '${providerInfo.id}', model '${modelInfo.id}'.`);
88
+ }
89
+ });
90
+ // Handle reasoning settings for models that don't support it
91
+ // This happens after validateReasoningSettings so we know it's safe to strip
92
+ if (!modelInfo.reasoning?.supported && filteredSettings.reasoning) {
93
+ console.log(`LLMService: Removing reasoning settings for non-reasoning model ${modelInfo.id}`);
94
+ delete filteredSettings.reasoning;
95
+ }
96
+ return filteredSettings;
97
+ }
98
+ /**
99
+ * Validates settings extracted from templates, warning about invalid fields
100
+ * and returning only valid settings
101
+ *
102
+ * @param settings - The settings to validate (e.g., from template metadata)
103
+ * @returns Validated settings with invalid fields removed
104
+ */
105
+ validateTemplateSettings(settings) {
106
+ const validated = {};
107
+ const knownFields = [
108
+ 'temperature',
109
+ 'maxTokens',
110
+ 'topP',
111
+ 'stopSequences',
112
+ 'frequencyPenalty',
113
+ 'presencePenalty',
114
+ 'user',
115
+ 'supportsSystemMessage',
116
+ 'geminiSafetySettings',
117
+ 'reasoning',
118
+ 'thinkingExtraction'
119
+ ];
120
+ // Check each setting field
121
+ for (const [key, value] of Object.entries(settings)) {
122
+ // Check if it's a known field
123
+ if (!knownFields.includes(key)) {
124
+ console.warn(`Unknown setting "${key}" in template metadata. Ignoring.`);
125
+ continue;
126
+ }
127
+ // Type-specific validation
128
+ if (key === 'temperature') {
129
+ if (typeof value !== 'number' || value < 0 || value > 2) {
130
+ console.warn(`Invalid temperature value in template: ${value}. Must be a number between 0 and 2.`);
131
+ continue;
132
+ }
133
+ }
134
+ if (key === 'maxTokens') {
135
+ if (typeof value !== 'number' || value <= 0) {
136
+ console.warn(`Invalid maxTokens value in template: ${value}. Must be a positive number.`);
137
+ continue;
138
+ }
139
+ }
140
+ if (key === 'topP') {
141
+ if (typeof value !== 'number' || value < 0 || value > 1) {
142
+ console.warn(`Invalid topP value in template: ${value}. Must be a number between 0 and 1.`);
143
+ continue;
144
+ }
145
+ }
146
+ if (key === 'stopSequences') {
147
+ if (!Array.isArray(value) || !value.every(v => typeof v === 'string')) {
148
+ console.warn(`Invalid stopSequences value in template. Must be an array of strings.`);
149
+ continue;
150
+ }
151
+ }
152
+ if ((key === 'frequencyPenalty' || key === 'presencePenalty')) {
153
+ if (typeof value !== 'number' || value < -2 || value > 2) {
154
+ console.warn(`Invalid ${key} value in template: ${value}. Must be a number between -2 and 2.`);
155
+ continue;
156
+ }
157
+ }
158
+ if (key === 'user' && typeof value !== 'string') {
159
+ console.warn(`Invalid user value in template. Must be a string.`);
160
+ continue;
161
+ }
162
+ if (key === 'supportsSystemMessage' && typeof value !== 'boolean') {
163
+ console.warn(`Invalid supportsSystemMessage value in template. Must be a boolean.`);
164
+ continue;
165
+ }
166
+ // Nested object validation
167
+ if (key === 'reasoning' && typeof value === 'object' && value !== null) {
168
+ const reasoningValidated = {};
169
+ if ('enabled' in value && typeof value.enabled !== 'boolean') {
170
+ console.warn(`Invalid reasoning.enabled value in template. Must be a boolean.`);
171
+ }
172
+ else if ('enabled' in value) {
173
+ reasoningValidated.enabled = value.enabled;
174
+ }
175
+ if ('effort' in value && !['low', 'medium', 'high'].includes(value.effort)) {
176
+ console.warn(`Invalid reasoning.effort value in template: ${value.effort}. Must be 'low', 'medium', or 'high'.`);
177
+ }
178
+ else if ('effort' in value) {
179
+ reasoningValidated.effort = value.effort;
180
+ }
181
+ if ('maxTokens' in value && (typeof value.maxTokens !== 'number' || value.maxTokens <= 0)) {
182
+ console.warn(`Invalid reasoning.maxTokens value in template. Must be a positive number.`);
183
+ }
184
+ else if ('maxTokens' in value) {
185
+ reasoningValidated.maxTokens = value.maxTokens;
186
+ }
187
+ if ('exclude' in value && typeof value.exclude !== 'boolean') {
188
+ console.warn(`Invalid reasoning.exclude value in template. Must be a boolean.`);
189
+ }
190
+ else if ('exclude' in value) {
191
+ reasoningValidated.exclude = value.exclude;
192
+ }
193
+ if (Object.keys(reasoningValidated).length > 0) {
194
+ validated.reasoning = reasoningValidated;
195
+ }
196
+ continue;
197
+ }
198
+ if (key === 'thinkingExtraction' && typeof value === 'object' && value !== null) {
199
+ const thinkingValidated = {};
200
+ if ('enabled' in value && typeof value.enabled !== 'boolean') {
201
+ console.warn(`Invalid thinkingExtraction.enabled value in template. Must be a boolean.`);
202
+ }
203
+ else if ('enabled' in value) {
204
+ thinkingValidated.enabled = value.enabled;
205
+ }
206
+ if ('tag' in value && typeof value.tag !== 'string') {
207
+ console.warn(`Invalid thinkingExtraction.tag value in template. Must be a string.`);
208
+ }
209
+ else if ('tag' in value) {
210
+ thinkingValidated.tag = value.tag;
211
+ }
212
+ if (Object.keys(thinkingValidated).length > 0) {
213
+ validated.thinkingExtraction = thinkingValidated;
214
+ }
215
+ continue;
216
+ }
217
+ // If we made it here, the field is valid
218
+ validated[key] = value;
219
+ }
220
+ return validated;
221
+ }
222
+ }
223
+ exports.SettingsManager = SettingsManager;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SettingsManager_1 = require("./SettingsManager");
4
+ const config_1 = require("../config");
5
+ jest.mock('../config', () => ({
6
+ getDefaultSettingsForModel: jest.fn().mockReturnValue({
7
+ temperature: 0.7,
8
+ maxTokens: 1000,
9
+ topP: 0.9,
10
+ stopSequences: [],
11
+ frequencyPenalty: 0,
12
+ presencePenalty: 0,
13
+ user: '',
14
+ supportsSystemMessage: true,
15
+ geminiSafetySettings: [],
16
+ reasoning: {
17
+ enabled: false,
18
+ effort: undefined,
19
+ maxTokens: undefined,
20
+ exclude: false,
21
+ },
22
+ thinkingExtraction: {
23
+ enabled: true,
24
+ tag: 'thinking',
25
+ },
26
+ }),
27
+ }));
28
+ describe('SettingsManager', () => {
29
+ let settingsManager;
30
+ beforeEach(() => {
31
+ settingsManager = new SettingsManager_1.SettingsManager();
32
+ jest.clearAllMocks();
33
+ });
34
+ describe('mergeSettingsForModel', () => {
35
+ it('should return default settings when no request settings provided', () => {
36
+ const result = settingsManager.mergeSettingsForModel('gpt-4.1', 'openai');
37
+ expect(config_1.getDefaultSettingsForModel).toHaveBeenCalledWith('gpt-4.1', 'openai');
38
+ expect(result).toEqual({
39
+ temperature: 0.7,
40
+ maxTokens: 1000,
41
+ topP: 0.9,
42
+ stopSequences: [],
43
+ frequencyPenalty: 0,
44
+ presencePenalty: 0,
45
+ user: '',
46
+ supportsSystemMessage: true,
47
+ geminiSafetySettings: [],
48
+ reasoning: {
49
+ enabled: false,
50
+ effort: undefined,
51
+ maxTokens: undefined,
52
+ exclude: false,
53
+ },
54
+ thinkingExtraction: {
55
+ enabled: true,
56
+ tag: 'thinking',
57
+ },
58
+ });
59
+ });
60
+ it('should merge user settings with defaults', () => {
61
+ const userSettings = {
62
+ temperature: 0.9,
63
+ maxTokens: 500,
64
+ };
65
+ const result = settingsManager.mergeSettingsForModel('gpt-4.1', 'openai', userSettings);
66
+ expect(result.temperature).toBe(0.9);
67
+ expect(result.maxTokens).toBe(500);
68
+ // Other settings should remain as defaults
69
+ expect(result.topP).toBe(0.9);
70
+ expect(result.frequencyPenalty).toBe(0);
71
+ });
72
+ it('should handle reasoning settings override', () => {
73
+ const userSettings = {
74
+ reasoning: {
75
+ enabled: true,
76
+ effort: 'high',
77
+ },
78
+ };
79
+ const result = settingsManager.mergeSettingsForModel('claude-3-7-sonnet-20250219', 'anthropic', userSettings);
80
+ expect(result.reasoning).toEqual({
81
+ enabled: true,
82
+ effort: 'high',
83
+ maxTokens: undefined,
84
+ exclude: false,
85
+ });
86
+ });
87
+ it('should handle complex settings including Gemini safety settings', () => {
88
+ const userSettings = {
89
+ temperature: 0.5,
90
+ geminiSafetySettings: [
91
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' },
92
+ ],
93
+ };
94
+ const result = settingsManager.mergeSettingsForModel('gemini-2.0-flash', 'gemini', userSettings);
95
+ expect(result.temperature).toBe(0.5);
96
+ expect(result.geminiSafetySettings).toHaveLength(1);
97
+ expect(result.geminiSafetySettings[0]).toEqual({
98
+ category: 'HARM_CATEGORY_HATE_SPEECH',
99
+ threshold: 'BLOCK_NONE',
100
+ });
101
+ });
102
+ it('should handle all optional fields being provided', () => {
103
+ const userSettings = {
104
+ temperature: 0.8,
105
+ maxTokens: 2000,
106
+ topP: 0.95,
107
+ stopSequences: ['END', 'STOP'],
108
+ frequencyPenalty: 0.5,
109
+ presencePenalty: -0.5,
110
+ user: 'test-user',
111
+ supportsSystemMessage: false,
112
+ geminiSafetySettings: [],
113
+ reasoning: {
114
+ enabled: true,
115
+ effort: 'medium',
116
+ maxTokens: 5000,
117
+ exclude: true,
118
+ },
119
+ thinkingExtraction: {
120
+ enabled: false,
121
+ tag: 'scratchpad',
122
+ },
123
+ };
124
+ const result = settingsManager.mergeSettingsForModel('gpt-4.1', 'openai', userSettings);
125
+ expect(result).toEqual(userSettings);
126
+ });
127
+ });
128
+ describe('filterUnsupportedParameters', () => {
129
+ const baseSettings = {
130
+ temperature: 0.7,
131
+ maxTokens: 1000,
132
+ topP: 0.9,
133
+ stopSequences: [],
134
+ frequencyPenalty: 0,
135
+ presencePenalty: 0,
136
+ user: '',
137
+ supportsSystemMessage: true,
138
+ geminiSafetySettings: [],
139
+ reasoning: {
140
+ enabled: false,
141
+ effort: undefined,
142
+ maxTokens: undefined,
143
+ exclude: false,
144
+ },
145
+ thinkingExtraction: {
146
+ enabled: true,
147
+ tag: 'thinking',
148
+ },
149
+ };
150
+ const mockModelInfo = {
151
+ id: 'test-model',
152
+ providerId: 'test-provider',
153
+ name: 'Test Model',
154
+ supportsPromptCache: false,
155
+ contextWindow: 4096,
156
+ maxTokens: 1000,
157
+ reasoning: { supported: true }, // Add reasoning support to prevent it being stripped
158
+ };
159
+ const mockProviderInfo = {
160
+ id: 'test-provider',
161
+ name: 'Test Provider',
162
+ };
163
+ it('should return settings unchanged when no parameters need filtering', () => {
164
+ const result = settingsManager.filterUnsupportedParameters(baseSettings, mockModelInfo, mockProviderInfo);
165
+ expect(result).toEqual(baseSettings);
166
+ });
167
+ it('should filter out provider-level unsupported parameters', () => {
168
+ const providerWithExclusions = {
169
+ ...mockProviderInfo,
170
+ unsupportedParameters: ['frequencyPenalty', 'presencePenalty'],
171
+ };
172
+ const settingsWithPenalties = {
173
+ ...baseSettings,
174
+ frequencyPenalty: 0.5,
175
+ presencePenalty: -0.5,
176
+ };
177
+ const result = settingsManager.filterUnsupportedParameters(settingsWithPenalties, mockModelInfo, providerWithExclusions);
178
+ expect(result.frequencyPenalty).toBeUndefined();
179
+ expect(result.presencePenalty).toBeUndefined();
180
+ expect(result.temperature).toBe(0.7); // Should remain unchanged
181
+ });
182
+ it('should filter out model-level unsupported parameters', () => {
183
+ const modelWithExclusions = {
184
+ ...mockModelInfo,
185
+ unsupportedParameters: ['stopSequences', 'user'],
186
+ };
187
+ const settingsWithExcluded = {
188
+ ...baseSettings,
189
+ stopSequences: ['END'],
190
+ user: 'test-user',
191
+ };
192
+ const result = settingsManager.filterUnsupportedParameters(settingsWithExcluded, modelWithExclusions, mockProviderInfo);
193
+ expect(result.stopSequences).toBeUndefined();
194
+ expect(result.user).toBeUndefined();
195
+ });
196
+ it('should combine provider and model exclusions', () => {
197
+ const providerWithExclusions = {
198
+ ...mockProviderInfo,
199
+ unsupportedParameters: ['frequencyPenalty'],
200
+ };
201
+ const modelWithExclusions = {
202
+ ...mockModelInfo,
203
+ unsupportedParameters: ['presencePenalty', 'stopSequences'],
204
+ };
205
+ const settingsWithAll = {
206
+ ...baseSettings,
207
+ frequencyPenalty: 0.5,
208
+ presencePenalty: -0.5,
209
+ stopSequences: ['END'],
210
+ };
211
+ const result = settingsManager.filterUnsupportedParameters(settingsWithAll, modelWithExclusions, providerWithExclusions);
212
+ expect(result.frequencyPenalty).toBeUndefined();
213
+ expect(result.presencePenalty).toBeUndefined();
214
+ expect(result.stopSequences).toBeUndefined();
215
+ });
216
+ it('should remove reasoning settings for non-reasoning models', () => {
217
+ const nonReasoningModel = {
218
+ ...mockModelInfo,
219
+ reasoning: { supported: false },
220
+ };
221
+ const settingsWithReasoning = {
222
+ ...baseSettings,
223
+ reasoning: {
224
+ enabled: true,
225
+ effort: 'high',
226
+ maxTokens: 5000,
227
+ exclude: false,
228
+ },
229
+ };
230
+ const result = settingsManager.filterUnsupportedParameters(settingsWithReasoning, nonReasoningModel, mockProviderInfo);
231
+ expect(result.reasoning).toBeUndefined();
232
+ });
233
+ it('should keep reasoning settings for reasoning-supported models', () => {
234
+ const reasoningModel = {
235
+ ...mockModelInfo,
236
+ reasoning: { supported: true },
237
+ };
238
+ const settingsWithReasoning = {
239
+ ...baseSettings,
240
+ reasoning: {
241
+ enabled: true,
242
+ effort: 'high',
243
+ maxTokens: 5000,
244
+ exclude: false,
245
+ },
246
+ };
247
+ const result = settingsManager.filterUnsupportedParameters(settingsWithReasoning, reasoningModel, mockProviderInfo);
248
+ expect(result.reasoning).toEqual(settingsWithReasoning.reasoning);
249
+ });
250
+ it('should handle geminiSafetySettings appropriately', () => {
251
+ const geminiProvider = {
252
+ id: 'gemini',
253
+ name: 'Google Gemini',
254
+ };
255
+ const settingsWithGemini = {
256
+ ...baseSettings,
257
+ geminiSafetySettings: [
258
+ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' },
259
+ ],
260
+ };
261
+ const result = settingsManager.filterUnsupportedParameters(settingsWithGemini, mockModelInfo, geminiProvider);
262
+ // Should not be filtered out for Gemini provider
263
+ expect(result.geminiSafetySettings).toEqual(settingsWithGemini.geminiSafetySettings);
264
+ });
265
+ });
266
+ });
@@ -29,6 +29,43 @@ export interface GeminiSafetySetting {
29
29
  category: GeminiHarmCategory;
30
30
  threshold: GeminiHarmBlockThreshold;
31
31
  }
32
+ /**
33
+ * Reasoning/thinking configuration for LLM requests
34
+ */
35
+ export interface LLMReasoningSettings {
36
+ /** Enable reasoning/thinking mode */
37
+ enabled?: boolean;
38
+ /** Effort-based control (OpenAI style) */
39
+ effort?: 'high' | 'medium' | 'low';
40
+ /** Token-based control (Anthropic/Gemini style) */
41
+ maxTokens?: number;
42
+ /** Exclude reasoning from response (keep internal only) */
43
+ exclude?: boolean;
44
+ }
45
+ /**
46
+ * Settings for extracting 'thinking' content from the start of a response
47
+ */
48
+ export interface LLMThinkingExtractionSettings {
49
+ /**
50
+ * If true, enables the automatic extraction of content from a specified XML tag.
51
+ * @default false
52
+ */
53
+ enabled?: boolean;
54
+ /**
55
+ * The XML tag name to look for (e.g., 'thinking', 'reasoning', 'scratchpad').
56
+ * @default 'thinking'
57
+ */
58
+ tag?: string;
59
+ /**
60
+ * Defines behavior when the tag is not found. 'auto' is the recommended default.
61
+ * - 'ignore': Silently continue without a warning or error.
62
+ * - 'warn': Log a console warning but return the response as-is.
63
+ * - 'error': Return an LLMFailureResponse, treating it as a failed request.
64
+ * - 'auto': Becomes 'error' unless the model has active native reasoning. If native reasoning is active, this becomes 'ignore'.
65
+ * @default 'auto'
66
+ */
67
+ onMissing?: 'ignore' | 'warn' | 'error' | 'auto';
68
+ }
32
69
  /**
33
70
  * Configurable settings for LLM requests
34
71
  */
@@ -51,6 +88,13 @@ export interface LLMSettings {
51
88
  supportsSystemMessage?: boolean;
52
89
  /** Gemini-specific safety settings for content filtering */
53
90
  geminiSafetySettings?: GeminiSafetySetting[];
91
+ /** Universal reasoning/thinking configuration */
92
+ reasoning?: LLMReasoningSettings;
93
+ /**
94
+ * Configuration for automatically extracting 'thinking' blocks from responses.
95
+ * Enabled by default.
96
+ */
97
+ thinkingExtraction?: LLMThinkingExtractionSettings;
54
98
  }
55
99
  /**
56
100
  * Request structure for chat completion
@@ -62,6 +106,17 @@ export interface LLMChatRequest {
62
106
  systemMessage?: string;
63
107
  settings?: LLMSettings;
64
108
  }
109
+ /**
110
+ * Extended request structure that supports preset IDs
111
+ */
112
+ export interface LLMChatRequestWithPreset extends Omit<LLMChatRequest, 'providerId' | 'modelId'> {
113
+ /** Provider ID (required if not using presetId) */
114
+ providerId?: ApiProviderId;
115
+ /** Model ID (required if not using presetId) */
116
+ modelId?: string;
117
+ /** Preset ID (alternative to providerId/modelId) */
118
+ presetId?: string;
119
+ }
65
120
  /**
66
121
  * Individual choice in an LLM response
67
122
  */
@@ -69,6 +124,10 @@ export interface LLMChoice {
69
124
  message: LLMMessage;
70
125
  finish_reason: string | null;
71
126
  index?: number;
127
+ /** Reasoning/thinking content (if available and not excluded) */
128
+ reasoning?: string;
129
+ /** Provider-specific reasoning details that need to be preserved */
130
+ reasoning_details?: any;
72
131
  }
73
132
  /**
74
133
  * Token usage information from LLM APIs
@@ -117,6 +176,34 @@ export interface ProviderInfo {
117
176
  name: string;
118
177
  unsupportedParameters?: (keyof LLMSettings)[];
119
178
  }
179
+ /**
180
+ * Reasoning/thinking capabilities for a model
181
+ */
182
+ export interface ModelReasoningCapabilities {
183
+ /** Does this model support reasoning/thinking? */
184
+ supported: boolean;
185
+ /** Is reasoning enabled by default? */
186
+ enabledByDefault?: boolean;
187
+ /** Can reasoning be disabled? (e.g., Gemini Pro can't) */
188
+ canDisable?: boolean;
189
+ /** Minimum token budget for reasoning */
190
+ minBudget?: number;
191
+ /** Maximum token budget for reasoning */
192
+ maxBudget?: number;
193
+ /** Default token budget if not specified */
194
+ defaultBudget?: number;
195
+ /** Special budget values (e.g., -1 for Gemini's dynamic) */
196
+ dynamicBudget?: {
197
+ value: number;
198
+ description: string;
199
+ };
200
+ /** Price per 1M reasoning tokens (optional - if not set, uses regular outputPrice) */
201
+ outputPrice?: number;
202
+ /** What type of reasoning output is returned */
203
+ outputType?: 'full' | 'summary' | 'none';
204
+ /** Token count above which streaming is required */
205
+ requiresStreamingAbove?: number;
206
+ }
120
207
  /**
121
208
  * Information about a supported LLM model
122
209
  */
@@ -132,10 +219,13 @@ export interface ModelInfo {
132
219
  maxTokens?: number;
133
220
  supportsImages?: boolean;
134
221
  supportsPromptCache: boolean;
222
+ /** @deprecated Use reasoning instead */
135
223
  thinkingConfig?: {
136
224
  maxBudget?: number;
137
225
  outputPrice?: number;
138
226
  };
227
+ /** Reasoning/thinking capabilities */
228
+ reasoning?: ModelReasoningCapabilities;
139
229
  cacheWritesPrice?: number;
140
230
  cacheReadsPrice?: number;
141
231
  unsupportedParameters?: (keyof LLMSettings)[];
@@ -153,3 +243,20 @@ export declare const LLM_IPC_CHANNELS: {
153
243
  * Type for LLM IPC channel names
154
244
  */
155
245
  export type LLMIPCChannelName = (typeof LLM_IPC_CHANNELS)[keyof typeof LLM_IPC_CHANNELS];
246
+ /**
247
+ * Model context variables injected into templates
248
+ */
249
+ export interface ModelContext {
250
+ /** Whether reasoning/thinking is enabled for this request */
251
+ thinking_enabled: boolean;
252
+ /** Whether the model supports reasoning/thinking */
253
+ thinking_available: boolean;
254
+ /** The resolved model ID */
255
+ model_id: string;
256
+ /** The resolved provider ID */
257
+ provider_id: string;
258
+ /** Reasoning effort level if specified */
259
+ reasoning_effort?: string;
260
+ /** Reasoning max tokens if specified */
261
+ reasoning_max_tokens?: number;
262
+ }
@@ -13,6 +13,10 @@ import type { LLMMessage } from '../llm/types';
13
13
  * and constructs a properly formatted array of LLMMessage objects ready to be
14
14
  * sent to an LLM service.
15
15
  *
16
+ * @deprecated Use `LLMService.createMessages` for a more integrated experience that includes
17
+ * model-aware template rendering. For standalone, model-agnostic role tag parsing, consider
18
+ * using the new `parseRoleTags` utility directly.
19
+ *
16
20
  * @param template The template string with {{variables}} and <ROLE> tags.
17
21
  * @param variables An object with values to substitute into the template.
18
22
  * @returns An array of LLMMessage objects.