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:
Shirone
2026-01-02 14:55:52 +01:00
parent 914734cff6
commit 81d300391d
27 changed files with 1134 additions and 101 deletions

View File

@@ -19,7 +19,13 @@ import type { Options } from '@anthropic-ai/claude-agent-sdk';
import os from 'os';
import path from 'path';
import { resolveModelString } from '@automaker/model-resolver';
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP, type McpServerConfig } from '@automaker/types';
import {
DEFAULT_MODELS,
CLAUDE_MODEL_MAP,
type McpServerConfig,
type ThinkingLevel,
getThinkingTokenBudget,
} from '@automaker/types';
import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
/**
@@ -317,6 +323,21 @@ function buildMcpOptions(config: CreateSdkOptionsConfig): McpPermissionOptions {
};
}
/**
* Build thinking options for SDK configuration.
* Converts ThinkingLevel to maxThinkingTokens for the Claude SDK.
*
* @param thinkingLevel - The thinking level to convert
* @returns Object with maxThinkingTokens if thinking is enabled
*/
function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial<Options> {
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
console.log(
`[SDK-Options] buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}`
);
return maxThinkingTokens ? { maxThinkingTokens } : {};
}
/**
* Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true:
@@ -409,6 +430,9 @@ export interface CreateSdkOptionsConfig {
/** Allow unrestricted tools when MCP servers are enabled */
mcpUnrestrictedTools?: boolean;
/** Extended thinking level for Claude models */
thinkingLevel?: ThinkingLevel;
}
// Re-export MCP types from @automaker/types for convenience
@@ -435,6 +459,9 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return {
...getBaseOptions(),
// Override permissionMode - spec generation only needs read-only tools
@@ -446,6 +473,7 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.specGeneration],
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
@@ -467,6 +495,9 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return {
...getBaseOptions(),
// Override permissionMode - feature generation only needs read-only tools
@@ -476,6 +507,7 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
};
}
@@ -496,6 +528,9 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
// Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return {
...getBaseOptions(),
model: getModelForUseCase('suggestions', config.model),
@@ -503,6 +538,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly],
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }),
};
@@ -531,6 +567,9 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// Check sandbox compatibility (auto-disables for cloud storage paths)
const sandboxCheck = checkSandboxCompatibility(config.cwd, config.enableSandboxMode);
@@ -550,6 +589,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
},
}),
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};
@@ -575,6 +615,9 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// Check sandbox compatibility (auto-disables for cloud storage paths)
const sandboxCheck = checkSandboxCompatibility(config.cwd, config.enableSandboxMode);
@@ -594,6 +637,7 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
},
}),
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};
@@ -621,6 +665,9 @@ export function createCustomOptions(
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// For custom options: use explicit allowedTools if provided, otherwise use preset based on MCP settings
const effectiveAllowedTools = config.allowedTools
? [...config.allowedTools]
@@ -638,6 +685,7 @@ export function createCustomOptions(
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
};

View File

@@ -8,6 +8,7 @@
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
import { BaseProvider } from './base-provider.js';
import { classifyError, getUserFriendlyErrorMessage } from '@automaker/utils';
import { getThinkingTokenBudget } from '@automaker/types';
import type {
ExecuteOptions,
ProviderMessage,
@@ -60,8 +61,12 @@ export class ClaudeProvider extends BaseProvider {
abortController,
conversationHistory,
sdkSessionId,
thinkingLevel,
} = options;
// Convert thinking level to token budget
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
// Build Claude SDK options
// MCP permission logic - determines how to handle tool permissions when MCP servers are configured.
// This logic mirrors buildMcpOptions() in sdk-options.ts but is applied here since
@@ -103,6 +108,8 @@ export class ClaudeProvider extends BaseProvider {
...(options.sandbox && { sandbox: options.sandbox }),
// Forward MCP servers configuration
...(options.mcpServers && { mcpServers: options.mcpServers }),
// Extended thinking configuration
...(maxThinkingTokens && { maxThinkingTokens }),
};
// Build prompt payload

View File

@@ -10,7 +10,7 @@ import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logAuthStatus } from './common.js';
@@ -109,9 +109,9 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const featureGenerationModel =
const phaseModelEntry =
settings?.phaseModels?.featureGenerationModel || DEFAULT_PHASE_MODELS.featureGenerationModel;
const model = resolveModelString(featureGenerationModel);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info('Using model:', model);
@@ -172,6 +172,7 @@ CRITICAL INSTRUCTIONS:
abortController,
autoLoadClaudeMd,
model,
thinkingLevel, // Pass thinking level for extended thinking
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2));

