genai-lite 0.2.1 → 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 (49) hide show
  1. package/README.md +382 -49
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.js +4 -3
  4. package/dist/llm/LLMService.createMessages.test.d.ts +4 -0
  5. package/dist/llm/LLMService.createMessages.test.js +364 -0
  6. package/dist/llm/LLMService.d.ts +48 -83
  7. package/dist/llm/LLMService.js +172 -480
  8. package/dist/llm/LLMService.original.d.ts +147 -0
  9. package/dist/llm/LLMService.original.js +656 -0
  10. package/dist/llm/LLMService.test.js +192 -0
  11. package/dist/llm/clients/AnthropicClientAdapter.test.js +4 -0
  12. package/dist/llm/clients/GeminiClientAdapter.test.js +4 -0
  13. package/dist/llm/clients/MockClientAdapter.js +9 -3
  14. package/dist/llm/clients/MockClientAdapter.test.js +4 -0
  15. package/dist/llm/clients/OpenAIClientAdapter.test.js +4 -0
  16. package/dist/llm/config.js +5 -0
  17. package/dist/llm/services/AdapterRegistry.d.ts +59 -0
  18. package/dist/llm/services/AdapterRegistry.js +113 -0
  19. package/dist/llm/services/AdapterRegistry.test.d.ts +1 -0
  20. package/dist/llm/services/AdapterRegistry.test.js +239 -0
  21. package/dist/llm/services/ModelResolver.d.ts +35 -0
  22. package/dist/llm/services/ModelResolver.js +116 -0
  23. package/dist/llm/services/ModelResolver.test.d.ts +1 -0
  24. package/dist/llm/services/ModelResolver.test.js +158 -0
  25. package/dist/llm/services/PresetManager.d.ts +27 -0
  26. package/dist/llm/services/PresetManager.js +50 -0
  27. package/dist/llm/services/PresetManager.test.d.ts +1 -0
  28. package/dist/llm/services/PresetManager.test.js +210 -0
  29. package/dist/llm/services/RequestValidator.d.ts +31 -0
  30. package/dist/llm/services/RequestValidator.js +122 -0
  31. package/dist/llm/services/RequestValidator.test.d.ts +1 -0
  32. package/dist/llm/services/RequestValidator.test.js +159 -0
  33. package/dist/llm/services/SettingsManager.d.ts +32 -0
  34. package/dist/llm/services/SettingsManager.js +223 -0
  35. package/dist/llm/services/SettingsManager.test.d.ts +1 -0
  36. package/dist/llm/services/SettingsManager.test.js +266 -0
  37. package/dist/llm/types.d.ts +29 -28
  38. package/dist/prompting/builder.d.ts +4 -0
  39. package/dist/prompting/builder.js +12 -61
  40. package/dist/prompting/content.js +3 -9
  41. package/dist/prompting/index.d.ts +2 -3
  42. package/dist/prompting/index.js +4 -5
  43. package/dist/prompting/parser.d.ts +80 -0
  44. package/dist/prompting/parser.js +133 -0
  45. package/dist/prompting/parser.test.js +348 -0
  46. package/dist/prompting/template.d.ts +8 -0
  47. package/dist/prompting/template.js +89 -6
  48. package/dist/prompting/template.test.js +116 -0
  49. package/package.json +1 -1
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Mock the client adapters first
4
+ jest.mock('../clients/MockClientAdapter');
5
+ jest.mock('../clients/OpenAIClientAdapter', () => ({
6
+ OpenAIClientAdapter: jest.fn().mockImplementation(() => ({
7
+ sendMessage: jest.fn(),
8
+ getAdapterInfo: () => ({ providerId: 'openai', name: 'OpenAI Adapter' })
9
+ }))
10
+ }));
11
+ jest.mock('../clients/AnthropicClientAdapter', () => ({
12
+ AnthropicClientAdapter: jest.fn().mockImplementation(() => ({
13
+ sendMessage: jest.fn(),
14
+ getAdapterInfo: () => ({ providerId: 'anthropic', name: 'Anthropic Adapter' })
15
+ }))
16
+ }));
17
+ jest.mock('../clients/GeminiClientAdapter', () => ({
18
+ GeminiClientAdapter: jest.fn().mockImplementation(() => ({
19
+ sendMessage: jest.fn(),
20
+ getAdapterInfo: () => ({ providerId: 'gemini', name: 'Gemini Adapter' })
21
+ }))
22
+ }));
23
+ // Mock the config imports - must be before imports that use it
24
+ jest.mock('../config', () => {
25
+ const { OpenAIClientAdapter } = require('../clients/OpenAIClientAdapter');
26
+ const { AnthropicClientAdapter } = require('../clients/AnthropicClientAdapter');
27
+ const { GeminiClientAdapter } = require('../clients/GeminiClientAdapter');
28
+ return {
29
+ SUPPORTED_PROVIDERS: [
30
+ { id: 'openai', name: 'OpenAI' },
31
+ { id: 'anthropic', name: 'Anthropic' },
32
+ { id: 'gemini', name: 'Google Gemini' },
33
+ { id: 'mistral', name: 'Mistral' }
34
+ ],
35
+ ADAPTER_CONSTRUCTORS: {
36
+ openai: OpenAIClientAdapter,
37
+ anthropic: AnthropicClientAdapter,
38
+ gemini: GeminiClientAdapter,
39
+ // mistral intentionally missing to test fallback
40
+ },
41
+ ADAPTER_CONFIGS: {
42
+ openai: {},
43
+ anthropic: {},
44
+ gemini: {},
45
+ mistral: {}
46
+ }
47
+ };
48
+ });
49
+ // Now import the modules
50
+ const AdapterRegistry_1 = require("./AdapterRegistry");
51
+ const MockClientAdapter_1 = require("../clients/MockClientAdapter");
52
+ const OpenAIClientAdapter_1 = require("../clients/OpenAIClientAdapter");
53
+ describe('AdapterRegistry', () => {
54
+ let registry;
55
+ let consoleLogSpy;
56
+ let consoleWarnSpy;
57
+ let consoleErrorSpy;
58
+ beforeEach(() => {
59
+ jest.clearAllMocks();
60
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
61
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
62
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
63
+ });
64
+ afterEach(() => {
65
+ consoleLogSpy.mockRestore();
66
+ consoleWarnSpy.mockRestore();
67
+ consoleErrorSpy.mockRestore();
68
+ });
69
+ describe('initialization', () => {
70
+ it('should initialize with adapters for providers with constructors', () => {
71
+ registry = new AdapterRegistry_1.AdapterRegistry();
72
+ // Should have logged successful initialization
73
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Initialized with 3 dynamically registered adapter(s)'));
74
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('openai, anthropic, gemini'));
75
+ // Should warn about missing constructor for mistral
76
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("No adapter constructor found for supported provider 'mistral'"));
77
+ });
78
+ it('should handle adapter initialization errors gracefully', () => {
79
+ // Make OpenAIClientAdapter throw an error on construction
80
+ OpenAIClientAdapter_1.OpenAIClientAdapter.mockImplementationOnce(() => {
81
+ throw new Error('Initialization failed');
82
+ });
83
+ registry = new AdapterRegistry_1.AdapterRegistry();
84
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to instantiate adapter for provider 'openai'"), expect.any(Error));
85
+ });
86
+ it('should log when no adapters are registered', () => {
87
+ // Clear all module cache to ensure clean isolation
88
+ jest.resetModules();
89
+ // Mock the config module with no constructors before requiring AdapterRegistry
90
+ jest.doMock('../config', () => ({
91
+ SUPPORTED_PROVIDERS: [
92
+ { id: 'openai', name: 'OpenAI' },
93
+ { id: 'anthropic', name: 'Anthropic' },
94
+ { id: 'gemini', name: 'Google Gemini' },
95
+ { id: 'mistral', name: 'Mistral' }
96
+ ],
97
+ ADAPTER_CONSTRUCTORS: {}, // No constructors
98
+ ADAPTER_CONFIGS: {
99
+ openai: {},
100
+ anthropic: {},
101
+ gemini: {},
102
+ mistral: {}
103
+ }
104
+ }));
105
+ // Mock the client adapters to prevent import errors
106
+ jest.doMock('../clients/MockClientAdapter', () => ({
107
+ MockClientAdapter: jest.fn()
108
+ }));
109
+ jest.doMock('../clients/OpenAIClientAdapter', () => ({
110
+ OpenAIClientAdapter: jest.fn()
111
+ }));
112
+ jest.doMock('../clients/AnthropicClientAdapter', () => ({
113
+ AnthropicClientAdapter: jest.fn()
114
+ }));
115
+ jest.doMock('../clients/GeminiClientAdapter', () => ({
116
+ GeminiClientAdapter: jest.fn()
117
+ }));
118
+ // Clear console spy calls before the test
119
+ consoleLogSpy.mockClear();
120
+ // Now require the AdapterRegistry with mocked dependencies
121
+ const { AdapterRegistry: IsolatedAdapterRegistry } = require('./AdapterRegistry');
122
+ const isolatedRegistry = new IsolatedAdapterRegistry();
123
+ expect(consoleLogSpy).toHaveBeenCalledWith('LLMService: No real adapters were dynamically registered. All providers will use the mock adapter.');
124
+ // Restore modules after test
125
+ jest.resetModules();
126
+ });
127
+ });
128
+ describe('getAdapter', () => {
129
+ beforeEach(() => {
130
+ registry = new AdapterRegistry_1.AdapterRegistry();
131
+ });
132
+ it('should return registered adapter for supported provider', () => {
133
+ const adapter = registry.getAdapter('openai');
134
+ expect(adapter).toBeDefined();
135
+ expect(consoleLogSpy).toHaveBeenCalledWith('Using registered adapter for provider: openai');
136
+ });
137
+ it('should return mock adapter for provider without adapter', () => {
138
+ const adapter = registry.getAdapter('mistral');
139
+ expect(adapter).toBeInstanceOf(MockClientAdapter_1.MockClientAdapter);
140
+ expect(consoleLogSpy).toHaveBeenCalledWith('No real adapter found for mistral, using mock adapter');
141
+ });
142
+ it('should return mock adapter for unknown provider', () => {
143
+ const adapter = registry.getAdapter('unknown-provider');
144
+ expect(adapter).toBeInstanceOf(MockClientAdapter_1.MockClientAdapter);
145
+ expect(consoleLogSpy).toHaveBeenCalledWith('No real adapter found for unknown-provider, using mock adapter');
146
+ });
147
+ });
148
+ describe('registerAdapter', () => {
149
+ beforeEach(() => {
150
+ registry = new AdapterRegistry_1.AdapterRegistry();
151
+ });
152
+ it('should register new adapter', () => {
153
+ const mockAdapter = {
154
+ sendMessage: jest.fn(),
155
+ getAdapterInfo: () => ({ providerId: 'custom-provider', name: 'Custom Adapter' })
156
+ };
157
+ registry.registerAdapter('custom-provider', mockAdapter);
158
+ expect(consoleLogSpy).toHaveBeenCalledWith('Registered client adapter for provider: custom-provider');
159
+ // Should be able to retrieve it
160
+ const retrievedAdapter = registry.getAdapter('custom-provider');
161
+ expect(retrievedAdapter).toBe(mockAdapter);
162
+ });
163
+ it('should override existing adapter', () => {
164
+ const newAdapter = {
165
+ sendMessage: jest.fn(),
166
+ getAdapterInfo: () => ({ providerId: 'openai', name: 'New OpenAI Adapter' })
167
+ };
168
+ registry.registerAdapter('openai', newAdapter);
169
+ const retrievedAdapter = registry.getAdapter('openai');
170
+ expect(retrievedAdapter).toBe(newAdapter);
171
+ });
172
+ });
173
+ describe('getRegisteredAdapters', () => {
174
+ beforeEach(() => {
175
+ registry = new AdapterRegistry_1.AdapterRegistry();
176
+ });
177
+ it('should return info for all registered adapters', () => {
178
+ const adapterInfo = registry.getRegisteredAdapters();
179
+ expect(adapterInfo.size).toBe(3); // openai, anthropic, gemini
180
+ expect(adapterInfo.get('openai')).toEqual({
181
+ providerId: 'openai',
182
+ hasAdapter: true,
183
+ adapterInfo: { providerId: 'openai', name: 'OpenAI Adapter' }
184
+ });
185
+ expect(adapterInfo.get('anthropic')).toEqual({
186
+ providerId: 'anthropic',
187
+ hasAdapter: true,
188
+ adapterInfo: { providerId: 'anthropic', name: 'Anthropic Adapter' }
189
+ });
190
+ expect(adapterInfo.get('gemini')).toEqual({
191
+ providerId: 'gemini',
192
+ hasAdapter: true,
193
+ adapterInfo: { providerId: 'gemini', name: 'Gemini Adapter' }
194
+ });
195
+ // mistral should not be in the map
196
+ expect(adapterInfo.has('mistral')).toBe(false);
197
+ });
198
+ it('should handle adapters without getAdapterInfo method', () => {
199
+ const adapterWithoutInfo = {
200
+ sendMessage: jest.fn(),
201
+ // No getAdapterInfo method
202
+ };
203
+ registry.registerAdapter('custom', adapterWithoutInfo);
204
+ const adapterInfo = registry.getRegisteredAdapters();
205
+ expect(adapterInfo.get('custom')).toEqual({
206
+ providerId: 'custom',
207
+ hasAdapter: true,
208
+ adapterInfo: { name: 'Unknown Adapter' }
209
+ });
210
+ });
211
+ });
212
+ describe('getProviderSummary', () => {
213
+ beforeEach(() => {
214
+ registry = new AdapterRegistry_1.AdapterRegistry();
215
+ });
216
+ it('should return correct summary of provider availability', () => {
217
+ const summary = registry.getProviderSummary();
218
+ expect(summary).toEqual({
219
+ totalProviders: 4,
220
+ providersWithAdapters: 3,
221
+ availableProviders: ['openai', 'anthropic', 'gemini'],
222
+ unavailableProviders: ['mistral']
223
+ });
224
+ });
225
+ it('should update summary after registering new adapter', () => {
226
+ const mockAdapter = {
227
+ sendMessage: jest.fn(),
228
+ };
229
+ registry.registerAdapter('mistral', mockAdapter);
230
+ const summary = registry.getProviderSummary();
231
+ expect(summary).toEqual({
232
+ totalProviders: 4,
233
+ providersWithAdapters: 4,
234
+ availableProviders: ['openai', 'anthropic', 'gemini', 'mistral'],
235
+ unavailableProviders: []
236
+ });
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,35 @@
1
+ import type { LLMFailureResponse, LLMSettings, ModelInfo } from "../types";
2
+ import { PresetManager } from "./PresetManager";
3
+ /**
4
+ * Options for model selection
5
+ */
6
+ export interface ModelSelectionOptions {
7
+ presetId?: string;
8
+ providerId?: string;
9
+ modelId?: string;
10
+ settings?: LLMSettings;
11
+ }
12
+ /**
13
+ * Result of model resolution
14
+ */
15
+ export interface ModelResolution {
16
+ providerId?: string;
17
+ modelId?: string;
18
+ modelInfo?: ModelInfo;
19
+ settings?: LLMSettings;
20
+ error?: LLMFailureResponse;
21
+ }
22
+ /**
23
+ * Resolves model information from presets or direct provider/model IDs
24
+ */
25
+ export declare class ModelResolver {
26
+ private presetManager;
27
+ constructor(presetManager: PresetManager);
28
+ /**
29
+ * Resolves model information from either a preset ID or provider/model IDs
30
+ *
31
+ * @param options Options containing either presetId or providerId/modelId
32
+ * @returns Resolved model info and settings or error response
33
+ */
34
+ resolve(options: ModelSelectionOptions): ModelResolution;
35
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModelResolver = void 0;
4
+ const config_1 = require("../config");
5
+ /**
6
+ * Resolves model information from presets or direct provider/model IDs
7
+ */
8
+ class ModelResolver {
9
+ constructor(presetManager) {
10
+ this.presetManager = presetManager;
11
+ }
12
+ /**
13
+ * Resolves model information from either a preset ID or provider/model IDs
14
+ *
15
+ * @param options Options containing either presetId or providerId/modelId
16
+ * @returns Resolved model info and settings or error response
17
+ */
18
+ resolve(options) {
19
+ // If presetId is provided, use it
20
+ if (options.presetId) {
21
+ const preset = this.presetManager.resolvePreset(options.presetId);
22
+ if (!preset) {
23
+ return {
24
+ error: {
25
+ provider: 'unknown',
26
+ model: 'unknown',
27
+ error: {
28
+ message: `Preset not found: ${options.presetId}`,
29
+ code: 'PRESET_NOT_FOUND',
30
+ type: 'validation_error',
31
+ },
32
+ object: 'error',
33
+ }
34
+ };
35
+ }
36
+ const modelInfo = (0, config_1.getModelById)(preset.modelId, preset.providerId);
37
+ if (!modelInfo) {
38
+ return {
39
+ error: {
40
+ provider: preset.providerId,
41
+ model: preset.modelId,
42
+ error: {
43
+ message: `Model not found for preset: ${options.presetId}`,
44
+ code: 'MODEL_NOT_FOUND',
45
+ type: 'validation_error',
46
+ },
47
+ object: 'error',
48
+ }
49
+ };
50
+ }
51
+ // Merge preset settings with user settings
52
+ const settings = {
53
+ ...preset.settings,
54
+ ...options.settings
55
+ };
56
+ return {
57
+ providerId: preset.providerId,
58
+ modelId: preset.modelId,
59
+ modelInfo,
60
+ settings
61
+ };
62
+ }
63
+ // Otherwise, use providerId and modelId
64
+ if (!options.providerId || !options.modelId) {
65
+ return {
66
+ error: {
67
+ provider: (options.providerId || 'unknown'),
68
+ model: options.modelId || 'unknown',
69
+ error: {
70
+ message: 'Either presetId or both providerId and modelId must be provided',
71
+ code: 'INVALID_MODEL_SELECTION',
72
+ type: 'validation_error',
73
+ },
74
+ object: 'error',
75
+ }
76
+ };
77
+ }
78
+ // Check if provider is supported first
79
+ if (!(0, config_1.isProviderSupported)(options.providerId)) {
80
+ return {
81
+ error: {
82
+ provider: options.providerId,
83
+ model: options.modelId,
84
+ error: {
85
+ message: `Unsupported provider: ${options.providerId}. Supported providers: ${config_1.SUPPORTED_PROVIDERS.map((p) => p.id).join(', ')}`,
86
+ code: 'UNSUPPORTED_PROVIDER',
87
+ type: 'validation_error',
88
+ },
89
+ object: 'error',
90
+ }
91
+ };
92
+ }
93
+ const modelInfo = (0, config_1.getModelById)(options.modelId, options.providerId);
94
+ if (!modelInfo) {
95
+ return {
96
+ error: {
97
+ provider: options.providerId,
98
+ model: options.modelId,
99
+ error: {
100
+ message: `Unsupported model: ${options.modelId} for provider: ${options.providerId}`,
101
+ code: 'UNSUPPORTED_MODEL',
102
+ type: 'validation_error',
103
+ },
104
+ object: 'error',
105
+ }
106
+ };
107
+ }
108
+ return {
109
+ providerId: options.providerId,
110
+ modelId: options.modelId,
111
+ modelInfo,
112
+ settings: options.settings
113
+ };
114
+ }
115
+ }
116
+ exports.ModelResolver = ModelResolver;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ModelResolver_1 = require("./ModelResolver");
4
+ describe('ModelResolver', () => {
5
+ let resolver;
6
+ let mockPresetManager;
7
+ beforeEach(() => {
8
+ mockPresetManager = {
9
+ getPresets: jest.fn(),
10
+ resolvePreset: jest.fn(),
11
+ };
12
+ resolver = new ModelResolver_1.ModelResolver(mockPresetManager);
13
+ });
14
+ describe('preset resolution', () => {
15
+ it('should resolve model from valid preset', () => {
16
+ const mockPreset = {
17
+ id: 'test-preset',
18
+ displayName: 'Test Preset',
19
+ providerId: 'openai',
20
+ modelId: 'gpt-4.1',
21
+ settings: { temperature: 0.7 }
22
+ };
23
+ mockPresetManager.resolvePreset.mockReturnValue(mockPreset);
24
+ const result = resolver.resolve({ presetId: 'test-preset' });
25
+ expect(mockPresetManager.resolvePreset).toHaveBeenCalledWith('test-preset');
26
+ expect(result.error).toBeUndefined();
27
+ expect(result.providerId).toBe('openai');
28
+ expect(result.modelId).toBe('gpt-4.1');
29
+ expect(result.modelInfo).toBeDefined();
30
+ expect(result.settings).toEqual({ temperature: 0.7 });
31
+ });
32
+ it('should return error for non-existent preset', () => {
33
+ mockPresetManager.resolvePreset.mockReturnValue(null);
34
+ const result = resolver.resolve({ presetId: 'non-existent' });
35
+ expect(result.error).toBeDefined();
36
+ expect(result.error?.error.code).toBe('PRESET_NOT_FOUND');
37
+ expect(result.error?.error.message).toContain('Preset not found: non-existent');
38
+ });
39
+ it('should return error for preset with invalid model', () => {
40
+ const mockPreset = {
41
+ id: 'invalid-model-preset',
42
+ displayName: 'Invalid Model Preset',
43
+ providerId: 'openai',
44
+ modelId: 'invalid-model',
45
+ settings: {}
46
+ };
47
+ mockPresetManager.resolvePreset.mockReturnValue(mockPreset);
48
+ const result = resolver.resolve({ presetId: 'invalid-model-preset' });
49
+ expect(result.error).toBeDefined();
50
+ expect(result.error?.error.code).toBe('MODEL_NOT_FOUND');
51
+ expect(result.error?.error.message).toContain('Model not found for preset');
52
+ });
53
+ it('should merge preset settings with user settings', () => {
54
+ const mockPreset = {
55
+ id: 'test-preset',
56
+ displayName: 'Test Preset',
57
+ providerId: 'openai',
58
+ modelId: 'gpt-4.1',
59
+ settings: {
60
+ temperature: 0.7,
61
+ maxTokens: 1000
62
+ }
63
+ };
64
+ mockPresetManager.resolvePreset.mockReturnValue(mockPreset);
65
+ const result = resolver.resolve({
66
+ presetId: 'test-preset',
67
+ settings: {
68
+ temperature: 0.9, // Override
69
+ topP: 0.95 // New setting
70
+ }
71
+ });
72
+ expect(result.settings).toEqual({
73
+ temperature: 0.9, // User override
74
+ maxTokens: 1000, // From preset
75
+ topP: 0.95 // User addition
76
+ });
77
+ });
78
+ });
79
+ describe('direct model resolution', () => {
80
+ it('should resolve model from valid provider and model IDs', () => {
81
+ const result = resolver.resolve({
82
+ providerId: 'openai',
83
+ modelId: 'gpt-4.1'
84
+ });
85
+ expect(result.error).toBeUndefined();
86
+ expect(result.providerId).toBe('openai');
87
+ expect(result.modelId).toBe('gpt-4.1');
88
+ expect(result.modelInfo).toBeDefined();
89
+ expect(result.modelInfo?.id).toBe('gpt-4.1');
90
+ });
91
+ it('should return error when neither preset nor provider/model provided', () => {
92
+ const result = resolver.resolve({});
93
+ expect(result.error).toBeDefined();
94
+ expect(result.error?.error.code).toBe('INVALID_MODEL_SELECTION');
95
+ expect(result.error?.error.message).toContain('Either presetId or both providerId and modelId must be provided');
96
+ });
97
+ it('should return error when only providerId provided', () => {
98
+ const result = resolver.resolve({ providerId: 'openai' });
99
+ expect(result.error).toBeDefined();
100
+ expect(result.error?.error.code).toBe('INVALID_MODEL_SELECTION');
101
+ });
102
+ it('should return error when only modelId provided', () => {
103
+ const result = resolver.resolve({ modelId: 'gpt-4.1' });
104
+ expect(result.error).toBeDefined();
105
+ expect(result.error?.error.code).toBe('INVALID_MODEL_SELECTION');
106
+ });
107
+ it('should return error for unsupported provider', () => {
108
+ const result = resolver.resolve({
109
+ providerId: 'unsupported-provider',
110
+ modelId: 'some-model'
111
+ });
112
+ expect(result.error).toBeDefined();
113
+ expect(result.error?.error.code).toBe('UNSUPPORTED_PROVIDER');
114
+ expect(result.error?.error.message).toContain('Unsupported provider');
115
+ expect(result.error?.error.message).toContain('Supported providers:');
116
+ });
117
+ it('should return error for unsupported model', () => {
118
+ const result = resolver.resolve({
119
+ providerId: 'openai',
120
+ modelId: 'unsupported-model'
121
+ });
122
+ expect(result.error).toBeDefined();
123
+ expect(result.error?.error.code).toBe('UNSUPPORTED_MODEL');
124
+ expect(result.error?.error.message).toContain('Unsupported model: unsupported-model');
125
+ });
126
+ it('should pass through user settings for direct resolution', () => {
127
+ const settings = {
128
+ temperature: 0.8,
129
+ maxTokens: 2000
130
+ };
131
+ const result = resolver.resolve({
132
+ providerId: 'openai',
133
+ modelId: 'gpt-4.1',
134
+ settings
135
+ });
136
+ expect(result.settings).toBe(settings); // Should be the same reference
137
+ });
138
+ });
139
+ describe('priority handling', () => {
140
+ it('should prioritize presetId over providerId/modelId when both provided', () => {
141
+ const mockPreset = {
142
+ id: 'test-preset',
143
+ displayName: 'Test Preset',
144
+ providerId: 'anthropic',
145
+ modelId: 'claude-3-5-sonnet-20241022',
146
+ settings: {}
147
+ };
148
+ mockPresetManager.resolvePreset.mockReturnValue(mockPreset);
149
+ const result = resolver.resolve({
150
+ presetId: 'test-preset',
151
+ providerId: 'openai', // These should be ignored
152
+ modelId: 'gpt-4.1' // These should be ignored
153
+ });
154
+ expect(result.providerId).toBe('anthropic');
155
+ expect(result.modelId).toBe('claude-3-5-sonnet-20241022');
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,27 @@
1
+ import type { ModelPreset } from "../../types/presets";
2
+ /**
3
+ * Defines how custom presets interact with the default presets.
4
+ * 'replace': Use only the custom presets provided. The default set is ignored.
5
+ * 'extend': Use the default presets, and add/override them with the custom presets. This is the default behavior.
6
+ */
7
+ export type PresetMode = 'replace' | 'extend';
8
+ /**
9
+ * Manages model presets including loading, merging, and resolution
10
+ */
11
+ export declare class PresetManager {
12
+ private presets;
13
+ constructor(customPresets?: ModelPreset[], mode?: PresetMode);
14
+ /**
15
+ * Gets all configured model presets
16
+ *
17
+ * @returns Array of model presets
18
+ */
19
+ getPresets(): ModelPreset[];
20
+ /**
21
+ * Resolves a preset by ID
22
+ *
23
+ * @param presetId - The preset ID to resolve
24
+ * @returns The preset if found, null otherwise
25
+ */
26
+ resolvePreset(presetId: string): ModelPreset | null;
27
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PresetManager = void 0;
7
+ const presets_json_1 = __importDefault(require("../../config/presets.json"));
8
+ /**
9
+ * Manages model presets including loading, merging, and resolution
10
+ */
11
+ class PresetManager {
12
+ constructor(customPresets = [], mode = 'extend') {
13
+ // Initialize presets based on mode
14
+ const finalPresets = new Map();
15
+ if (mode === 'replace') {
16
+ // Replace Mode: Only use custom presets.
17
+ for (const preset of customPresets) {
18
+ finalPresets.set(preset.id, preset);
19
+ }
20
+ }
21
+ else {
22
+ // Extend Mode: Load defaults first, then add/override.
23
+ for (const preset of presets_json_1.default) {
24
+ finalPresets.set(preset.id, preset);
25
+ }
26
+ for (const preset of customPresets) {
27
+ finalPresets.set(preset.id, preset);
28
+ }
29
+ }
30
+ this.presets = Array.from(finalPresets.values());
31
+ }
32
+ /**
33
+ * Gets all configured model presets
34
+ *
35
+ * @returns Array of model presets
36
+ */
37
+ getPresets() {
38
+ return [...this.presets]; // Return a copy to prevent external modification
39
+ }
40
+ /**
41
+ * Resolves a preset by ID
42
+ *
43
+ * @param presetId - The preset ID to resolve
44
+ * @returns The preset if found, null otherwise
45
+ */
46
+ resolvePreset(presetId) {
47
+ return this.presets.find(p => p.id === presetId) || null;
48
+ }
49
+ }
50
+ exports.PresetManager = PresetManager;
@@ -0,0 +1 @@
1
+ export {};