mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -24,6 +24,7 @@ export type {
|
||||
ProjectSettings,
|
||||
PhaseModelConfig,
|
||||
PhaseModelKey,
|
||||
PhaseModelEntry,
|
||||
} from '@automaker/types';
|
||||
|
||||
export {
|
||||
|
||||
@@ -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')(
|
||||
|
||||
@@ -4,10 +4,22 @@ import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||
import { PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string
|
||||
*/
|
||||
function extractModel(entry: PhaseModelEntry | string | null): ModelAlias | CursorModelId | null {
|
||||
if (!entry) return null;
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
export interface ModelOverrideTriggerProps {
|
||||
/** Current effective model (from global settings or explicit override) */
|
||||
currentModel: ModelAlias | CursorModelId;
|
||||
@@ -53,8 +65,8 @@ export function ModelOverrideTrigger({
|
||||
const [open, setOpen] = useState(false);
|
||||
const { phaseModels, enabledCursorModels } = useAppStore();
|
||||
|
||||
// Get the global default for this phase
|
||||
const globalDefault = phase ? phaseModels[phase] : null;
|
||||
// Get the global default for this phase (extract model string from PhaseModelEntry)
|
||||
const globalDefault = phase ? extractModel(phaseModels[phase]) : null;
|
||||
|
||||
// Filter Cursor models to only show enabled ones
|
||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||
|
||||
export interface UseModelOverrideOptions {
|
||||
/** Which phase this override is for */
|
||||
@@ -24,6 +24,16 @@ export interface UseModelOverrideResult {
|
||||
override: ModelAlias | CursorModelId | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string
|
||||
*/
|
||||
function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId {
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing model overrides per phase
|
||||
*
|
||||
@@ -55,7 +65,8 @@ export function useModelOverride({
|
||||
const { phaseModels } = useAppStore();
|
||||
const [override, setOverrideState] = useState<ModelAlias | CursorModelId | null>(initialOverride);
|
||||
|
||||
const globalDefault = phaseModels[phase];
|
||||
// Extract model string from PhaseModelEntry (handles both old string format and new object format)
|
||||
const globalDefault = extractModel(phaseModels[phase]);
|
||||
|
||||
const effectiveModel = useMemo(() => {
|
||||
return override ?? globalDefault;
|
||||
|
||||
@@ -23,10 +23,26 @@ import {
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { BacklogPlanResult, BacklogChange, ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import type {
|
||||
BacklogPlanResult,
|
||||
BacklogChange,
|
||||
ModelAlias,
|
||||
CursorModelId,
|
||||
PhaseModelEntry,
|
||||
} from '@automaker/types';
|
||||
import { ModelOverrideTrigger } from '@/components/shared/model-override-trigger';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string
|
||||
*/
|
||||
function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId {
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
interface BacklogPlanDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -88,8 +104,8 @@ export function BacklogPlanDialog({
|
||||
// Start generation in background
|
||||
setIsGeneratingPlan(true);
|
||||
|
||||
// Use model override if set, otherwise use global default
|
||||
const effectiveModel = modelOverride || phaseModels.backlogPlanningModel;
|
||||
// Use model override if set, otherwise use global default (extract model string from PhaseModelEntry)
|
||||
const effectiveModel = modelOverride || extractModel(phaseModels.backlogPlanningModel);
|
||||
const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel);
|
||||
if (!result.success) {
|
||||
setIsGeneratingPlan(false);
|
||||
@@ -365,8 +381,8 @@ export function BacklogPlanDialog({
|
||||
}
|
||||
};
|
||||
|
||||
// Get effective model (override or global default)
|
||||
const effectiveModel = modelOverride || phaseModels.backlogPlanningModel;
|
||||
// Get effective model (override or global default) - extract model string from PhaseModelEntry
|
||||
const effectiveModel = modelOverride || extractModel(phaseModels.backlogPlanningModel);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
|
||||
@@ -7,11 +7,24 @@ import {
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
} from '@/lib/electron';
|
||||
import type { LinkedPRInfo } from '@automaker/types';
|
||||
import type { LinkedPRInfo, PhaseModelEntry, ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { isValidationStale } from '../utils';
|
||||
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string (handles both formats)
|
||||
*/
|
||||
function extractModel(
|
||||
entry: PhaseModelEntry | string | undefined
|
||||
): ModelAlias | CursorModelId | undefined {
|
||||
if (!entry) return undefined;
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
interface UseIssueValidationOptions {
|
||||
selectedIssue: GitHubIssue | null;
|
||||
showValidationDialog: boolean;
|
||||
@@ -244,7 +257,8 @@ export function useIssueValidation({
|
||||
});
|
||||
|
||||
// Use provided model override or fall back to phaseModels.validationModel
|
||||
const modelToUse = model || phaseModels.validationModel;
|
||||
// Extract model string from PhaseModelEntry (handles both old string format and new object format)
|
||||
const modelToUse = model || extractModel(phaseModels.validationModel);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { ModelAlias, CursorModelId, GroupedModel } from '@automaker/types';
|
||||
import type {
|
||||
ModelAlias,
|
||||
CursorModelId,
|
||||
GroupedModel,
|
||||
PhaseModelEntry,
|
||||
ThinkingLevel,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
stripProviderPrefix,
|
||||
CURSOR_MODEL_GROUPS,
|
||||
STANDALONE_CURSOR_MODELS,
|
||||
getModelGroup,
|
||||
isGroupSelected,
|
||||
getSelectedVariant,
|
||||
isCursorModel,
|
||||
} from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
import {
|
||||
CLAUDE_MODELS,
|
||||
CURSOR_MODELS,
|
||||
THINKING_LEVELS,
|
||||
THINKING_LEVEL_LABELS,
|
||||
} from '@/components/views/board-view/shared/model-constants';
|
||||
import { Check, ChevronsUpDown, Star, Brain, Sparkles, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -27,8 +38,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
interface PhaseModelSelectorProps {
|
||||
label: string;
|
||||
description: string;
|
||||
value: ModelAlias | CursorModelId;
|
||||
onChange: (model: ModelAlias | CursorModelId) => void;
|
||||
value: PhaseModelEntry;
|
||||
onChange: (entry: PhaseModelEntry) => void;
|
||||
}
|
||||
|
||||
export function PhaseModelSelector({
|
||||
@@ -39,10 +50,16 @@ export function PhaseModelSelector({
|
||||
}: PhaseModelSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [expandedGroup, setExpandedGroup] = React.useState<string | null>(null);
|
||||
const [expandedClaudeModel, setExpandedClaudeModel] = React.useState<ModelAlias | null>(null);
|
||||
const commandListRef = React.useRef<HTMLDivElement>(null);
|
||||
const expandedTriggerRef = React.useRef<HTMLDivElement>(null);
|
||||
const expandedClaudeTriggerRef = React.useRef<HTMLDivElement>(null);
|
||||
const { enabledCursorModels, favoriteModels, toggleFavoriteModel } = useAppStore();
|
||||
|
||||
// Extract model and thinking level from value
|
||||
const selectedModel = value.model;
|
||||
const selectedThinkingLevel = value.thinkingLevel || 'none';
|
||||
|
||||
// Close expanded group when trigger scrolls out of view
|
||||
React.useEffect(() => {
|
||||
const triggerElement = expandedTriggerRef.current;
|
||||
@@ -66,6 +83,29 @@ export function PhaseModelSelector({
|
||||
return () => observer.disconnect();
|
||||
}, [expandedGroup]);
|
||||
|
||||
// Close expanded Claude model popover when trigger scrolls out of view
|
||||
React.useEffect(() => {
|
||||
const triggerElement = expandedClaudeTriggerRef.current;
|
||||
const listElement = commandListRef.current;
|
||||
if (!triggerElement || !listElement || !expandedClaudeModel) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry.isIntersecting) {
|
||||
setExpandedClaudeModel(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
root: listElement,
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(triggerElement);
|
||||
return () => observer.disconnect();
|
||||
}, [expandedClaudeModel]);
|
||||
|
||||
// Filter Cursor models to only show enabled ones
|
||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
@@ -74,18 +114,31 @@ export function PhaseModelSelector({
|
||||
|
||||
// Helper to find current selected model details
|
||||
const currentModel = React.useMemo(() => {
|
||||
const claudeModel = CLAUDE_MODELS.find((m) => m.id === value);
|
||||
if (claudeModel) return { ...claudeModel, icon: Brain };
|
||||
const claudeModel = CLAUDE_MODELS.find((m) => m.id === selectedModel);
|
||||
if (claudeModel) {
|
||||
// Add thinking level to label if not 'none'
|
||||
const thinkingLabel =
|
||||
selectedThinkingLevel !== 'none'
|
||||
? ` (${THINKING_LEVEL_LABELS[selectedThinkingLevel]} Thinking)`
|
||||
: '';
|
||||
return {
|
||||
...claudeModel,
|
||||
label: `${claudeModel.label}${thinkingLabel}`,
|
||||
icon: Brain,
|
||||
};
|
||||
}
|
||||
|
||||
const cursorModel = availableCursorModels.find((m) => stripProviderPrefix(m.id) === value);
|
||||
const cursorModel = availableCursorModels.find(
|
||||
(m) => stripProviderPrefix(m.id) === selectedModel
|
||||
);
|
||||
if (cursorModel) return { ...cursorModel, icon: Sparkles };
|
||||
|
||||
// Check if value is part of a grouped model
|
||||
const group = getModelGroup(value as CursorModelId);
|
||||
// Check if selectedModel is part of a grouped model
|
||||
const group = getModelGroup(selectedModel as CursorModelId);
|
||||
if (group) {
|
||||
const variant = getSelectedVariant(group, value as CursorModelId);
|
||||
const variant = getSelectedVariant(group, selectedModel as CursorModelId);
|
||||
return {
|
||||
id: value,
|
||||
id: selectedModel,
|
||||
label: `${group.label} (${variant?.label || 'Unknown'})`,
|
||||
description: group.description,
|
||||
provider: 'cursor' as const,
|
||||
@@ -94,7 +147,7 @@ export function PhaseModelSelector({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [value, availableCursorModels]);
|
||||
}, [selectedModel, selectedThinkingLevel, availableCursorModels]);
|
||||
|
||||
// Compute grouped vs standalone Cursor models
|
||||
const { groupedModels, standaloneCursorModels } = React.useMemo(() => {
|
||||
@@ -156,26 +209,24 @@ export function PhaseModelSelector({
|
||||
return { favorites: favs, claude: cModels, cursor: curModels };
|
||||
}, [favoriteModels, availableCursorModels]);
|
||||
|
||||
const renderModelItem = (model: (typeof CLAUDE_MODELS)[0], type: 'claude' | 'cursor') => {
|
||||
const isClaude = type === 'claude';
|
||||
// For Claude, value is model.id. For Cursor, it's stripped ID.
|
||||
const modelValue = isClaude ? model.id : stripProviderPrefix(model.id);
|
||||
const isSelected = value === modelValue;
|
||||
// Render Cursor model item (no thinking level needed)
|
||||
const renderCursorModelItem = (model: (typeof CURSOR_MODELS)[0]) => {
|
||||
const modelValue = stripProviderPrefix(model.id);
|
||||
const isSelected = selectedModel === modelValue;
|
||||
const isFavorite = favoriteModels.includes(model.id);
|
||||
const Icon = isClaude ? Brain : Sparkles;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.label}
|
||||
onSelect={() => {
|
||||
onChange(modelValue as ModelAlias | CursorModelId);
|
||||
onChange({ model: modelValue as CursorModelId });
|
||||
setOpen(false);
|
||||
}}
|
||||
className="group flex items-center justify-between py-2"
|
||||
>
|
||||
<div className="flex items-center gap-3 overflow-hidden">
|
||||
<Icon
|
||||
<Sparkles
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0',
|
||||
isSelected ? 'text-primary' : 'text-muted-foreground'
|
||||
@@ -212,10 +263,138 @@ export function PhaseModelSelector({
|
||||
);
|
||||
};
|
||||
|
||||
// Render Claude model item with secondary popover for thinking level
|
||||
const renderClaudeModelItem = (model: (typeof CLAUDE_MODELS)[0]) => {
|
||||
const isSelected = selectedModel === model.id;
|
||||
const isFavorite = favoriteModels.includes(model.id);
|
||||
const isExpanded = expandedClaudeModel === model.id;
|
||||
const currentThinking = isSelected ? selectedThinkingLevel : 'none';
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.label}
|
||||
onSelect={() => setExpandedClaudeModel(isExpanded ? null : (model.id as ModelAlias))}
|
||||
className="p-0 data-[selected=true]:bg-transparent"
|
||||
>
|
||||
<Popover
|
||||
open={isExpanded}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setExpandedClaudeModel(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
ref={isExpanded ? expandedClaudeTriggerRef : undefined}
|
||||
className={cn(
|
||||
'w-full group flex items-center justify-between py-2 px-2 rounded-sm cursor-pointer',
|
||||
'hover:bg-accent',
|
||||
isExpanded && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3 overflow-hidden">
|
||||
<Brain
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0',
|
||||
isSelected ? 'text-primary' : 'text-muted-foreground'
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col truncate">
|
||||
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{isSelected && currentThinking !== 'none'
|
||||
? `Thinking: ${THINKING_LEVEL_LABELS[currentThinking]}`
|
||||
: model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
|
||||
isFavorite
|
||||
? 'text-yellow-500 opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavoriteModel(model.id);
|
||||
}}
|
||||
>
|
||||
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
|
||||
</Button>
|
||||
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
isExpanded && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="center"
|
||||
avoidCollisions={false}
|
||||
className="w-[220px] p-1"
|
||||
sideOffset={8}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
|
||||
Thinking Level
|
||||
</div>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
onClick={() => {
|
||||
onChange({
|
||||
model: model.id as ModelAlias,
|
||||
thinkingLevel: level,
|
||||
});
|
||||
setExpandedClaudeModel(null);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-2 py-2 rounded-sm text-sm',
|
||||
'hover:bg-accent cursor-pointer transition-colors',
|
||||
isSelected && currentThinking === level && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">{THINKING_LEVEL_LABELS[level]}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{level === 'none' && 'No extended thinking'}
|
||||
{level === 'low' && 'Light reasoning (1k tokens)'}
|
||||
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
|
||||
{level === 'high' && 'Deep reasoning (16k tokens)'}
|
||||
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
|
||||
</span>
|
||||
</div>
|
||||
{isSelected && currentThinking === level && (
|
||||
<Check className="h-3.5 w-3.5 text-primary" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
|
||||
// Render a grouped model with secondary popover for variant selection
|
||||
const renderGroupedModelItem = (group: GroupedModel) => {
|
||||
const groupIsSelected = isGroupSelected(group, value as CursorModelId);
|
||||
const selectedVariant = getSelectedVariant(group, value as CursorModelId);
|
||||
const groupIsSelected = isGroupSelected(group, selectedModel as CursorModelId);
|
||||
const selectedVariant = getSelectedVariant(group, selectedModel as CursorModelId);
|
||||
const isExpanded = expandedGroup === group.baseId;
|
||||
|
||||
const variantTypeLabel =
|
||||
@@ -293,14 +472,14 @@ export function PhaseModelSelector({
|
||||
<button
|
||||
key={variant.id}
|
||||
onClick={() => {
|
||||
onChange(variant.id);
|
||||
onChange({ model: variant.id });
|
||||
setExpandedGroup(null);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'w-full flex items-center justify-between px-2 py-2 rounded-sm text-sm',
|
||||
'hover:bg-accent cursor-pointer transition-colors',
|
||||
value === variant.id && 'bg-accent text-accent-foreground'
|
||||
selectedModel === variant.id && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -315,7 +494,7 @@ export function PhaseModelSelector({
|
||||
{variant.badge}
|
||||
</span>
|
||||
)}
|
||||
{value === variant.id && <Check className="h-3.5 w-3.5 text-primary" />}
|
||||
{selectedModel === variant.id && <Check className="h-3.5 w-3.5 text-primary" />}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -388,11 +567,11 @@ export function PhaseModelSelector({
|
||||
return renderGroupedModelItem(filteredGroup);
|
||||
}
|
||||
}
|
||||
// Standalone Cursor model
|
||||
return renderCursorModelItem(model);
|
||||
}
|
||||
return renderModelItem(
|
||||
model,
|
||||
model.provider === 'claude' ? 'claude' : 'cursor'
|
||||
);
|
||||
// Claude model
|
||||
return renderClaudeModelItem(model);
|
||||
});
|
||||
})()}
|
||||
</CommandGroup>
|
||||
@@ -402,7 +581,7 @@ export function PhaseModelSelector({
|
||||
|
||||
{claude.length > 0 && (
|
||||
<CommandGroup heading="Claude Models">
|
||||
{claude.map((model) => renderModelItem(model, 'claude'))}
|
||||
{claude.map((model) => renderClaudeModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
@@ -411,7 +590,7 @@ export function PhaseModelSelector({
|
||||
{/* Grouped models with secondary popover */}
|
||||
{groupedModels.map((group) => renderGroupedModelItem(group))}
|
||||
{/* Standalone models */}
|
||||
{standaloneCursorModels.map((model) => renderModelItem(model, 'cursor'))}
|
||||
{standaloneCursorModels.map((model) => renderCursorModelItem(model))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
CursorModelId,
|
||||
PhaseModelConfig,
|
||||
PhaseModelKey,
|
||||
PhaseModelEntry,
|
||||
MCPServerConfig,
|
||||
FeatureStatusWithPipeline,
|
||||
PipelineConfig,
|
||||
@@ -786,7 +787,7 @@ export interface AppActions {
|
||||
setValidationModel: (model: ModelAlias) => void;
|
||||
|
||||
// Phase Model actions
|
||||
setPhaseModel: (phase: PhaseModelKey, model: ModelAlias | CursorModelId) => Promise<void>;
|
||||
setPhaseModel: (phase: PhaseModelKey, entry: PhaseModelEntry) => Promise<void>;
|
||||
setPhaseModels: (models: Partial<PhaseModelConfig>) => Promise<void>;
|
||||
resetPhaseModels: () => Promise<void>;
|
||||
toggleFavoriteModel: (modelId: string) => void;
|
||||
@@ -1652,11 +1653,11 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
setValidationModel: (model) => set({ validationModel: model }),
|
||||
|
||||
// Phase Model actions
|
||||
setPhaseModel: async (phase, model) => {
|
||||
setPhaseModel: async (phase, entry) => {
|
||||
set((state) => ({
|
||||
phaseModels: {
|
||||
...state.phaseModels,
|
||||
[phase]: model,
|
||||
[phase]: entry,
|
||||
},
|
||||
}));
|
||||
// Sync to server settings file
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Feature types for AutoMaker feature management
|
||||
*/
|
||||
|
||||
import type { PlanningMode } from './settings.js';
|
||||
import type { PlanningMode, ThinkingLevel } from './settings.js';
|
||||
|
||||
export interface FeatureImagePath {
|
||||
id: string;
|
||||
@@ -38,7 +38,7 @@ export interface Feature {
|
||||
// Branch info - worktree path is derived at runtime from branchName
|
||||
branchName?: string; // Name of the feature branch (undefined = use current worktree)
|
||||
skipTests?: boolean;
|
||||
thinkingLevel?: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
planningMode?: PlanningMode;
|
||||
requirePlanApproval?: boolean;
|
||||
planSpec?: {
|
||||
|
||||
@@ -71,6 +71,7 @@ export type {
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
PhaseModelEntry,
|
||||
PhaseModelConfig,
|
||||
PhaseModelKey,
|
||||
KeyboardShortcuts,
|
||||
@@ -95,8 +96,10 @@ export {
|
||||
SETTINGS_VERSION,
|
||||
CREDENTIALS_VERSION,
|
||||
PROJECT_SETTINGS_VERSION,
|
||||
THINKING_TOKEN_BUDGET,
|
||||
profileHasThinking,
|
||||
getProfileModelString,
|
||||
getThinkingTokenBudget,
|
||||
} from './settings.js';
|
||||
|
||||
// Model display constants
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Shared types for AI model providers
|
||||
*/
|
||||
|
||||
import type { ThinkingLevel } from './settings.js';
|
||||
|
||||
/**
|
||||
* Configuration for a provider instance
|
||||
*/
|
||||
@@ -84,6 +86,12 @@ export interface ExecuteOptions {
|
||||
* Default: false (allows edits)
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Extended thinking level for Claude models.
|
||||
* Controls the amount of reasoning tokens allocated.
|
||||
* Only applies to Claude models; Cursor models handle thinking internally.
|
||||
*/
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,9 +70,46 @@ export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
|
||||
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
||||
|
||||
/**
|
||||
* Thinking token budget mapping based on Claude SDK documentation.
|
||||
* @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
|
||||
*
|
||||
* - Minimum budget: 1,024 tokens
|
||||
* - Complex tasks starting point: 16,000+ tokens
|
||||
* - Above 32,000: Risk of timeouts (batch processing recommended)
|
||||
*/
|
||||
export const THINKING_TOKEN_BUDGET: Record<ThinkingLevel, number | undefined> = {
|
||||
none: undefined, // Thinking disabled
|
||||
low: 1024, // Minimum per docs
|
||||
medium: 10000, // Light reasoning
|
||||
high: 16000, // Complex tasks (recommended starting point)
|
||||
ultrathink: 32000, // Maximum safe (above this risks timeouts)
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert thinking level to SDK maxThinkingTokens value
|
||||
*/
|
||||
export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number | undefined {
|
||||
if (!level || level === 'none') return undefined;
|
||||
return THINKING_TOKEN_BUDGET[level];
|
||||
}
|
||||
|
||||
/** ModelProvider - AI model provider for credentials and API key management */
|
||||
export type ModelProvider = 'claude' | 'cursor';
|
||||
|
||||
/**
|
||||
* PhaseModelEntry - Configuration for a single phase model
|
||||
*
|
||||
* Encapsulates both the model selection and optional thinking level
|
||||
* for Claude models. Cursor models handle thinking internally.
|
||||
*/
|
||||
export interface PhaseModelEntry {
|
||||
/** The model to use (Claude alias or Cursor model ID) */
|
||||
model: ModelAlias | CursorModelId;
|
||||
/** Extended thinking level (only applies to Claude models, defaults to 'none') */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* PhaseModelConfig - Configuration for AI models used in different application phases
|
||||
*
|
||||
@@ -83,25 +120,25 @@ export type ModelProvider = 'claude' | 'cursor';
|
||||
export interface PhaseModelConfig {
|
||||
// Quick tasks - recommend fast/cheap models (Haiku, Cursor auto)
|
||||
/** Model for enhancing feature names and descriptions */
|
||||
enhancementModel: ModelAlias | CursorModelId;
|
||||
enhancementModel: PhaseModelEntry;
|
||||
/** Model for generating file context descriptions */
|
||||
fileDescriptionModel: ModelAlias | CursorModelId;
|
||||
fileDescriptionModel: PhaseModelEntry;
|
||||
/** Model for analyzing and describing context images */
|
||||
imageDescriptionModel: ModelAlias | CursorModelId;
|
||||
imageDescriptionModel: PhaseModelEntry;
|
||||
|
||||
// Validation tasks - recommend smart models (Sonnet, Opus)
|
||||
/** Model for validating and improving GitHub issues */
|
||||
validationModel: ModelAlias | CursorModelId;
|
||||
validationModel: PhaseModelEntry;
|
||||
|
||||
// Generation tasks - recommend powerful models (Opus, Sonnet)
|
||||
/** Model for generating full application specifications */
|
||||
specGenerationModel: ModelAlias | CursorModelId;
|
||||
specGenerationModel: PhaseModelEntry;
|
||||
/** Model for creating features from specifications */
|
||||
featureGenerationModel: ModelAlias | CursorModelId;
|
||||
featureGenerationModel: PhaseModelEntry;
|
||||
/** Model for reorganizing and prioritizing backlog */
|
||||
backlogPlanningModel: ModelAlias | CursorModelId;
|
||||
backlogPlanningModel: PhaseModelEntry;
|
||||
/** Model for analyzing project structure */
|
||||
projectAnalysisModel: ModelAlias | CursorModelId;
|
||||
projectAnalysisModel: PhaseModelEntry;
|
||||
}
|
||||
|
||||
/** Keys of PhaseModelConfig for type-safe access */
|
||||
@@ -559,22 +596,22 @@ export interface ProjectSettings {
|
||||
/** Default phase model configuration - sensible defaults for each task type */
|
||||
export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
|
||||
// Quick tasks - use fast models for speed and cost
|
||||
enhancementModel: 'sonnet',
|
||||
fileDescriptionModel: 'haiku',
|
||||
imageDescriptionModel: 'haiku',
|
||||
enhancementModel: { model: 'sonnet' },
|
||||
fileDescriptionModel: { model: 'haiku' },
|
||||
imageDescriptionModel: { model: 'haiku' },
|
||||
|
||||
// Validation - use smart models for accuracy
|
||||
validationModel: 'sonnet',
|
||||
validationModel: { model: 'sonnet' },
|
||||
|
||||
// Generation - use powerful models for quality
|
||||
specGenerationModel: 'opus',
|
||||
featureGenerationModel: 'sonnet',
|
||||
backlogPlanningModel: 'sonnet',
|
||||
projectAnalysisModel: 'sonnet',
|
||||
specGenerationModel: { model: 'opus' },
|
||||
featureGenerationModel: { model: 'sonnet' },
|
||||
backlogPlanningModel: { model: 'sonnet' },
|
||||
projectAnalysisModel: { model: 'sonnet' },
|
||||
};
|
||||
|
||||
/** Current version of the global settings schema */
|
||||
export const SETTINGS_VERSION = 2;
|
||||
export const SETTINGS_VERSION = 3;
|
||||
/** Current version of the credentials schema */
|
||||
export const CREDENTIALS_VERSION = 1;
|
||||
/** Current version of the project settings schema */
|
||||
|
||||
Reference in New Issue
Block a user