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.
This commit is contained in:
webdevcody
2026-01-18 18:42:52 -05:00
parent 3faebfa3fe
commit 4b0d1399b1
36 changed files with 1508 additions and 592 deletions

View File

@@ -2,18 +2,19 @@
* Cursor CLI Model IDs
* Reference: https://cursor.com/docs
*
* IMPORTANT: GPT models use 'cursor-' prefix to distinguish from Codex CLI models
* All Cursor model IDs use 'cursor-' prefix for consistent provider routing.
* This prevents naming collisions (e.g., cursor-gpt-5.2-codex vs codex-gpt-5.2-codex).
*/
export type CursorModelId =
| 'auto' // Auto-select best model
| 'composer-1' // Cursor Composer agent model
| 'sonnet-4.5' // Claude Sonnet 4.5
| 'sonnet-4.5-thinking' // Claude Sonnet 4.5 with extended thinking
| 'opus-4.5' // Claude Opus 4.5
| 'opus-4.5-thinking' // Claude Opus 4.5 with extended thinking
| 'opus-4.1' // Claude Opus 4.1
| 'gemini-3-pro' // Gemini 3 Pro
| 'gemini-3-flash' // Gemini 3 Flash
| 'cursor-auto' // Auto-select best model
| 'cursor-composer-1' // Cursor Composer agent model
| 'cursor-sonnet-4.5' // Claude Sonnet 4.5
| 'cursor-sonnet-4.5-thinking' // Claude Sonnet 4.5 with extended thinking
| 'cursor-opus-4.5' // Claude Opus 4.5
| 'cursor-opus-4.5-thinking' // Claude Opus 4.5 with extended thinking
| 'cursor-opus-4.1' // Claude Opus 4.1
| 'cursor-gemini-3-pro' // Gemini 3 Pro
| 'cursor-gemini-3-flash' // Gemini 3 Flash
| 'cursor-gpt-5.2' // GPT-5.2 via Cursor
| 'cursor-gpt-5.1' // GPT-5.1 via Cursor
| 'cursor-gpt-5.2-high' // GPT-5.2 High via Cursor
@@ -26,7 +27,22 @@ export type CursorModelId =
| 'cursor-gpt-5.2-codex-high' // GPT-5.2 Codex High via Cursor
| 'cursor-gpt-5.2-codex-max' // GPT-5.2 Codex Max via Cursor
| 'cursor-gpt-5.2-codex-max-high' // GPT-5.2 Codex Max High via Cursor
| 'grok'; // Grok
| 'cursor-grok'; // Grok
/**
* Legacy Cursor model IDs (without prefix) for migration support
*/
export type LegacyCursorModelId =
| 'auto'
| 'composer-1'
| 'sonnet-4.5'
| 'sonnet-4.5-thinking'
| 'opus-4.5'
| 'opus-4.5-thinking'
| 'opus-4.1'
| 'gemini-3-pro'
| 'gemini-3-flash'
| 'grok';
/**
* Cursor model metadata
@@ -42,66 +58,67 @@ export interface CursorModelConfig {
/**
* Complete model map for Cursor CLI
* All keys use 'cursor-' prefix for consistent provider routing.
*/
export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
auto: {
id: 'auto',
'cursor-auto': {
id: 'cursor-auto',
label: 'Auto (Recommended)',
description: 'Automatically selects the best model for each task',
hasThinking: false,
supportsVision: false, // Vision not yet supported by Cursor CLI
},
'composer-1': {
id: 'composer-1',
'cursor-composer-1': {
id: 'cursor-composer-1',
label: 'Composer 1',
description: 'Cursor Composer agent model optimized for multi-file edits',
hasThinking: false,
supportsVision: false,
},
'sonnet-4.5': {
id: 'sonnet-4.5',
'cursor-sonnet-4.5': {
id: 'cursor-sonnet-4.5',
label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via Cursor',
hasThinking: false,
supportsVision: false, // Model supports vision but Cursor CLI doesn't pass images
},
'sonnet-4.5-thinking': {
id: 'sonnet-4.5-thinking',
'cursor-sonnet-4.5-thinking': {
id: 'cursor-sonnet-4.5-thinking',
label: 'Claude Sonnet 4.5 (Thinking)',
description: 'Claude Sonnet 4.5 with extended thinking enabled',
hasThinking: true,
supportsVision: false,
},
'opus-4.5': {
id: 'opus-4.5',
'cursor-opus-4.5': {
id: 'cursor-opus-4.5',
label: 'Claude Opus 4.5',
description: 'Anthropic Claude Opus 4.5 via Cursor',
hasThinking: false,
supportsVision: false,
},
'opus-4.5-thinking': {
id: 'opus-4.5-thinking',
'cursor-opus-4.5-thinking': {
id: 'cursor-opus-4.5-thinking',
label: 'Claude Opus 4.5 (Thinking)',
description: 'Claude Opus 4.5 with extended thinking enabled',
hasThinking: true,
supportsVision: false,
},
'opus-4.1': {
id: 'opus-4.1',
'cursor-opus-4.1': {
id: 'cursor-opus-4.1',
label: 'Claude Opus 4.1',
description: 'Anthropic Claude Opus 4.1 via Cursor',
hasThinking: false,
supportsVision: false,
},
'gemini-3-pro': {
id: 'gemini-3-pro',
'cursor-gemini-3-pro': {
id: 'cursor-gemini-3-pro',
label: 'Gemini 3 Pro',
description: 'Google Gemini 3 Pro via Cursor',
hasThinking: false,
supportsVision: false,
},
'gemini-3-flash': {
id: 'gemini-3-flash',
'cursor-gemini-3-flash': {
id: 'cursor-gemini-3-flash',
label: 'Gemini 3 Flash',
description: 'Google Gemini 3 Flash (faster)',
hasThinking: false,
@@ -191,8 +208,8 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
hasThinking: false,
supportsVision: false,
},
grok: {
id: 'grok',
'cursor-grok': {
id: 'cursor-grok',
label: 'Grok',
description: 'xAI Grok via Cursor',
hasThinking: false,
@@ -200,6 +217,22 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
},
};
/**
* Map from legacy model IDs to canonical prefixed IDs
*/
export const LEGACY_CURSOR_MODEL_MAP: Record<LegacyCursorModelId, CursorModelId> = {
auto: 'cursor-auto',
'composer-1': 'cursor-composer-1',
'sonnet-4.5': 'cursor-sonnet-4.5',
'sonnet-4.5-thinking': 'cursor-sonnet-4.5-thinking',
'opus-4.5': 'cursor-opus-4.5',
'opus-4.5-thinking': 'cursor-opus-4.5-thinking',
'opus-4.1': 'cursor-opus-4.1',
'gemini-3-pro': 'cursor-gemini-3-pro',
'gemini-3-flash': 'cursor-gemini-3-flash',
grok: 'cursor-grok',
};
/**
* Helper: Check if model has thinking capability
*/
@@ -254,6 +287,7 @@ export interface GroupedModel {
/**
* Configuration for grouping Cursor models with variants
* All variant IDs use 'cursor-' prefix for consistent provider routing.
*/
export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
// GPT-5.2 group (compute levels)
@@ -346,14 +380,14 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
},
// Sonnet 4.5 group (thinking mode)
{
baseId: 'sonnet-4.5-group',
baseId: 'cursor-sonnet-4.5-group',
label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via Cursor',
variantType: 'thinking',
variants: [
{ id: 'sonnet-4.5', label: 'Standard', description: 'Fast responses' },
{ id: 'cursor-sonnet-4.5', label: 'Standard', description: 'Fast responses' },
{
id: 'sonnet-4.5-thinking',
id: 'cursor-sonnet-4.5-thinking',
label: 'Thinking',
description: 'Extended reasoning',
badge: 'Reasoning',
@@ -362,14 +396,14 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
},
// Opus 4.5 group (thinking mode)
{
baseId: 'opus-4.5-group',
baseId: 'cursor-opus-4.5-group',
label: 'Claude Opus 4.5',
description: 'Anthropic Claude Opus 4.5 via Cursor',
variantType: 'thinking',
variants: [
{ id: 'opus-4.5', label: 'Standard', description: 'Fast responses' },
{ id: 'cursor-opus-4.5', label: 'Standard', description: 'Fast responses' },
{
id: 'opus-4.5-thinking',
id: 'cursor-opus-4.5-thinking',
label: 'Thinking',
description: 'Extended reasoning',
badge: 'Reasoning',
@@ -380,14 +414,15 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
/**
* Cursor models that are not part of any group (standalone)
* All IDs use 'cursor-' prefix for consistent provider routing.
*/
export const STANDALONE_CURSOR_MODELS: CursorModelId[] = [
'auto',
'composer-1',
'opus-4.1',
'gemini-3-pro',
'gemini-3-flash',
'grok',
'cursor-auto',
'cursor-composer-1',
'cursor-opus-4.1',
'cursor-gemini-3-pro',
'cursor-gemini-3-flash',
'cursor-grok',
];
/**

View File

@@ -77,12 +77,15 @@ export type { ImageData, ImageContentBlock } from './image.js';
// Model types and constants
export {
CLAUDE_MODEL_MAP,
CLAUDE_CANONICAL_MAP,
LEGACY_CLAUDE_ALIAS_MAP,
CODEX_MODEL_MAP,
CODEX_MODEL_IDS,
REASONING_CAPABLE_MODELS,
supportsReasoningEffort,
getAllCodexModelIds,
DEFAULT_MODELS,
type ClaudeCanonicalId,
type ModelAlias,
type CodexModelId,
type AgentModel,
@@ -237,6 +240,18 @@ export {
validateBareModelId,
} from './provider-utils.js';
// Model migration utilities
export {
isLegacyCursorModelId,
isLegacyOpencodeModelId,
isLegacyClaudeAlias,
migrateModelId,
migrateCursorModelIds,
migrateOpencodeModelIds,
migratePhaseModelEntry,
getBareModelIdForCli,
} from './model-migration.js';
// Pipeline types
export type {
PipelineStep,

View File

@@ -0,0 +1,218 @@
/**
* Model ID Migration Utilities
*
* Provides functions to migrate legacy model IDs to the canonical prefixed format.
* This ensures backward compatibility when loading settings from older versions.
*/
import type { CursorModelId, LegacyCursorModelId } from './cursor-models.js';
import { LEGACY_CURSOR_MODEL_MAP, CURSOR_MODEL_MAP } from './cursor-models.js';
import type { OpencodeModelId, LegacyOpencodeModelId } from './opencode-models.js';
import { LEGACY_OPENCODE_MODEL_MAP, OPENCODE_MODEL_CONFIG_MAP } from './opencode-models.js';
import type { ClaudeCanonicalId } from './model.js';
import { LEGACY_CLAUDE_ALIAS_MAP, CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP } from './model.js';
import type { PhaseModelEntry } from './settings.js';
/**
* Check if a string is a legacy Cursor model ID (without prefix)
*/
export function isLegacyCursorModelId(id: string): id is LegacyCursorModelId {
return id in LEGACY_CURSOR_MODEL_MAP;
}
/**
* Check if a string is a legacy OpenCode model ID (with slash format)
*/
export function isLegacyOpencodeModelId(id: string): id is LegacyOpencodeModelId {
return id in LEGACY_OPENCODE_MODEL_MAP;
}
/**
* Check if a string is a legacy Claude alias (short name without prefix)
*/
export function isLegacyClaudeAlias(id: string): boolean {
return id in LEGACY_CLAUDE_ALIAS_MAP;
}
/**
* Migrate a single model ID to canonical format
*
* Handles:
* - Legacy Cursor IDs (e.g., 'auto' -> 'cursor-auto')
* - Legacy OpenCode IDs (e.g., 'opencode/big-pickle' -> 'opencode-big-pickle')
* - Legacy Claude aliases (e.g., 'sonnet' -> 'claude-sonnet')
* - Already-canonical IDs are passed through unchanged
*
* @param legacyId - The model ID to migrate
* @returns The canonical model ID
*/
export function migrateModelId(legacyId: string | undefined | null): string {
if (!legacyId) {
return legacyId as string;
}
// Already has cursor- prefix and is in the map - it's canonical
if (legacyId.startsWith('cursor-') && legacyId in CURSOR_MODEL_MAP) {
return legacyId;
}
// Legacy Cursor model ID (without prefix)
if (isLegacyCursorModelId(legacyId)) {
return LEGACY_CURSOR_MODEL_MAP[legacyId];
}
// Already has opencode- prefix - it's canonical
if (legacyId.startsWith('opencode-') && legacyId in OPENCODE_MODEL_CONFIG_MAP) {
return legacyId;
}
// Legacy OpenCode model ID (with slash format)
if (isLegacyOpencodeModelId(legacyId)) {
return LEGACY_OPENCODE_MODEL_MAP[legacyId];
}
// Already has claude- prefix and is in canonical map
if (legacyId.startsWith('claude-') && legacyId in CLAUDE_CANONICAL_MAP) {
return legacyId;
}
// Legacy Claude alias (short name)
if (isLegacyClaudeAlias(legacyId)) {
return LEGACY_CLAUDE_ALIAS_MAP[legacyId];
}
// Unknown or already canonical - pass through
return legacyId;
}
/**
* Migrate an array of Cursor model IDs to canonical format
*
* @param ids - Array of legacy or canonical Cursor model IDs
* @returns Array of canonical Cursor model IDs
*/
export function migrateCursorModelIds(ids: string[]): CursorModelId[] {
if (!ids || !Array.isArray(ids)) {
return [];
}
return ids.map((id) => {
// Already canonical
if (id.startsWith('cursor-') && id in CURSOR_MODEL_MAP) {
return id as CursorModelId;
}
// Legacy ID
if (isLegacyCursorModelId(id)) {
return LEGACY_CURSOR_MODEL_MAP[id];
}
// Unknown - assume it might be a valid cursor model with prefix
if (id.startsWith('cursor-')) {
return id as CursorModelId;
}
// Add prefix if not present
return `cursor-${id}` as CursorModelId;
});
}
/**
* Migrate an array of OpenCode model IDs to canonical format
*
* @param ids - Array of legacy or canonical OpenCode model IDs
* @returns Array of canonical OpenCode model IDs
*/
export function migrateOpencodeModelIds(ids: string[]): OpencodeModelId[] {
if (!ids || !Array.isArray(ids)) {
return [];
}
return ids.map((id) => {
// Already canonical (dash format)
if (id.startsWith('opencode-') && id in OPENCODE_MODEL_CONFIG_MAP) {
return id as OpencodeModelId;
}
// Legacy ID (slash format)
if (isLegacyOpencodeModelId(id)) {
return LEGACY_OPENCODE_MODEL_MAP[id];
}
// Convert slash to dash format for unknown models
if (id.startsWith('opencode/')) {
return id.replace('opencode/', 'opencode-') as OpencodeModelId;
}
// Add prefix if not present
if (!id.startsWith('opencode-')) {
return `opencode-${id}` as OpencodeModelId;
}
return id as OpencodeModelId;
});
}
/**
* Migrate a PhaseModelEntry to use canonical model IDs
*
* @param entry - The phase model entry to migrate
* @returns Migrated phase model entry with canonical model ID
*/
export function migratePhaseModelEntry(
entry: PhaseModelEntry | string | undefined | null
): PhaseModelEntry {
// Handle null/undefined
if (!entry) {
return { model: 'claude-sonnet' }; // Default
}
// Handle legacy string format
if (typeof entry === 'string') {
return { model: migrateModelId(entry) };
}
// Handle PhaseModelEntry object
return {
...entry,
model: migrateModelId(entry.model),
};
}
/**
* Get the bare model ID for CLI calls (strip provider prefix)
*
* When calling provider CLIs, we need to strip the provider prefix:
* - 'cursor-auto' -> 'auto' (for Cursor CLI)
* - 'cursor-composer-1' -> 'composer-1' (for Cursor CLI)
* - 'opencode-big-pickle' -> 'big-pickle' (for OpenCode CLI)
*
* Note: GPT models via Cursor keep the gpt- part: 'cursor-gpt-5.2' -> 'gpt-5.2'
*
* @param modelId - The canonical model ID with provider prefix
* @returns The bare model ID for CLI usage
*/
export function getBareModelIdForCli(modelId: string): string {
if (!modelId) return modelId;
// Cursor models
if (modelId.startsWith('cursor-')) {
const bareId = modelId.slice(7); // Remove 'cursor-'
// For GPT models, keep the gpt- prefix since that's what the CLI expects
// e.g., 'cursor-gpt-5.2' -> 'gpt-5.2'
return bareId;
}
// OpenCode models - strip prefix
if (modelId.startsWith('opencode-')) {
return modelId.slice(9); // Remove 'opencode-'
}
// Codex models - strip prefix
if (modelId.startsWith('codex-')) {
return modelId.slice(6); // Remove 'codex-'
}
// Claude and other models - pass through
return modelId;
}

View File

@@ -4,12 +4,42 @@
import type { CursorModelId } from './cursor-models.js';
import type { OpencodeModelId } from './opencode-models.js';
/**
* Canonical Claude model IDs with provider prefix
* Used for internal storage and consistent provider routing.
*/
export type ClaudeCanonicalId = 'claude-haiku' | 'claude-sonnet' | 'claude-opus';
/**
* Canonical Claude model map - maps prefixed IDs to full model strings
* Use these IDs for internal storage and routing.
*/
export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
'claude-haiku': 'claude-haiku-4-5-20251001',
'claude-sonnet': 'claude-sonnet-4-5-20250929',
'claude-opus': 'claude-opus-4-5-20251101',
} as const;
/**
* Legacy Claude model aliases (short names) for backward compatibility
* These map to the same full model strings as the canonical map.
* @deprecated Use CLAUDE_CANONICAL_MAP for new code
*/
export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929',
opus: 'claude-opus-4-5-20251101',
} as const;
/**
* Map from legacy aliases to canonical IDs
*/
export const LEGACY_CLAUDE_ALIAS_MAP: Record<string, ClaudeCanonicalId> = {
haiku: 'claude-haiku',
sonnet: 'claude-sonnet',
opus: 'claude-opus',
} as const;
/**
* Codex/OpenAI model identifiers
* Based on OpenAI Codex CLI official models
@@ -62,10 +92,11 @@ export function getAllCodexModelIds(): CodexModelId[] {
/**
* Default models per provider
* Uses canonical prefixed IDs for consistent routing.
*/
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
cursor: 'auto', // Cursor's recommended default
cursor: 'cursor-auto', // Cursor's recommended default (with prefix)
codex: CODEX_MODEL_MAP.gpt52Codex, // GPT-5.2-Codex is the most advanced agentic coding model
} as const;

View File

@@ -1,9 +1,22 @@
/**
* OpenCode Model IDs
* Models available via OpenCode CLI (opencode models command)
*
* All OpenCode model IDs use 'opencode-' prefix for consistent provider routing.
* This prevents naming collisions and ensures clear provider attribution.
*/
export type OpencodeModelId =
// OpenCode Free Tier Models
| 'opencode-big-pickle'
| 'opencode-glm-4.7-free'
| 'opencode-gpt-5-nano'
| 'opencode-grok-code'
| 'opencode-minimax-m2.1-free';
/**
* Legacy OpenCode model IDs (with slash format) for migration support
*/
export type LegacyOpencodeModelId =
| 'opencode/big-pickle'
| 'opencode/glm-4.7-free'
| 'opencode/gpt-5-nano'
@@ -20,16 +33,27 @@ export type OpencodeProvider = 'opencode';
*/
export const OPENCODE_MODEL_MAP: Record<string, OpencodeModelId> = {
// OpenCode free tier aliases
'big-pickle': 'opencode/big-pickle',
pickle: 'opencode/big-pickle',
'glm-free': 'opencode/glm-4.7-free',
'gpt-nano': 'opencode/gpt-5-nano',
nano: 'opencode/gpt-5-nano',
'grok-code': 'opencode/grok-code',
grok: 'opencode/grok-code',
minimax: 'opencode/minimax-m2.1-free',
'big-pickle': 'opencode-big-pickle',
pickle: 'opencode-big-pickle',
'glm-free': 'opencode-glm-4.7-free',
'gpt-nano': 'opencode-gpt-5-nano',
nano: 'opencode-gpt-5-nano',
'grok-code': 'opencode-grok-code',
grok: 'opencode-grok-code',
minimax: 'opencode-minimax-m2.1-free',
} as const;
/**
* Map from legacy slash-format model IDs to canonical prefixed IDs
*/
export const LEGACY_OPENCODE_MODEL_MAP: Record<LegacyOpencodeModelId, OpencodeModelId> = {
'opencode/big-pickle': 'opencode-big-pickle',
'opencode/glm-4.7-free': 'opencode-glm-4.7-free',
'opencode/gpt-5-nano': 'opencode-gpt-5-nano',
'opencode/grok-code': 'opencode-grok-code',
'opencode/minimax-m2.1-free': 'opencode-minimax-m2.1-free',
};
/**
* OpenCode model metadata
*/
@@ -44,11 +68,12 @@ export interface OpencodeModelConfig {
/**
* Complete list of OpenCode model configurations
* All IDs use 'opencode-' prefix for consistent provider routing.
*/
export const OPENCODE_MODELS: OpencodeModelConfig[] = [
// OpenCode Free Tier Models
{
id: 'opencode/big-pickle',
id: 'opencode-big-pickle',
label: 'Big Pickle',
description: 'OpenCode free tier model - great for general coding',
supportsVision: false,
@@ -56,7 +81,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/glm-4.7-free',
id: 'opencode-glm-4.7-free',
label: 'GLM 4.7 Free',
description: 'OpenCode free tier GLM model',
supportsVision: false,
@@ -64,7 +89,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/gpt-5-nano',
id: 'opencode-gpt-5-nano',
label: 'GPT-5 Nano',
description: 'OpenCode free tier nano model - fast and lightweight',
supportsVision: false,
@@ -72,7 +97,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/grok-code',
id: 'opencode-grok-code',
label: 'Grok Code',
description: 'OpenCode free tier Grok model for coding',
supportsVision: false,
@@ -80,7 +105,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/minimax-m2.1-free',
id: 'opencode-minimax-m2.1-free',
label: 'MiniMax M2.1 Free',
description: 'OpenCode free tier MiniMax model',
supportsVision: false,
@@ -104,7 +129,7 @@ export const OPENCODE_MODEL_CONFIG_MAP: Record<OpencodeModelId, OpencodeModelCon
/**
* Default OpenCode model - OpenCode free tier
*/
export const DEFAULT_OPENCODE_MODEL: OpencodeModelId = 'opencode/big-pickle';
export const DEFAULT_OPENCODE_MODEL: OpencodeModelId = 'opencode-big-pickle';
/**
* Helper: Get display name for model

View File

@@ -7,9 +7,9 @@
*/
import type { ModelProvider } from './settings.js';
import { CURSOR_MODEL_MAP } from './cursor-models.js';
import { CURSOR_MODEL_MAP, LEGACY_CURSOR_MODEL_MAP } from './cursor-models.js';
import { CLAUDE_MODEL_MAP, CODEX_MODEL_MAP } from './model.js';
import { OPENCODE_MODEL_CONFIG_MAP } from './opencode-models.js';
import { OPENCODE_MODEL_CONFIG_MAP, LEGACY_OPENCODE_MODEL_MAP } from './opencode-models.js';
/** Provider prefix constants */
export const PROVIDER_PREFIXES = {
@@ -21,20 +21,23 @@ export const PROVIDER_PREFIXES = {
/**
* Check if a model string represents a Cursor model
*
* @param model - Model string to check (e.g., "cursor-composer-1" or "composer-1")
* @returns true if the model is a Cursor model (excluding Codex-specific models)
* With canonical model IDs, Cursor models always have 'cursor-' prefix.
* Legacy IDs without prefix are handled by migration utilities.
*
* @param model - Model string to check (e.g., "cursor-auto", "cursor-composer-1")
* @returns true if the model is a Cursor model
*/
export function isCursorModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Check for explicit cursor- prefix
// Canonical format: all Cursor models have cursor- prefix
if (model.startsWith(PROVIDER_PREFIXES.cursor)) {
return true;
}
// Check if it's a bare Cursor model ID (excluding Codex-specific models)
// Codex-specific models should always route to Codex provider, not Cursor
if (model in CURSOR_MODEL_MAP) {
// Legacy support: check if it's a known legacy bare ID
// This handles transition period before migration
if (model in LEGACY_CURSOR_MODEL_MAP) {
return true;
}
@@ -90,12 +93,14 @@ export function isCodexModel(model: string | undefined | null): boolean {
/**
* Check if a model string represents an OpenCode model
*
* With canonical model IDs, static OpenCode models use 'opencode-' prefix.
* Dynamic models from OpenCode CLI still use provider/model format.
*
* OpenCode models can be identified by:
* - Explicit 'opencode-' prefix (for routing in Automaker)
* - 'opencode/' prefix (OpenCode free tier models)
* - 'opencode-' prefix (canonical format for static models)
* - 'opencode/' prefix (legacy format, will be migrated)
* - 'amazon-bedrock/' prefix (AWS Bedrock models via OpenCode)
* - Full model ID from OPENCODE_MODEL_CONFIG_MAP
* - Dynamic models from OpenCode CLI with provider/model format (e.g., "github-copilot/gpt-4o", "google/gemini-2.5-pro")
* - Dynamic models with provider/model format (e.g., "github-copilot/gpt-4o")
*
* @param model - Model string to check
* @returns true if the model is an OpenCode model
@@ -103,19 +108,18 @@ export function isCodexModel(model: string | undefined | null): boolean {
export function isOpencodeModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Check for explicit opencode- prefix (Automaker routing prefix)
// Canonical format: opencode- prefix for static models
if (model.startsWith(PROVIDER_PREFIXES.opencode)) {
return true;
}
// Check if it's a known OpenCode model ID
// Check if it's a known OpenCode model ID (handles both formats during transition)
if (model in OPENCODE_MODEL_CONFIG_MAP) {
return true;
}
// Check for OpenCode native model prefixes
// - opencode/ = OpenCode free tier models
// - amazon-bedrock/ = AWS Bedrock models
// Legacy format: opencode/ prefix (will be migrated to opencode-)
// Also supports amazon-bedrock/ for AWS Bedrock models
if (model.startsWith('opencode/') || model.startsWith('amazon-bedrock/')) {
return true;
}
@@ -228,32 +232,47 @@ export function getBareModelId(model: string): string {
/**
* Normalize a model string to its canonical form
* - For Cursor: adds cursor- prefix if missing
* - For Codex: can add codex- prefix (but bare gpt-* is also valid)
* - For Claude: returns as-is
*
* With the new canonical format:
* - Cursor models: always have cursor- prefix
* - OpenCode models: always have opencode- prefix (static) or provider/model format (dynamic)
* - Claude models: can use legacy aliases or claude- prefix
* - Codex models: always have codex- prefix
*
* @param model - Model string to normalize
* @returns Normalized model string
*/
export function normalizeModelString(model: string | undefined | null): string {
if (!model || typeof model !== 'string') return 'sonnet'; // Default
if (!model || typeof model !== 'string') return 'claude-sonnet'; // Default to canonical
// If it's a Cursor model without prefix, add the prefix
if (model in CURSOR_MODEL_MAP && !model.startsWith(PROVIDER_PREFIXES.cursor)) {
return `${PROVIDER_PREFIXES.cursor}${model}`;
// Already has a canonical prefix - return as-is
if (
model.startsWith(PROVIDER_PREFIXES.cursor) ||
model.startsWith(PROVIDER_PREFIXES.codex) ||
model.startsWith(PROVIDER_PREFIXES.opencode) ||
model.startsWith('claude-')
) {
return model;
}
// For Codex, bare gpt-* and o-series models are valid canonical forms
// Check if it's in the CODEX_MODEL_MAP
if (model in CODEX_MODEL_MAP) {
// If it already starts with gpt- or o, it's canonical
if (model.startsWith('gpt-') || /^o\d/.test(model)) {
return model;
}
// Otherwise, it might need a prefix (though this is unlikely)
if (!model.startsWith(PROVIDER_PREFIXES.codex)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
// Check if it's a legacy Cursor model ID
if (model in LEGACY_CURSOR_MODEL_MAP) {
return LEGACY_CURSOR_MODEL_MAP[model as keyof typeof LEGACY_CURSOR_MODEL_MAP];
}
// Check if it's a legacy OpenCode model ID
if (model in LEGACY_OPENCODE_MODEL_MAP) {
return LEGACY_OPENCODE_MODEL_MAP[model as keyof typeof LEGACY_OPENCODE_MODEL_MAP];
}
// Legacy Claude aliases
if (model in CLAUDE_MODEL_MAP) {
return `claude-${model}`;
}
// For Codex, bare gpt-* and o-series models need codex- prefix
if (model.startsWith('gpt-') || /^o\d/.test(model)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
return model;

View File

@@ -780,34 +780,42 @@ export interface ProjectSettings {
* Value: agent configuration
*/
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
// Auto Mode Configuration (per-project)
/** Whether auto mode is enabled for this project (backend-controlled loop) */
automodeEnabled?: boolean;
/** Maximum concurrent agents for this project (overrides global maxConcurrency) */
maxConcurrentAgents?: number;
}
/**
* Default values and constants
*/
/** Default phase model configuration - sensible defaults for each task type */
/** Default phase model configuration - sensible defaults for each task type
* Uses canonical prefixed model IDs for consistent routing.
*/
export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
// Quick tasks - use fast models for speed and cost
enhancementModel: { model: 'sonnet' },
fileDescriptionModel: { model: 'haiku' },
imageDescriptionModel: { model: 'haiku' },
enhancementModel: { model: 'claude-sonnet' },
fileDescriptionModel: { model: 'claude-haiku' },
imageDescriptionModel: { model: 'claude-haiku' },
// Validation - use smart models for accuracy
validationModel: { model: 'sonnet' },
validationModel: { model: 'claude-sonnet' },
// Generation - use powerful models for quality
specGenerationModel: { model: 'opus' },
featureGenerationModel: { model: 'sonnet' },
backlogPlanningModel: { model: 'sonnet' },
projectAnalysisModel: { model: 'sonnet' },
suggestionsModel: { model: 'sonnet' },
specGenerationModel: { model: 'claude-opus' },
featureGenerationModel: { model: 'claude-sonnet' },
backlogPlanningModel: { model: 'claude-sonnet' },
projectAnalysisModel: { model: 'claude-sonnet' },
suggestionsModel: { model: 'claude-sonnet' },
// Memory - use fast model for learning extraction (cost-effective)
memoryExtractionModel: { model: 'haiku' },
memoryExtractionModel: { model: 'claude-haiku' },
// Commit messages - use fast model for speed
commitMessageModel: { model: 'haiku' },
commitMessageModel: { model: 'claude-haiku' },
};
/** Current version of the global settings schema */
@@ -857,18 +865,18 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
useWorktrees: true,
defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false,
defaultFeatureModel: { model: 'opus' },
defaultFeatureModel: { model: 'claude-opus' }, // Use canonical ID
muteDoneSound: false,
serverLogLevel: 'info',
enableRequestLogging: true,
enableAiCommitMessages: true,
phaseModels: DEFAULT_PHASE_MODELS,
enhancementModel: 'sonnet',
validationModel: 'opus',
enabledCursorModels: getAllCursorModelIds(),
cursorDefaultModel: 'auto',
enabledOpencodeModels: getAllOpencodeModelIds(),
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL,
enhancementModel: 'sonnet', // Legacy alias still supported
validationModel: 'opus', // Legacy alias still supported
enabledCursorModels: getAllCursorModelIds(), // Returns prefixed IDs
cursorDefaultModel: 'cursor-auto', // Use canonical prefixed ID
enabledOpencodeModels: getAllOpencodeModelIds(), // Returns prefixed IDs
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, // Already prefixed
enabledDynamicModelIds: [],
disabledProviders: [],
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,