mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: enhance SDK options with thinking level support
- Introduced a new function, buildThinkingOptions, to handle the conversion of ThinkingLevel to maxThinkingTokens for the Claude SDK. - Updated existing SDK option creation functions to incorporate thinking options, ensuring that maxThinkingTokens are included based on the specified thinking level. - Enhanced the settings service to support migration of phase models to include thinking levels, improving compatibility with new configurations. - Added comprehensive tests for thinking level integration and migration logic, ensuring robust functionality across the application. This update significantly improves the SDK's configurability and performance by allowing for more nuanced control over reasoning capabilities.
This commit is contained in:
@@ -554,4 +554,203 @@ describe('sdk-options.ts', () => {
|
||||
expect(options.abortController).toBe(abortController);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThinkingTokenBudget (from @automaker/types)', () => {
|
||||
it('should return undefined for "none" thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget('none')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for undefined thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 1024 for "low" thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget('low')).toBe(1024);
|
||||
});
|
||||
|
||||
it('should return 10000 for "medium" thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget('medium')).toBe(10000);
|
||||
});
|
||||
|
||||
it('should return 16000 for "high" thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget('high')).toBe(16000);
|
||||
});
|
||||
|
||||
it('should return 32000 for "ultrathink" thinking level', async () => {
|
||||
const { getThinkingTokenBudget } = await import('@automaker/types');
|
||||
expect(getThinkingTokenBudget('ultrathink')).toBe(32000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('THINKING_TOKEN_BUDGET constant', () => {
|
||||
it('should have correct values for all thinking levels', async () => {
|
||||
const { THINKING_TOKEN_BUDGET } = await import('@automaker/types');
|
||||
|
||||
expect(THINKING_TOKEN_BUDGET.none).toBeUndefined();
|
||||
expect(THINKING_TOKEN_BUDGET.low).toBe(1024);
|
||||
expect(THINKING_TOKEN_BUDGET.medium).toBe(10000);
|
||||
expect(THINKING_TOKEN_BUDGET.high).toBe(16000);
|
||||
expect(THINKING_TOKEN_BUDGET.ultrathink).toBe(32000);
|
||||
});
|
||||
|
||||
it('should have minimum of 1024 for enabled thinking levels', async () => {
|
||||
const { THINKING_TOKEN_BUDGET } = await import('@automaker/types');
|
||||
|
||||
// Per Claude SDK docs: minimum is 1024 tokens
|
||||
expect(THINKING_TOKEN_BUDGET.low).toBeGreaterThanOrEqual(1024);
|
||||
expect(THINKING_TOKEN_BUDGET.medium).toBeGreaterThanOrEqual(1024);
|
||||
expect(THINKING_TOKEN_BUDGET.high).toBeGreaterThanOrEqual(1024);
|
||||
expect(THINKING_TOKEN_BUDGET.ultrathink).toBeGreaterThanOrEqual(1024);
|
||||
});
|
||||
|
||||
it('should have ultrathink at or below 32000 to avoid timeouts', async () => {
|
||||
const { THINKING_TOKEN_BUDGET } = await import('@automaker/types');
|
||||
|
||||
// Per Claude SDK docs: above 32000 risks timeouts
|
||||
expect(THINKING_TOKEN_BUDGET.ultrathink).toBeLessThanOrEqual(32000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('thinking level integration with SDK options', () => {
|
||||
describe('createSpecGenerationOptions with thinkingLevel', () => {
|
||||
it('should not include maxThinkingTokens when thinkingLevel is undefined', async () => {
|
||||
const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSpecGenerationOptions({ cwd: '/test/path' });
|
||||
|
||||
expect(options.maxThinkingTokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not include maxThinkingTokens when thinkingLevel is "none"', async () => {
|
||||
const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSpecGenerationOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'none',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include maxThinkingTokens for "low" thinkingLevel', async () => {
|
||||
const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSpecGenerationOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'low',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(1024);
|
||||
});
|
||||
|
||||
it('should include maxThinkingTokens for "high" thinkingLevel', async () => {
|
||||
const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSpecGenerationOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'high',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(16000);
|
||||
});
|
||||
|
||||
it('should include maxThinkingTokens for "ultrathink" thinkingLevel', async () => {
|
||||
const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSpecGenerationOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'ultrathink',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(32000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAutoModeOptions with thinkingLevel', () => {
|
||||
it('should not include maxThinkingTokens when thinkingLevel is undefined', async () => {
|
||||
const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createAutoModeOptions({ cwd: '/test/path' });
|
||||
|
||||
expect(options.maxThinkingTokens).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include maxThinkingTokens for "medium" thinkingLevel', async () => {
|
||||
const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createAutoModeOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'medium',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(10000);
|
||||
});
|
||||
|
||||
it('should include maxThinkingTokens for "ultrathink" thinkingLevel', async () => {
|
||||
const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createAutoModeOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'ultrathink',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(32000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createChatOptions with thinkingLevel', () => {
|
||||
it('should include maxThinkingTokens for enabled thinkingLevel', async () => {
|
||||
const { createChatOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createChatOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'high',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(16000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSuggestionsOptions with thinkingLevel', () => {
|
||||
it('should include maxThinkingTokens for enabled thinkingLevel', async () => {
|
||||
const { createSuggestionsOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createSuggestionsOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'low',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(1024);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCustomOptions with thinkingLevel', () => {
|
||||
it('should include maxThinkingTokens for enabled thinkingLevel', async () => {
|
||||
const { createCustomOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createCustomOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'medium',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBe(10000);
|
||||
});
|
||||
|
||||
it('should not include maxThinkingTokens when thinkingLevel is "none"', async () => {
|
||||
const { createCustomOptions } = await import('@/lib/sdk-options.js');
|
||||
|
||||
const options = createCustomOptions({
|
||||
cwd: '/test/path',
|
||||
thinkingLevel: 'none',
|
||||
});
|
||||
|
||||
expect(options.maxThinkingTokens).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -597,6 +597,170 @@ describe('settings-service.ts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('phase model migration (v2 -> v3)', () => {
|
||||
it('should migrate string phase models to PhaseModelEntry format', async () => {
|
||||
// Simulate v2 format with string phase models
|
||||
const v2Settings = {
|
||||
version: 2,
|
||||
theme: 'dark',
|
||||
phaseModels: {
|
||||
enhancementModel: 'sonnet',
|
||||
fileDescriptionModel: 'haiku',
|
||||
imageDescriptionModel: 'haiku',
|
||||
validationModel: 'sonnet',
|
||||
specGenerationModel: 'opus',
|
||||
featureGenerationModel: 'sonnet',
|
||||
backlogPlanningModel: 'sonnet',
|
||||
projectAnalysisModel: 'sonnet',
|
||||
},
|
||||
};
|
||||
const settingsPath = path.join(testDataDir, 'settings.json');
|
||||
await fs.writeFile(settingsPath, JSON.stringify(v2Settings, null, 2));
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Verify all phase models are now PhaseModelEntry objects
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
|
||||
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'haiku' });
|
||||
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
|
||||
expect(settings.version).toBe(SETTINGS_VERSION);
|
||||
});
|
||||
|
||||
it('should preserve PhaseModelEntry objects during migration', async () => {
|
||||
// Simulate v3 format (already has PhaseModelEntry objects)
|
||||
const v3Settings = {
|
||||
version: 3,
|
||||
theme: 'dark',
|
||||
phaseModels: {
|
||||
enhancementModel: { model: 'sonnet', thinkingLevel: 'high' },
|
||||
fileDescriptionModel: { model: 'haiku' },
|
||||
imageDescriptionModel: { model: 'haiku', thinkingLevel: 'low' },
|
||||
validationModel: { model: 'sonnet' },
|
||||
specGenerationModel: { model: 'opus', thinkingLevel: 'ultrathink' },
|
||||
featureGenerationModel: { model: 'sonnet' },
|
||||
backlogPlanningModel: { model: 'sonnet', thinkingLevel: 'medium' },
|
||||
projectAnalysisModel: { model: 'sonnet' },
|
||||
},
|
||||
};
|
||||
const settingsPath = path.join(testDataDir, 'settings.json');
|
||||
await fs.writeFile(settingsPath, JSON.stringify(v3Settings, null, 2));
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Verify PhaseModelEntry objects are preserved with thinkingLevel
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'high',
|
||||
});
|
||||
expect(settings.phaseModels.specGenerationModel).toEqual({
|
||||
model: 'opus',
|
||||
thinkingLevel: 'ultrathink',
|
||||
});
|
||||
expect(settings.phaseModels.backlogPlanningModel).toEqual({
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'medium',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed format (some string, some object)', async () => {
|
||||
// Edge case: mixed format (shouldn't happen but handle gracefully)
|
||||
const mixedSettings = {
|
||||
version: 2,
|
||||
theme: 'dark',
|
||||
phaseModels: {
|
||||
enhancementModel: 'sonnet', // string
|
||||
fileDescriptionModel: { model: 'haiku', thinkingLevel: 'low' }, // object
|
||||
imageDescriptionModel: 'haiku', // string
|
||||
validationModel: { model: 'opus' }, // object without thinkingLevel
|
||||
specGenerationModel: 'opus',
|
||||
featureGenerationModel: 'sonnet',
|
||||
backlogPlanningModel: 'sonnet',
|
||||
projectAnalysisModel: 'sonnet',
|
||||
},
|
||||
};
|
||||
const settingsPath = path.join(testDataDir, 'settings.json');
|
||||
await fs.writeFile(settingsPath, JSON.stringify(mixedSettings, null, 2));
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Strings should be converted to objects
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
|
||||
expect(settings.phaseModels.imageDescriptionModel).toEqual({ model: 'haiku' });
|
||||
// Objects should be preserved
|
||||
expect(settings.phaseModels.fileDescriptionModel).toEqual({
|
||||
model: 'haiku',
|
||||
thinkingLevel: 'low',
|
||||
});
|
||||
expect(settings.phaseModels.validationModel).toEqual({ model: 'opus' });
|
||||
});
|
||||
|
||||
it('should migrate legacy enhancementModel/validationModel fields', async () => {
|
||||
// Simulate v1 format with legacy fields
|
||||
const v1Settings = {
|
||||
version: 1,
|
||||
theme: 'dark',
|
||||
enhancementModel: 'haiku',
|
||||
validationModel: 'opus',
|
||||
// No phaseModels object
|
||||
};
|
||||
const settingsPath = path.join(testDataDir, 'settings.json');
|
||||
await fs.writeFile(settingsPath, JSON.stringify(v1Settings, null, 2));
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Legacy fields should be migrated to phaseModels
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'haiku' });
|
||||
expect(settings.phaseModels.validationModel).toEqual({ model: 'opus' });
|
||||
// Other fields should use defaults
|
||||
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
|
||||
});
|
||||
|
||||
it('should use default phase models when none are configured', async () => {
|
||||
// Simulate empty settings
|
||||
const emptySettings = {
|
||||
version: 1,
|
||||
theme: 'dark',
|
||||
};
|
||||
const settingsPath = path.join(testDataDir, 'settings.json');
|
||||
await fs.writeFile(settingsPath, JSON.stringify(emptySettings, null, 2));
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Should use DEFAULT_PHASE_MODELS
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'sonnet' });
|
||||
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'haiku' });
|
||||
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'opus' });
|
||||
});
|
||||
|
||||
it('should deep merge phaseModels on update', async () => {
|
||||
// Create initial settings with some phase models
|
||||
await settingsService.updateGlobalSettings({
|
||||
phaseModels: {
|
||||
enhancementModel: { model: 'sonnet', thinkingLevel: 'high' },
|
||||
},
|
||||
});
|
||||
|
||||
// Update with a different phase model
|
||||
await settingsService.updateGlobalSettings({
|
||||
phaseModels: {
|
||||
specGenerationModel: { model: 'opus', thinkingLevel: 'ultrathink' },
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
|
||||
// Both should be preserved
|
||||
expect(settings.phaseModels.enhancementModel).toEqual({
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'high',
|
||||
});
|
||||
expect(settings.phaseModels.specGenerationModel).toEqual({
|
||||
model: 'opus',
|
||||
thinkingLevel: 'ultrathink',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('atomicWriteJson', () => {
|
||||
// Skip on Windows as chmod doesn't work the same way (CI runs on Linux)
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
|
||||
Reference in New Issue
Block a user