mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33: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:
@@ -13,4 +13,9 @@ export {
|
||||
} from '@automaker/types';
|
||||
|
||||
// Export resolver functions
|
||||
export { resolveModelString, getEffectiveModel } from './resolver.js';
|
||||
export {
|
||||
resolveModelString,
|
||||
getEffectiveModel,
|
||||
resolvePhaseModel,
|
||||
type ResolvedPhaseModel,
|
||||
} from './resolver.js';
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
PROVIDER_PREFIXES,
|
||||
isCursorModel,
|
||||
stripProviderPrefix,
|
||||
type PhaseModelEntry,
|
||||
type ThinkingLevel,
|
||||
} from '@automaker/types';
|
||||
|
||||
/**
|
||||
@@ -98,3 +100,72 @@ export function getEffectiveModel(
|
||||
): string {
|
||||
return resolveModelString(explicitModel || sessionModel, defaultModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of resolving a phase model entry
|
||||
*/
|
||||
export interface ResolvedPhaseModel {
|
||||
/** Resolved model string (full model ID) */
|
||||
model: string;
|
||||
/** Optional thinking level for extended thinking */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a phase model entry to a model string and thinking level
|
||||
*
|
||||
* Handles both legacy format (string) and new format (PhaseModelEntry object).
|
||||
* This centralizes the pattern used across phase model routes.
|
||||
*
|
||||
* @param phaseModel - Phase model entry (string or PhaseModelEntry object)
|
||||
* @param defaultModel - Fallback model if resolution fails
|
||||
* @returns Resolved model string and optional thinking level
|
||||
*
|
||||
* @remarks
|
||||
* - For Cursor models, `thinkingLevel` is returned as `undefined` since Cursor
|
||||
* handles thinking internally via model variants (e.g., 'claude-sonnet-4-thinking')
|
||||
* - Defensively handles null/undefined from corrupted settings JSON
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const phaseModel = settings?.phaseModels?.enhancementModel || DEFAULT_PHASE_MODELS.enhancementModel;
|
||||
* const { model, thinkingLevel } = resolvePhaseModel(phaseModel);
|
||||
* ```
|
||||
*/
|
||||
export function resolvePhaseModel(
|
||||
phaseModel: string | PhaseModelEntry | null | undefined,
|
||||
defaultModel: string = DEFAULT_MODELS.claude
|
||||
): ResolvedPhaseModel {
|
||||
console.log(
|
||||
`[ModelResolver] resolvePhaseModel called with:`,
|
||||
JSON.stringify(phaseModel),
|
||||
`type: ${typeof phaseModel}`
|
||||
);
|
||||
|
||||
// Handle null/undefined (defensive against corrupted JSON)
|
||||
if (!phaseModel) {
|
||||
console.log(`[ModelResolver] phaseModel is null/undefined, using default`);
|
||||
return {
|
||||
model: resolveModelString(undefined, defaultModel),
|
||||
thinkingLevel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle legacy string format
|
||||
if (typeof phaseModel === 'string') {
|
||||
console.log(`[ModelResolver] phaseModel is string format (legacy): "${phaseModel}"`);
|
||||
return {
|
||||
model: resolveModelString(phaseModel, defaultModel),
|
||||
thinkingLevel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle new PhaseModelEntry object format
|
||||
console.log(
|
||||
`[ModelResolver] phaseModel is object format: model="${phaseModel.model}", thinkingLevel="${phaseModel.thinkingLevel}"`
|
||||
);
|
||||
return {
|
||||
model: resolveModelString(phaseModel.model, defaultModel),
|
||||
thinkingLevel: phaseModel.thinkingLevel,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { resolveModelString, getEffectiveModel } from '../src/resolver';
|
||||
import { CLAUDE_MODEL_MAP, CURSOR_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
|
||||
import { resolveModelString, getEffectiveModel, resolvePhaseModel } from '../src/resolver';
|
||||
import {
|
||||
CLAUDE_MODEL_MAP,
|
||||
CURSOR_MODEL_MAP,
|
||||
DEFAULT_MODELS,
|
||||
type PhaseModelEntry,
|
||||
} from '@automaker/types';
|
||||
|
||||
describe('model-resolver', () => {
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
@@ -353,4 +358,182 @@ describe('model-resolver', () => {
|
||||
expect(DEFAULT_MODELS.claude).toContain('claude-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePhaseModel', () => {
|
||||
describe('with null/undefined input (defensive handling)', () => {
|
||||
it('should return default model when phaseModel is null', () => {
|
||||
const result = resolvePhaseModel(null);
|
||||
|
||||
expect(result.model).toBe(DEFAULT_MODELS.claude);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return default model when phaseModel is undefined', () => {
|
||||
const result = resolvePhaseModel(undefined);
|
||||
|
||||
expect(result.model).toBe(DEFAULT_MODELS.claude);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use custom default when phaseModel is null', () => {
|
||||
const customDefault = 'claude-opus-4-20241113';
|
||||
const result = resolvePhaseModel(null, customDefault);
|
||||
|
||||
expect(result.model).toBe(customDefault);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with legacy string format (v2 settings)', () => {
|
||||
it('should resolve Claude alias string', () => {
|
||||
const result = resolvePhaseModel('sonnet');
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve opus alias string', () => {
|
||||
const result = resolvePhaseModel('opus');
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve haiku alias string', () => {
|
||||
const result = resolvePhaseModel('haiku');
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.haiku);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should pass through full Claude model string', () => {
|
||||
const fullModel = 'claude-sonnet-4-20250514';
|
||||
const result = resolvePhaseModel(fullModel);
|
||||
|
||||
expect(result.model).toBe(fullModel);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle Cursor model string', () => {
|
||||
const result = resolvePhaseModel('cursor-auto');
|
||||
|
||||
expect(result.model).toBe('cursor-auto');
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with PhaseModelEntry object format (v3 settings)', () => {
|
||||
it('should resolve model from entry without thinkingLevel', () => {
|
||||
const entry: PhaseModelEntry = { model: 'sonnet' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve model and return thinkingLevel none', () => {
|
||||
const entry: PhaseModelEntry = { model: 'opus', thinkingLevel: 'none' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(result.thinkingLevel).toBe('none');
|
||||
});
|
||||
|
||||
it('should resolve model and return thinkingLevel low', () => {
|
||||
const entry: PhaseModelEntry = { model: 'sonnet', thinkingLevel: 'low' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
expect(result.thinkingLevel).toBe('low');
|
||||
});
|
||||
|
||||
it('should resolve model and return thinkingLevel medium', () => {
|
||||
const entry: PhaseModelEntry = { model: 'haiku', thinkingLevel: 'medium' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.haiku);
|
||||
expect(result.thinkingLevel).toBe('medium');
|
||||
});
|
||||
|
||||
it('should resolve model and return thinkingLevel high', () => {
|
||||
const entry: PhaseModelEntry = { model: 'opus', thinkingLevel: 'high' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(result.thinkingLevel).toBe('high');
|
||||
});
|
||||
|
||||
it('should resolve model and return thinkingLevel ultrathink', () => {
|
||||
const entry: PhaseModelEntry = { model: 'opus', thinkingLevel: 'ultrathink' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(result.thinkingLevel).toBe('ultrathink');
|
||||
});
|
||||
|
||||
it('should handle full Claude model string in entry', () => {
|
||||
const entry: PhaseModelEntry = {
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
thinkingLevel: 'high',
|
||||
};
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe('claude-opus-4-5-20251101');
|
||||
expect(result.thinkingLevel).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with Cursor models (thinkingLevel should be preserved but unused)', () => {
|
||||
it('should handle Cursor model entry without thinkingLevel', () => {
|
||||
const entry: PhaseModelEntry = { model: 'auto' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe('cursor-auto');
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve thinkingLevel even for Cursor models (caller handles)', () => {
|
||||
// Note: thinkingLevel is meaningless for Cursor but we don't filter it
|
||||
// The calling code should check isCursorModel() before using thinkingLevel
|
||||
const entry: PhaseModelEntry = { model: 'composer-1', thinkingLevel: 'high' };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe('cursor-composer-1');
|
||||
expect(result.thinkingLevel).toBe('high');
|
||||
});
|
||||
|
||||
it('should handle cursor-prefixed model in entry', () => {
|
||||
const entry: PhaseModelEntry = { model: 'cursor-gpt-4o' as any };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe('cursor-gpt-4o');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty string model in entry', () => {
|
||||
const entry: PhaseModelEntry = { model: '' as any };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(DEFAULT_MODELS.claude);
|
||||
expect(result.thinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle unknown model alias in entry', () => {
|
||||
const entry: PhaseModelEntry = { model: 'unknown-model' as any };
|
||||
const result = resolvePhaseModel(entry);
|
||||
|
||||
expect(result.model).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
|
||||
it('should use custom default for unknown model in entry', () => {
|
||||
const entry: PhaseModelEntry = { model: 'invalid' as any, thinkingLevel: 'high' };
|
||||
const customDefault = 'claude-haiku-4-5-20251001';
|
||||
const result = resolvePhaseModel(entry, customDefault);
|
||||
|
||||
expect(result.model).toBe(customDefault);
|
||||
expect(result.thinkingLevel).toBe('high');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user