mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge pull request #590 from AutoMaker-Org/automode-api
feat: implement cursor model migration and enhance auto mode function…
This commit is contained in:
@@ -6,10 +6,16 @@
|
||||
* - 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,
|
||||
@@ -17,6 +23,7 @@ import {
|
||||
isCursorModel,
|
||||
isOpencodeModel,
|
||||
stripProviderPrefix,
|
||||
migrateModelId,
|
||||
type PhaseModelEntry,
|
||||
type ThinkingLevel,
|
||||
} from '@automaker/types';
|
||||
@@ -29,7 +36,11 @@ 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")
|
||||
* 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
|
||||
*/
|
||||
@@ -47,74 +58,65 @@ export function resolveModelString(
|
||||
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;
|
||||
// First, migrate legacy IDs to canonical format
|
||||
const canonicalKey = migrateModelId(modelKey);
|
||||
if (canonicalKey !== modelKey) {
|
||||
console.log(`[ModelResolver] Migrated legacy ID: "${modelKey}" -> "${canonicalKey}"`);
|
||||
}
|
||||
|
||||
// Codex model with explicit prefix (e.g., "codex-gpt-5.1-codex-max") - pass through unchanged
|
||||
if (modelKey.startsWith(PROVIDER_PREFIXES.codex)) {
|
||||
console.log(`[ModelResolver] Using Codex model: ${modelKey}`);
|
||||
return modelKey;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// OpenCode model (static or dynamic) - pass through unchanged
|
||||
// This handles models like:
|
||||
// - opencode-* (Automaker routing prefix)
|
||||
// - opencode/* (free tier models)
|
||||
// - amazon-bedrock/* (AWS Bedrock models)
|
||||
// - provider/model-name (dynamic models like github-copilot/gpt-4o, google/gemini-2.5-pro)
|
||||
if (isOpencodeModel(modelKey)) {
|
||||
console.log(`[ModelResolver] Using OpenCode model: ${modelKey}`);
|
||||
return modelKey;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Full Claude model string - pass through unchanged
|
||||
if (modelKey.includes('claude-')) {
|
||||
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
|
||||
return modelKey;
|
||||
// OpenCode model (static with opencode- prefix or dynamic with provider/model format)
|
||||
if (isOpencodeModel(canonicalKey)) {
|
||||
console.log(`[ModelResolver] Using OpenCode model: ${canonicalKey}`);
|
||||
return canonicalKey;
|
||||
}
|
||||
|
||||
// Look up Claude model alias
|
||||
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||
if (resolved) {
|
||||
console.log(`[ModelResolver] Resolved Claude model alias: "${modelKey}" -> "${resolved}"`);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// OpenAI/Codex models - check for codex- or gpt- prefix
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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 "${modelKey}", using default: "${defaultModel}"`);
|
||||
console.warn(
|
||||
`[ModelResolver] Unknown model key "${canonicalKey}", using default: "${defaultModel}"`
|
||||
);
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,9 @@ describe('model-resolver', () => {
|
||||
const result = resolveModelString('sonnet');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
// Legacy aliases are migrated to canonical IDs then resolved
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Resolved Claude model alias: "sonnet"')
|
||||
expect.stringContaining('Migrated legacy ID: "sonnet" -> "claude-sonnet"')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -88,7 +89,7 @@ describe('model-resolver', () => {
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Resolved Claude model alias: "opus"')
|
||||
expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -101,8 +102,9 @@ describe('model-resolver', () => {
|
||||
it('should log the resolution for aliases', () => {
|
||||
resolveModelString('sonnet');
|
||||
|
||||
// Legacy aliases get migrated and resolved via canonical map
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Resolved Claude model alias')
|
||||
expect.stringContaining('Resolved Claude canonical ID')
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(CLAUDE_MODEL_MAP.sonnet)
|
||||
@@ -134,8 +136,9 @@ describe('model-resolver', () => {
|
||||
const result = resolveModelString('composer-1');
|
||||
|
||||
expect(result).toBe('cursor-composer-1');
|
||||
// Legacy bare IDs are migrated to canonical prefixed format
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Detected bare Cursor model ID')
|
||||
expect.stringContaining('Migrated legacy ID: "composer-1" -> "cursor-composer-1"')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -149,17 +152,18 @@ describe('model-resolver', () => {
|
||||
const result = resolveModelString('cursor-unknown-future-model');
|
||||
|
||||
expect(result).toBe('cursor-unknown-future-model');
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Passing through cursor-prefixed model')
|
||||
);
|
||||
// Unknown cursor-prefixed models pass through as Cursor models
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Using Cursor model'));
|
||||
});
|
||||
|
||||
it('should handle all known Cursor model IDs', () => {
|
||||
// CURSOR_MODEL_MAP now uses prefixed keys (e.g., 'cursor-auto')
|
||||
const cursorModelIds = Object.keys(CURSOR_MODEL_MAP);
|
||||
|
||||
for (const modelId of cursorModelIds) {
|
||||
const result = resolveModelString(`cursor-${modelId}`);
|
||||
expect(result).toBe(`cursor-${modelId}`);
|
||||
// modelId is already prefixed (e.g., 'cursor-auto')
|
||||
const result = resolveModelString(modelId);
|
||||
expect(result).toBe(modelId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
218
libs/types/src/model-migration.ts
Normal file
218
libs/types/src/model-migration.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user