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.
- package/README.md +382 -49
- 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 +48 -83
- package/dist/llm/LLMService.js +172 -480
- package/dist/llm/LLMService.original.d.ts +147 -0
- package/dist/llm/LLMService.original.js +656 -0
- package/dist/llm/LLMService.test.js +192 -0
- package/dist/llm/clients/AnthropicClientAdapter.test.js +4 -0
- package/dist/llm/clients/GeminiClientAdapter.test.js +4 -0
- package/dist/llm/clients/MockClientAdapter.js +9 -3
- package/dist/llm/clients/MockClientAdapter.test.js +4 -0
- package/dist/llm/clients/OpenAIClientAdapter.test.js +4 -0
- package/dist/llm/config.js +5 -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 +29 -28
- 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 +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 {};
|