Skip to content

Model Resolver ​

The Model Resolver (@protolabsai/model-resolver) converts human-friendly model aliases to full model identifiers used by AI providers. This system enables consistent model selection across protoLabs Studio while simplifying configuration.

Overview ​

Instead of using full model strings like claude-sonnet-4-6-20250929, users can specify aliases like sonnet. The resolver handles:

  • Alias expansion - sonnet → claude-sonnet-4-6
  • Version pinning - Specific versions for reproducibility
  • Provider detection - Auto-detect Claude vs OpenAI vs custom
  • Fallback handling - Graceful degradation when models are unavailable

Model Hierarchy ​

protoLabs Studio uses a three-tier model system:

AliasModel IDProviderUse CaseCost (per 1M tokens)
haikuclaude-haiku-4-5-20251001ClaudeQuick tasks, simple features$0.80 input, $4.00 output
sonnetclaude-sonnet-4-6ClaudeStandard features (default)$3.00 input, $15.00 output
opusclaude-opus-4-6ClaudeArchitectural decisions, complex work$15.00 input, $75.00 output

Auto-escalation: Features that fail 2+ times automatically escalate to opus on retry.

Usage ​

Basic Resolution ​

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

// Alias expansion
resolveModelString('sonnet'); // → 'claude-sonnet-4-6'
resolveModelString('opus'); // → 'claude-opus-4-6'
resolveModelString('haiku'); // → 'claude-haiku-4-5-20251001'

// Pass-through for full model strings
resolveModelString('claude-sonnet-4-6'); // → 'claude-sonnet-4-6'
resolveModelString('gpt-4-turbo'); // → 'gpt-4-turbo'

// Undefined/null returns default
resolveModelString(undefined); // → 'claude-sonnet-4-6'
resolveModelString(null); // → 'claude-sonnet-4-6'

In Agent Configuration ​

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

// Use in agent configuration
const model = resolveModelString('sonnet'); // Returns 'claude-sonnet-4-6'

// Use in auto-mode settings
const config = {
  defaultModel: resolveModelString('sonnet'),
  escalationModel: resolveModelString('opus'),
};

In Feature Execution ​

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

// Model resolution happens inside the Lead Engineer INTAKE phase.
// When creating a feature, specify the alias — the pipeline resolves it:
mcp__protolabs__create_feature({
  projectPath: '/path/to/project',
  title: 'Core Infrastructure',
  model: 'opus', // Resolved to claude-opus-4-6 at INTAKE
  complexity: 'architectural',
});

// Or resolve manually for direct SDK calls:
const model = resolveModelString('opus'); // → 'claude-opus-4-6'

In MCP Tools ​

typescript
// MCP tool call with alias
mcp__protolabs__start_agent({
  projectPath: '/path/to/project',
  featureId: 'feature-123',
  model: 'sonnet', // Automatically resolved to claude-sonnet-4-6
});

// Or use full model string
mcp__protolabs__start_agent({
  projectPath: '/path/to/project',
  featureId: 'feature-123',
  model: 'claude-opus-4-6', // Passed through as-is
});

Supported Aliases ​

Claude Models ​

typescript
const CLAUDE_ALIASES = {
  // Latest generations (recommended)
  opus: 'claude-opus-4-6',
  sonnet: 'claude-sonnet-4-6',
  haiku: 'claude-haiku-4-5-20251001',

  // Versioned aliases (pinned for reproducibility)
  'opus-4-6': 'claude-opus-4-6',
  'sonnet-4-6': 'claude-sonnet-4-6',
  'sonnet-4-5': 'claude-sonnet-4-5',
  'haiku-4-5': 'claude-haiku-4-5-20251001',

  // Legacy (for compatibility)
  'claude-4': 'claude-sonnet-4-6', // Default to sonnet
};

OpenAI Models ​

typescript
const OPENAI_ALIASES = {
  'gpt-4': 'gpt-4-turbo',
  'gpt-3.5': 'gpt-3.5-turbo',
  o1: 'o1-preview',
};

Custom Models ​

Any model string not matching an alias is passed through unchanged:

typescript
resolveModelString('custom-model-v1'); // → 'custom-model-v1'
resolveModelString('local-llama-70b'); // → 'local-llama-70b'

Provider Detection ​

The resolver can extract provider information from model strings:

typescript
import { stripProviderPrefix } from '@protolabsai/types';

// Removes provider prefix
stripProviderPrefix('claude-sonnet-4-6'); // → 'sonnet-4-6'
stripProviderPrefix('gpt-4-turbo'); // → '4-turbo'
stripProviderPrefix('custom:my-model'); // → 'my-model'

// Detect provider
function getProvider(model: string): string {
  if (model.startsWith('claude-')) return 'anthropic';
  if (model.startsWith('gpt-') || model.startsWith('o1-')) return 'openai';
  return 'custom';
}

Configuration ​

Global Default Model ​

Set the default model in settings.json or environment variables:

json
{
  "defaultModel": "sonnet"
}

Or via environment variable:

bash
AUTOMAKER_DEFAULT_MODEL=opus npm run dev

Per-Project Override ​

Override model selection in .automaker/settings.json:

json
{
  "defaultModel": "haiku",
  "complexityThresholds": {
    "small": "haiku",
    "medium": "sonnet",
    "large": "sonnet",
    "architectural": "opus"
  }
}

Per-Feature Override ​

Specify model when creating a feature:

typescript
mcp__protolabs__create_feature({
  projectPath: '/path/to/project',
  title: 'Performance Optimization',
  description: '...',
  model: 'opus', // Override default
  complexity: 'architectural',
});

Complexity-Based Model Selection ​

protoLabs Studio automatically selects models based on feature complexity:

typescript
interface ComplexityMapping {
  small: string; // Trivial tasks → haiku
  medium: string; // Standard features → sonnet
  large: string; // Complex features → sonnet
  architectural: string; // System design → opus
}

const DEFAULT_MAPPING: ComplexityMapping = {
  small: 'haiku',
  medium: 'sonnet',
  large: 'sonnet',
  architectural: 'opus',
};

Usage:

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

function getModelForComplexity(complexity: string): string {
  const mapping = settings.complexityThresholds || DEFAULT_MAPPING;
  const alias = mapping[complexity] || 'sonnet';
  return resolveModelString(alias);
}

// Example
const model = getModelForComplexity('architectural'); // → 'claude-opus-4-6'

Auto-Escalation ​

When a feature fails multiple times, the Lead Engineer state machine auto-escalates to a more capable model:

  1. Feature fails at current tier (e.g., Sonnet)
  2. LeadEngineerService increments failureCount and transitions to ESCALATE
  3. On retry, the INTAKE phase checks failureCount >= 2 and selects Opus
  4. If Opus also fails, the feature stays in ESCALATE for human intervention

This is handled automatically by the pipeline — no manual escalation code needed. The FeatureScheduler tracks failures via DispatchResult outcomes and the circuit breaker pauses after 3 consecutive failures.

Version Pinning ​

For reproducibility, use versioned aliases:

typescript
// ❌ Unpinned (may change over time)
model: 'sonnet'; // Resolves to latest sonnet

// ✅ Pinned (stable)
model: 'sonnet-4-6'; // Always uses claude-sonnet-4-6

// ✅ Fully qualified (most stable)
model: 'claude-sonnet-4-6-20250929'; // Specific build

When to pin:

  • Production deployments
  • Reproducible research
  • Compliance requirements

When to use latest:

  • Development
  • Rapid iteration
  • Benefit from latest improvements

Custom Resolvers ​

You can extend the resolver with custom logic:

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

function customResolve(input: string | undefined): string {
  // Custom logic
  if (input === 'experimental') {
    return 'claude-opus-4-6';
  }

  // Fallback to default resolver
  return resolveModelString(input);
}

// Use in your code
const model = customResolve('experimental'); // → 'claude-opus-4-6'

Testing ​

Unit Tests ​

typescript
import { resolveModelString } from '@protolabsai/model-resolver';
import { describe, it, expect } from 'vitest';

describe('resolveModelString', () => {
  it('resolves aliases', () => {
    expect(resolveModelString('sonnet')).toBe('claude-sonnet-4-6');
    expect(resolveModelString('opus')).toBe('claude-opus-4-6');
    expect(resolveModelString('haiku')).toBe('claude-haiku-4-5-20251001');
  });

  it('passes through full model strings', () => {
    expect(resolveModelString('claude-sonnet-4-6')).toBe('claude-sonnet-4-6');
    expect(resolveModelString('gpt-4-turbo')).toBe('gpt-4-turbo');
  });

  it('returns default for undefined', () => {
    expect(resolveModelString(undefined)).toBe('claude-sonnet-4-6');
    expect(resolveModelString(null)).toBe('claude-sonnet-4-6');
  });
});

Integration Tests ​

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

describe('Model resolution in feature pipeline', () => {
  it('resolves alias to full model string', () => {
    expect(resolveModelString('haiku')).toBe('claude-haiku-4-5-20251001');
    expect(resolveModelString('sonnet')).toBe('claude-sonnet-4-6');
    expect(resolveModelString('opus')).toBe('claude-opus-4-6');
  });

  it('returns default for undefined', () => {
    expect(resolveModelString(undefined)).toBe('claude-sonnet-4-6');
  });
});

Troubleshooting ​

"Model not found" ​

Issue: Provider doesn't recognize resolved model string.

Solution: Check if model ID is current. Update resolver if model was renamed:

typescript
// Update alias mapping in libs/model-resolver/src/index.ts
const CLAUDE_ALIASES = {
  sonnet: 'claude-sonnet-4-7', // Updated
};

"Unexpected model behavior" ​

Issue: Model output differs from expected.

Solution: Verify correct model is resolved:

typescript
import { resolveModelString } from '@protolabsai/model-resolver';

console.log('Resolved model:', resolveModelString('sonnet'));
// Should print: 'claude-sonnet-4-6'

"Cannot resolve custom alias" ​

Issue: Custom alias not recognized.

Solution: Use full model string or update resolver:

typescript
// Option 1: Use full string
model: 'custom-model-v1';

// Option 2: Extend resolver (see Custom Resolvers section)

Learn More ​

Built by protoLabs — Open source on GitHub