View File

@@ -17,7 +17,7 @@ import {
} from '../../lib/app-spec-format.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
import { extractJson } from '../../lib/json-extractor.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
@@ -102,9 +102,9 @@ ${getStructuredSpecPromptInstruction()}`;
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const specGenerationModel =
const phaseModelEntry =
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
const model = resolveModelString(specGenerationModel);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info('Using model:', model);
@@ -185,6 +185,7 @@ Your entire response should be valid JSON starting with { and ending with }. No
abortController,
autoLoadClaudeMd,
model,
thinkingLevel, // Pass thinking level for extended thinking
outputFormat: {
type: 'json_schema',
schema: specOutputSchema,

View File

@@ -7,7 +7,8 @@
import type { EventEmitter } from '../../lib/events.js';
import type { Feature, BacklogPlanResult, BacklogChange, DependencyUpdate } from '@automaker/types';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { DEFAULT_PHASE_MODELS, isCursorModel, type ThinkingLevel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { FeatureLoader } from '../../services/feature-loader.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { extractJsonWithArray } from '../../lib/json-extractor.js';
@@ -107,10 +108,14 @@ export async function generateBacklogPlan(
// Get the model to use from settings or provided override
let effectiveModel = model;
let thinkingLevel: ThinkingLevel | undefined;
if (!effectiveModel) {
const settings = await settingsService?.getGlobalSettings();
effectiveModel =
const phaseModelEntry =
settings?.phaseModels?.backlogPlanningModel || DEFAULT_PHASE_MODELS.backlogPlanningModel;
const resolved = resolvePhaseModel(phaseModelEntry);
effectiveModel = resolved.model;
thinkingLevel = resolved.thinkingLevel;
}
logger.info('[BacklogPlan] Using model:', effectiveModel);
@@ -154,6 +159,7 @@ ${userPrompt}`;
abortController,
settingSources: autoLoadClaudeMd ? ['user', 'project'] : undefined,
readOnly: true, // Plan generation only generates text, doesn't write files
thinkingLevel, // Pass thinking level for extended thinking
});
let responseText = '';

View File

