Files
automaker/libs/model-resolver/src/resolver.ts
DhanushSantosh 251f0fd88e chore: update CI configuration and enhance test stability
- Added deterministic API key and environment variables in e2e-tests.yml to ensure consistent test behavior.
- Refactored CodexProvider tests to improve type safety and mock handling, ensuring reliable test execution.
- Updated provider-factory tests to mock installation detection for CodexProvider, enhancing test isolation.
- Adjusted Playwright configuration to conditionally use external backend, improving flexibility in test environments.
- Enhanced kill-test-servers script to handle external server scenarios, ensuring proper cleanup of test processes.

These changes improve the reliability and maintainability of the testing framework, leading to a more stable development experience.
2026-01-07 19:09:26 +05:30

189 lines
6.3 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
*/
import {
CLAUDE_MODEL_MAP,
CURSOR_MODEL_MAP,
CODEX_MODEL_MAP,
DEFAULT_MODELS,
PROVIDER_PREFIXES,
isCursorModel,
stripProviderPrefix,
type PhaseModelEntry,
type ThinkingLevel,
} from '@automaker/types';
// Pattern definitions for Codex/OpenAI models
const CODEX_MODEL_PREFIXES = ['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
*
* @param modelKey - Model key (e.g., "opus", "cursor-composer-1", "claude-sonnet-4-20250514")
* @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;
}
// Cursor model with explicit prefix (e.g., "cursor-composer-1") - pass through unchanged
// CursorProvider will strip the prefix when calling the CLI
if (modelKey.startsWith(PROVIDER_PREFIXES.cursor)) {
const cursorModelId = stripProviderPrefix(modelKey);
// Verify it's a valid Cursor model
if (cursorModelId in CURSOR_MODEL_MAP) {
console.log(
`[ModelResolver] Using Cursor model: ${modelKey} (valid model ID: ${cursorModelId})`
);
return modelKey;
}
// Could be a cursor-prefixed model not in our map yet - still pass through
console.log(`[ModelResolver] Passing through cursor-prefixed model: ${modelKey}`);
return modelKey;
}
// Full Claude model string - pass through unchanged
if (modelKey.includes('claude-')) {
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
return modelKey;
}
// Look up Claude model alias
const resolved = CLAUDE_MODEL_MAP[modelKey];
if (resolved) {
console.log(`[ModelResolver] Resolved Claude model alias: "${modelKey}" -> "${resolved}"`);
return resolved;
}
// OpenAI/Codex models - check BEFORE bare Cursor models since they overlap
// (Cursor supports gpt models, but bare "gpt-*" should route to Codex)
if (
CODEX_MODEL_PREFIXES.some((prefix) => modelKey.startsWith(prefix)) ||
(OPENAI_O_SERIES_PATTERN.test(modelKey) && OPENAI_O_SERIES_ALLOWED_MODELS.has(modelKey))
) {
console.log(`[ModelResolver] Using OpenAI/Codex model: ${modelKey}`);
return modelKey;
}
// Check if it's a bare Cursor model ID (e.g., "composer-1", "auto", "gpt-4o")
// Note: This is checked AFTER Codex check to prioritize Codex for bare gpt-* models
if (modelKey in CURSOR_MODEL_MAP) {
// Return with cursor- prefix so provider routing works correctly
const prefixedModel = `${PROVIDER_PREFIXES.cursor}${modelKey}`;
console.log(
`[ModelResolver] Detected bare Cursor model ID: "${modelKey}" -> "${prefixedModel}"`
);
return prefixedModel;
}
// Unknown model key - use default
console.warn(`[ModelResolver] Unknown model key "${modelKey}", 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,
};
}