Spaces:
Paused
Paused
| /** | |
| * 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) | |
| * - Passes through Copilot models unchanged (handled by CopilotProvider) | |
| * - Passes through Gemini models unchanged (handled by GeminiProvider) | |
| * - 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-kimi-k2.5-free | |
| * - Copilot: copilot-gpt-5.1, copilot-claude-sonnet-4.5, copilot-gemini-3-pro-preview | |
| * - Gemini: gemini-2.5-flash, gemini-2.5-pro | |
| * - 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, | |
| isCopilotModel, | |
| isGeminiModel, | |
| stripProviderPrefix, | |
| migrateModelId, | |
| type PhaseModelEntry, | |
| type ThinkingLevel, | |
| type ReasoningEffort, | |
| } 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; | |
| } | |
| // Copilot model with explicit prefix (e.g., "copilot-gpt-5.1", "copilot-claude-sonnet-4.5") | |
| if (isCopilotModel(canonicalKey)) { | |
| console.log(`[ModelResolver] Using Copilot model: ${canonicalKey}`); | |
| return canonicalKey; | |
| } | |
| // Gemini model with explicit prefix (e.g., "gemini-2.5-flash", "gemini-2.5-pro") | |
| if (isGeminiModel(canonicalKey)) { | |
| console.log(`[ModelResolver] Using Gemini 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-6) - 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 - pass through as-is (could be a provider model like GLM-4.7, MiniMax-M2.1) | |
| // This allows ClaudeCompatibleProvider models to work without being registered here | |
| console.log( | |
| `[ModelResolver] Unknown model key "${canonicalKey}", passing through unchanged (may be a provider model)` | |
| ); | |
| return canonicalKey; | |
| } | |
| /** | |
| * 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 (Claude models) */ | |
| thinkingLevel?: ThinkingLevel; | |
| /** Optional reasoning effort for timeout calculation (Codex models) */ | |
| reasoningEffort?: ReasoningEffort; | |
| /** Provider ID if using a ClaudeCompatibleProvider */ | |
| providerId?: string; | |
| } | |
| /** | |
| * 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, | |
| reasoningEffort: 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, | |
| reasoningEffort: undefined, | |
| }; | |
| } | |
| // Handle new PhaseModelEntry object format | |
| console.log( | |
| `[ModelResolver] phaseModel is object format: model="${phaseModel.model}", thinkingLevel="${phaseModel.thinkingLevel}", reasoningEffort="${phaseModel.reasoningEffort}", providerId="${phaseModel.providerId}"` | |
| ); | |
| // If providerId is set, pass through the model string unchanged | |
| // (it's a provider-specific model ID like "GLM-4.5-Air", not a Claude alias) | |
| if (phaseModel.providerId) { | |
| console.log( | |
| `[ModelResolver] Using provider model: providerId="${phaseModel.providerId}", model="${phaseModel.model}"` | |
| ); | |
| return { | |
| model: phaseModel.model, // Pass through unchanged | |
| thinkingLevel: phaseModel.thinkingLevel, | |
| reasoningEffort: phaseModel.reasoningEffort, | |
| providerId: phaseModel.providerId, | |
| }; | |
| } | |
| // No providerId - resolve through normal Claude model mapping | |
| return { | |
| model: resolveModelString(phaseModel.model, defaultModel), | |
| thinkingLevel: phaseModel.thinkingLevel, | |
| reasoningEffort: phaseModel.reasoningEffort, | |
| }; | |
| } | |