@@ -15,7 +15,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { PathNotAllowedError } from '@automaker/platform';
import { resolveModelString } from '@automaker/model-resolver';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as secureFs from '../../../lib/secure-fs.js';
@@ -182,11 +182,16 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const fileDescriptionModel =
logger.info(
`[DescribeFile] Raw phaseModels from settings:`,
JSON.stringify(settings?.phaseModels, null, 2)
);
const phaseModelEntry =
settings?.phaseModels?.fileDescriptionModel || DEFAULT_PHASE_MODELS.fileDescriptionModel;
const model = resolveModelString(fileDescriptionModel);
logger.info(`[DescribeFile] fileDescriptionModel entry:`, JSON.stringify(phaseModelEntry));
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.debug(`[DescribeFile] Using model: ${model}`);
logger.info(`[DescribeFile] Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
let description: string;
@@ -231,6 +236,7 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
thinkingLevel, // Pass thinking level for extended thinking
});
const promptGenerator = (async function* () {

View File

@@ -15,7 +15,7 @@ import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger, readImageAsBase64 } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as secureFs from '../../../lib/secure-fs.js';
@@ -342,9 +342,9 @@ export function createDescribeImageHandler(
// Get model from phase settings
const settings = await settingsService?.getGlobalSettings();
const imageDescriptionModel =
const phaseModelEntry =
settings?.phaseModels?.imageDescriptionModel || DEFAULT_PHASE_MODELS.imageDescriptionModel;
const model = resolveModelString(imageDescriptionModel);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info(`[${requestId}] Using model: ${model}`);
@@ -395,6 +395,7 @@ export function createDescribeImageHandler(
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
thinkingLevel, // Pass thinking level for extended thinking
});
logger.info(

View File

@@ -17,7 +17,8 @@ import type {
GitHubComment,
LinkedPRInfo,
} from '@automaker/types';
import { isCursorModel } from '@automaker/types';
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
import { extractJson } from '../../../lib/json-extractor.js';
import { writeValidation } from '../../../lib/validation-storage.js';
@@ -174,6 +175,12 @@ ${prompt}`;
'[ValidateIssue]'
);
// Get thinkingLevel from phase model settings (the model comes from request, but thinkingLevel from settings)
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
const { thinkingLevel } = resolvePhaseModel(phaseModelEntry);
// Create SDK options with structured output and abort controller
const options = createSuggestionsOptions({
cwd: projectPath,
@@ -181,6 +188,7 @@ ${prompt}`;
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
abortController,
autoLoadClaudeMd,
thinkingLevel,
outputFormat: {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,

View File

@@ -9,7 +9,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSuggestionsOptions } from '../../lib/sdk-options.js';
import { extractJsonWithArray } from '../../lib/json-extractor.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
@@ -173,9 +173,9 @@ The response will be automatically formatted as structured JSON.`;
// Get model from phase settings (Feature Enhancement = enhancementModel)
const settings = await settingsService?.getGlobalSettings();
const enhancementModel =
const phaseModelEntry =
settings?.phaseModels?.enhancementModel || DEFAULT_PHASE_MODELS.enhancementModel;
const model = resolveModelString(enhancementModel);
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.info('[Suggestions] Using model:', model);
@@ -247,6 +247,7 @@ Your entire response should be valid JSON starting with { and ending with }. No
abortController,
autoLoadClaudeMd,
model, // Pass the model from settings
thinkingLevel, // Pass thinking level for extended thinking
outputFormat: {
type: 'json_schema',
schema: suggestionsSchema,

View File

@@ -16,6 +16,8 @@ import type {
ModelProvider,
PipelineConfig,
PipelineStep,
ThinkingLevel,
PlanningMode,
} from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import {
@@ -24,7 +26,7 @@ import {
classifyError,
loadContextFiles,
} from '@automaker/utils';
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
import { resolveModelString, resolvePhaseModel, DEFAULT_MODELS } from '@automaker/model-resolver';
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
import { getFeatureDir, getAutomakerDir, getFeaturesDir } from '@automaker/platform';
import { exec } from 'child_process';
@@ -51,8 +53,7 @@ import {
const execAsync = promisify(exec);
// Planning mode types for spec-driven development
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
// PlanningMode type is imported from @automaker/types
interface ParsedTask {
id: string; // e.g., "T001"
@@ -576,6 +577,7 @@ export class AutoModeService {
requirePlanApproval: feature.requirePlanApproval,
systemPrompt: contextFilesPrompt || undefined,
autoLoadClaudeMd,
thinkingLevel: feature.thinkingLevel,
}
);
@@ -734,6 +736,7 @@ export class AutoModeService {
previousContent: previousContext,
systemPrompt: contextFilesPrompt || undefined,
autoLoadClaudeMd,
thinkingLevel: feature.thinkingLevel,
}
);
@@ -1049,6 +1052,7 @@ Address the follow-up instructions above. Review the previous work and make the
previousContent: previousContext || undefined,
systemPrompt: contextFilesPrompt || undefined,
autoLoadClaudeMd,
thinkingLevel: feature?.thinkingLevel,
}
);
@@ -1278,9 +1282,10 @@ Format your response as a structured markdown document.`;
try {
// Get model from phase settings
const settings = await this.settingsService?.getGlobalSettings();
const projectAnalysisModel =
const phaseModelEntry =
settings?.phaseModels?.projectAnalysisModel || DEFAULT_PHASE_MODELS.projectAnalysisModel;
const analysisModel = resolveModelString(projectAnalysisModel, DEFAULT_MODELS.claude);
const { model: analysisModel, thinkingLevel: analysisThinkingLevel } =
resolvePhaseModel(phaseModelEntry);
console.log('[AutoMode] Using model for project analysis:', analysisModel);
const provider = ProviderFactory.getProviderForModel(analysisModel);
@@ -1300,6 +1305,7 @@ Format your response as a structured markdown document.`;
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
autoLoadClaudeMd,
thinkingLevel: analysisThinkingLevel,
});
const options: ExecuteOptions = {
@@ -1311,6 +1317,7 @@ Format your response as a structured markdown document.`;
abortController,
settingSources: sdkOptions.settingSources,
sandbox: sdkOptions.sandbox, // Pass sandbox configuration
thinkingLevel: analysisThinkingLevel, // Pass thinking level
};
const stream = provider.executeQuery(options);
@@ -1954,6 +1961,7 @@ This helps parse your summary correctly in the output logs.`;
previousContent?: string;
systemPrompt?: string;
autoLoadClaudeMd?: boolean;
thinkingLevel?: ThinkingLevel;
}
): Promise<void> {
const finalProjectPath = options?.projectPath || projectPath;
@@ -2052,6 +2060,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools,
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools,
thinkingLevel: options?.thinkingLevel,
});
// Extract model, maxTurns, and allowedTools from SDK options
@@ -2096,6 +2105,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
mcpAutoApproveTools: mcpPermissions.mcpAutoApproveTools, // Pass MCP auto-approve setting
mcpUnrestrictedTools: mcpPermissions.mcpUnrestrictedTools, // Pass MCP unrestricted tools setting
thinkingLevel: options?.thinkingLevel, // Pass thinking level for extended thinking
};
// Execute via provider

