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.
- package/README.md +508 -30
- package/dist/config/presets.json +121 -17
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/llm/LLMService.createMessages.test.d.ts +4 -0
- package/dist/llm/LLMService.createMessages.test.js +364 -0
- package/dist/llm/LLMService.d.ts +49 -47
- package/dist/llm/LLMService.js +208 -303
- package/dist/llm/LLMService.original.d.ts +147 -0
- package/dist/llm/LLMService.original.js +656 -0
- package/dist/llm/LLMService.prepareMessage.test.d.ts +1 -0
- package/dist/llm/LLMService.prepareMessage.test.js +303 -0
- package/dist/llm/LLMService.sendMessage.preset.test.d.ts +1 -0
- package/dist/llm/LLMService.sendMessage.preset.test.js +153 -0
- package/dist/llm/LLMService.test.js +275 -0
- package/dist/llm/clients/AnthropicClientAdapter.js +64 -10
- package/dist/llm/clients/AnthropicClientAdapter.test.js +11 -1
- package/dist/llm/clients/GeminiClientAdapter.js +70 -11
- package/dist/llm/clients/GeminiClientAdapter.test.js +125 -1
- package/dist/llm/clients/MockClientAdapter.js +9 -3
- package/dist/llm/clients/MockClientAdapter.test.js +11 -1
- package/dist/llm/clients/OpenAIClientAdapter.js +26 -10
- package/dist/llm/clients/OpenAIClientAdapter.test.js +11 -1
- package/dist/llm/config.js +117 -2
- package/dist/llm/config.test.js +17 -0
- package/dist/llm/services/AdapterRegistry.d.ts +59 -0
- package/dist/llm/services/AdapterRegistry.js +113 -0
- package/dist/llm/services/AdapterRegistry.test.d.ts +1 -0
- package/dist/llm/services/AdapterRegistry.test.js +239 -0
- package/dist/llm/services/ModelResolver.d.ts +35 -0
- package/dist/llm/services/ModelResolver.js +116 -0
- package/dist/llm/services/ModelResolver.test.d.ts +1 -0
- package/dist/llm/services/ModelResolver.test.js +158 -0
- package/dist/llm/services/PresetManager.d.ts +27 -0
- package/dist/llm/services/PresetManager.js +50 -0
- package/dist/llm/services/PresetManager.test.d.ts +1 -0
- package/dist/llm/services/PresetManager.test.js +210 -0
- package/dist/llm/services/RequestValidator.d.ts +31 -0
- package/dist/llm/services/RequestValidator.js +122 -0
- package/dist/llm/services/RequestValidator.test.d.ts +1 -0
- package/dist/llm/services/RequestValidator.test.js +159 -0
- package/dist/llm/services/SettingsManager.d.ts +32 -0
- package/dist/llm/services/SettingsManager.js +223 -0
- package/dist/llm/services/SettingsManager.test.d.ts +1 -0
- package/dist/llm/services/SettingsManager.test.js +266 -0
- package/dist/llm/types.d.ts +107 -0
- package/dist/prompting/builder.d.ts +4 -0
- package/dist/prompting/builder.js +12 -61
- package/dist/prompting/content.js +3 -9
- package/dist/prompting/index.d.ts +2 -3
- package/dist/prompting/index.js +4 -5
- package/dist/prompting/parser.d.ts +80 -0
- package/dist/prompting/parser.js +133 -0
- package/dist/prompting/parser.test.js +348 -0
- package/dist/prompting/template.d.ts +8 -0
- package/dist/prompting/template.js +89 -6
- package/dist/prompting/template.test.js +116 -0
- package/package.json +3 -2
- 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
|
+
});
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -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.
|