feat: Enhance model resolution for Cursor models

- Added support for Cursor models in the model resolver, allowing cursor-prefixed models to pass through unchanged.
- Implemented logic to handle bare Cursor model IDs by adding the cursor- prefix.
- Updated logging to provide detailed information on model resolution processes for both Claude and Cursor models.
- Expanded unit tests to cover new Cursor model handling scenarios, ensuring robust validation of model resolution logic.
This commit is contained in:
Shirone
2025-12-28 01:55:40 +01:00
parent 0bcc8fca5d
commit b32eacc913
6 changed files with 134 additions and 10 deletions

View File

@@ -4,7 +4,13 @@
*/
// Re-export constants from types
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, type ModelAlias } from '@automaker/types';
export {
CLAUDE_MODEL_MAP,
CURSOR_MODEL_MAP,
DEFAULT_MODELS,
type ModelAlias,
type CursorModelId,
} from '@automaker/types';
// Export resolver functions
export { resolveModelString, getEffectiveModel } from './resolver.js';

View File

@@ -3,16 +3,17 @@
*
* 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, DEFAULT_MODELS } from '@automaker/types';
import { CLAUDE_MODEL_MAP, CURSOR_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
/**
* Resolve a model key/alias to a full model string
*
* @param modelKey - Model key (e.g., "opus", "gpt-5.2", "claude-sonnet-4-20250514")
* @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
*/
@@ -20,11 +21,42 @@ 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('cursor-')) {
const cursorModelId = modelKey.replace('cursor-', '');
// 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;
}
// Check if it's a bare Cursor model ID (e.g., "composer-1", "auto", "gpt-4o")
if (modelKey in CURSOR_MODEL_MAP) {
// Return with cursor- prefix so provider routing works correctly
const prefixedModel = `cursor-${modelKey}`;
console.log(
`[ModelResolver] Detected bare Cursor model ID: "${modelKey}" -> "${prefixedModel}"`
);
return prefixedModel;
}
// Full Claude model string - pass through unchanged
if (modelKey.includes('claude-')) {
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
@@ -34,7 +66,7 @@ export function resolveModelString(
// Look up Claude model alias
const resolved = CLAUDE_MODEL_MAP[modelKey];
if (resolved) {
console.log(`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`);
console.log(`[ModelResolver] Resolved Claude model alias: "${modelKey}" -> "${resolved}"`);
return resolved;
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { resolveModelString, getEffectiveModel } from '../src/resolver';
import { CLAUDE_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
import { CLAUDE_MODEL_MAP, CURSOR_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
describe('model-resolver', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
@@ -74,7 +74,7 @@ describe('model-resolver', () => {
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Resolved model alias: "sonnet"')
expect.stringContaining('Resolved Claude model alias: "sonnet"')
);
});
@@ -83,7 +83,7 @@ describe('model-resolver', () => {
expect(result).toBe(CLAUDE_MODEL_MAP.opus);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Resolved model alias: "opus"')
expect.stringContaining('Resolved Claude model alias: "opus"')
);
});
@@ -96,13 +96,69 @@ describe('model-resolver', () => {
it('should log the resolution for aliases', () => {
resolveModelString('sonnet');
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Resolved model alias'));
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Resolved Claude model alias')
);
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining(CLAUDE_MODEL_MAP.sonnet)
);
});
});
describe('with Cursor models', () => {
it('should pass through cursor-prefixed model unchanged', () => {
const result = resolveModelString('cursor-composer-1');
expect(result).toBe('cursor-composer-1');
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Using Cursor model'));
});
it('should handle cursor-auto model', () => {
const result = resolveModelString('cursor-auto');
expect(result).toBe('cursor-auto');
});
it('should handle cursor-gpt-4o model', () => {
const result = resolveModelString('cursor-gpt-4o');
expect(result).toBe('cursor-gpt-4o');
});
it('should add cursor- prefix to bare Cursor model IDs', () => {
const result = resolveModelString('composer-1');
expect(result).toBe('cursor-composer-1');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Detected bare Cursor model ID')
);
});
it('should add cursor- prefix to auto model', () => {
const result = resolveModelString('auto');
expect(result).toBe('cursor-auto');
});
it('should pass through unknown cursor-prefixed models', () => {
const result = resolveModelString('cursor-unknown-future-model');
expect(result).toBe('cursor-unknown-future-model');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Passing through cursor-prefixed model')
);
});
it('should handle all known Cursor model IDs', () => {
const cursorModelIds = Object.keys(CURSOR_MODEL_MAP);
for (const modelId of cursorModelIds) {
const result = resolveModelString(`cursor-${modelId}`);
expect(result).toBe(`cursor-${modelId}`);
}
});
});
describe('with unknown model keys', () => {
it('should return default for unknown model key', () => {
const result = resolveModelString('unknown-model');