View File

@@ -28,6 +28,7 @@ import type {
BoardBackgroundSettings,
WorktreeInfo,
PhaseModelConfig,
PhaseModelEntry,
} from '../types/settings.js';
import {
DEFAULT_GLOBAL_SETTINGS,
@@ -157,10 +158,23 @@ export class SettingsService {
if (storedVersion < 2) {
logger.info('Migrating settings from v1 to v2: disabling sandbox mode');
result.enableSandboxMode = false;
result.version = SETTINGS_VERSION;
needsSave = true;
}
// Migration v2 -> v3: Convert string phase models to PhaseModelEntry objects
// Note: migratePhaseModels() handles the actual conversion for both v1 and v2 formats
if (storedVersion < 3) {
logger.info(
`Migrating settings from v${storedVersion} to v3: converting phase models to PhaseModelEntry format`
);
needsSave = true;
}
// Update version if any migration occurred
if (needsSave) {
result.version = SETTINGS_VERSION;
}
// Save migrated settings if needed
if (needsSave) {
try {
@@ -179,6 +193,7 @@ export class SettingsService {
* Migrate legacy enhancementModel/validationModel fields to phaseModels structure
*
* Handles backwards compatibility for settings created before phaseModels existed.
* Also handles migration from string phase models (v2) to PhaseModelEntry objects (v3).
* Legacy fields take precedence over defaults but phaseModels takes precedence over legacy.
*
* @param settings - Raw settings from file
@@ -190,26 +205,51 @@ export class SettingsService {
// If phaseModels exists, use it (with defaults for any missing fields)
if (settings.phaseModels) {
return {
...DEFAULT_PHASE_MODELS,
...settings.phaseModels,
};
// Merge with defaults and convert any string values to PhaseModelEntry
const merged: PhaseModelConfig = { ...DEFAULT_PHASE_MODELS };
for (const key of Object.keys(settings.phaseModels) as Array<keyof PhaseModelConfig>) {
const value = settings.phaseModels[key];
if (value !== undefined) {
// Convert string to PhaseModelEntry if needed (v2 -> v3 migration)
merged[key] = this.toPhaseModelEntry(value);
}
}
return merged;
}
// Migrate legacy fields if phaseModels doesn't exist
// These were the only two legacy fields that existed
if (settings.enhancementModel) {
result.enhancementModel = settings.enhancementModel;
result.enhancementModel = this.toPhaseModelEntry(settings.enhancementModel);
logger.debug(`Migrated legacy enhancementModel: ${settings.enhancementModel}`);
}
if (settings.validationModel) {
result.validationModel = settings.validationModel;
result.validationModel = this.toPhaseModelEntry(settings.validationModel);
logger.debug(`Migrated legacy validationModel: ${settings.validationModel}`);
}
return result;
}
/**
* Convert a phase model value to PhaseModelEntry format
*
* Handles migration from string format (v2) to object format (v3).
* - String values like 'sonnet' become { model: 'sonnet' }
* - Object values are returned as-is (with type assertion)
*
* @param value - Phase model value (string or PhaseModelEntry)
* @returns PhaseModelEntry object
*/
private toPhaseModelEntry(value: string | PhaseModelEntry): PhaseModelEntry {
if (typeof value === 'string') {
// v2 format: just a model string
return { model: value as PhaseModelEntry['model'] };
}
// v3 format: already a PhaseModelEntry object
return value;
}
/**
* Update global settings with partial changes
*

View File

@@ -24,6 +24,7 @@ export type {
ProjectSettings,
PhaseModelConfig,
PhaseModelKey,
PhaseModelEntry,
} from '@automaker/types';
export {