mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
This commit introduces significant updates to the cursor model handling and auto mode features. The cursor model IDs have been standardized to a canonical format, ensuring backward compatibility while migrating legacy IDs. New endpoints for starting and stopping the auto mode loop have been added, allowing for better control over project-specific auto mode operations. Key changes: - Updated cursor model IDs to use the 'cursor-' prefix for consistency. - Added new API endpoints: `/start` and `/stop` for managing auto mode. - Enhanced the status endpoint to provide detailed project-specific auto mode information. - Improved error handling and logging throughout the auto mode service. - Migrated legacy model IDs to their canonical counterparts in various components. This update aims to streamline the user experience and ensure a smooth transition for existing users while providing new functionalities.
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
/**
|
|
* Model resolution utilities for handling model string mapping
|
|
*
|
|
* Provides centralized model resolution logic:
|
|
* - Maps Claude model aliases to full model strings
|
|
* - Passes through Cursor models unchanged (handled by CursorProvider)
|
|
* - Provides default models per provider
|
|
* - Handles multiple model sources with priority
|
|
*
|
|
* With canonical model IDs:
|
|
* - Cursor: cursor-auto, cursor-composer-1, cursor-gpt-5.2
|
|
* - OpenCode: opencode-big-pickle, opencode-grok-code
|
|
* - Claude: claude-haiku, claude-sonnet, claude-opus (also supports legacy aliases)
|
|
*/
|
|
|
|
import {
|
|
CLAUDE_MODEL_MAP,
|
|
CLAUDE_CANONICAL_MAP,
|
|
CURSOR_MODEL_MAP,
|
|
CODEX_MODEL_MAP,
|
|
DEFAULT_MODELS,
|
|
PROVIDER_PREFIXES,
|
|
isCursorModel,
|
|
isOpencodeModel,
|
|
stripProviderPrefix,
|
|
migrateModelId,
|
|
type PhaseModelEntry,
|
|
type ThinkingLevel,
|
|
} from '@automaker/types';
|
|
|
|
// Pattern definitions for Codex/OpenAI models
|
|
const CODEX_MODEL_PREFIXES = ['codex-', 'gpt-'];
|
|
const OPENAI_O_SERIES_PATTERN = /^o\d/;
|
|
const OPENAI_O_SERIES_ALLOWED_MODELS = new Set<string>();
|
|
|
|
/**
|
|
* Resolve a model key/alias to a full model string
|
|
*
|
|
* Handles both canonical prefixed IDs and legacy aliases:
|
|
* - Canonical: cursor-auto, cursor-gpt-5.2, opencode-big-pickle, claude-sonnet
|
|
* - Legacy: auto, composer-1, sonnet, opus
|
|
*
|
|
* @param modelKey - Model key (e.g., "claude-opus", "cursor-composer-1", "sonnet")
|
|
* @param defaultModel - Fallback model if modelKey is undefined
|
|
* @returns Full model string
|
|
*/
|
|
export function resolveModelString(
|
|
modelKey?: string,
|
|
defaultModel: string = DEFAULT_MODELS.claude
|
|
): string {
|
|
console.log(
|
|
`[ModelResolver] resolveModelString called with modelKey: "${modelKey}", defaultModel: "${defaultModel}"`
|
|
);
|
|
|
|
// No model specified - use default
|
|
if (!modelKey) {
|
|
console.log(`[ModelResolver] No model specified, using default: ${defaultModel}`);
|
|
return defaultModel;
|
|
}
|
|
|
|
// First, migrate legacy IDs to canonical format
|
|
const canonicalKey = migrateModelId(modelKey);
|
|
if (canonicalKey !== modelKey) {
|
|
console.log(`[ModelResolver] Migrated legacy ID: "${modelKey}" -> "${canonicalKey}"`);
|
|
}
|
|
|
|
// Cursor model with explicit prefix (e.g., "cursor-auto", "cursor-composer-1")
|
|
// Pass through unchanged - provider will extract bare ID for CLI
|
|
if (canonicalKey.startsWith(PROVIDER_PREFIXES.cursor)) {
|
|
console.log(`[ModelResolver] Using Cursor model: ${canonicalKey}`);
|
|
return canonicalKey;
|
|
}
|
|
|
|
// Codex model with explicit prefix (e.g., "codex-gpt-5.1-codex-max")
|
|
if (canonicalKey.startsWith(PROVIDER_PREFIXES.codex)) {
|
|
console.log(`[ModelResolver] Using Codex model: ${canonicalKey}`);
|
|
return canonicalKey;
|
|
}
|
|
|
|
// OpenCode model (static with opencode- prefix or dynamic with provider/model format)
|
|
if (isOpencodeModel(canonicalKey)) {
|
|
console.log(`[ModelResolver] Using OpenCode model: ${canonicalKey}`);
|
|
return canonicalKey;
|
|
}
|
|
|
|
// Claude canonical ID (claude-haiku, claude-sonnet, claude-opus)
|
|
// Map to full model string
|
|
if (canonicalKey in CLAUDE_CANONICAL_MAP) {
|
|
const resolved = CLAUDE_CANONICAL_MAP[canonicalKey as keyof typeof CLAUDE_CANONICAL_MAP];
|
|
console.log(`[ModelResolver] Resolved Claude canonical ID: "${canonicalKey}" -> "${resolved}"`);
|
|
return resolved;
|
|
}
|
|
|
|
// Full Claude model string (e.g., claude-sonnet-4-5-20250929) - pass through
|
|
if (canonicalKey.includes('claude-')) {
|
|
console.log(`[ModelResolver] Using full Claude model string: ${canonicalKey}`);
|
|
return canonicalKey;
|
|
}
|
|
|
|
// Legacy Claude model alias (sonnet, opus, haiku) - support for backward compatibility
|
|
const resolved = CLAUDE_MODEL_MAP[canonicalKey];
|
|
if (resolved) {
|
|
console.log(`[ModelResolver] Resolved Claude legacy alias: "${canonicalKey}" -> "${resolved}"`);
|
|
return resolved;
|
|
}
|
|
|
|
// OpenAI/Codex models - check for gpt- prefix
|
|
if (
|
|
CODEX_MODEL_PREFIXES.some((prefix) => canonicalKey.startsWith(prefix)) ||
|
|
(OPENAI_O_SERIES_PATTERN.test(canonicalKey) && OPENAI_O_SERIES_ALLOWED_MODELS.has(canonicalKey))
|
|
) {
|
|
console.log(`[ModelResolver] Using OpenAI/Codex model: ${canonicalKey}`);
|
|
return canonicalKey;
|
|
}
|
|
|
|
// Unknown model key - use default
|
|
console.warn(
|
|
`[ModelResolver] Unknown model key "${canonicalKey}", using default: "${defaultModel}"`
|
|
);
|
|
return defaultModel;
|
|
}
|
|
|
|
/**
|
|
* Get the effective model from multiple sources
|
|
* Priority: explicit model > session model > default
|
|
*
|
|
* @param explicitModel - Explicitly provided model (highest priority)
|
|
* @param sessionModel - Model from session (medium priority)
|
|
* @param defaultModel - Fallback default model (lowest priority)
|
|
* @returns Resolved model string
|
|
*/
|
|
export function getEffectiveModel(
|
|
explicitModel?: string,
|
|
sessionModel?: string,
|
|
defaultModel?: string
|
|
): 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,
|
|
};
|
|
}
|