Files
automaker/libs/model-resolver/src/resolver.ts
webdevcody 4b0d1399b1 feat: implement cursor model migration and enhance auto mode functionality
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.
2026-01-18 18:42:52 -05:00

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,
};
}