How Free-Claude-Code Implements Per-Model Routing for MODEL_OPUS, MODEL_SONNET, and MODEL_HAIKU

Free-Claude-Code implements per-model routing for MODEL_OPUS, MODEL_SONNET, and MODEL_HAIKU through environment variable overrides and the Settings.resolve_model() method, which maps Claude family names to provider-specific model strings at request validation time.

The free-claude-code repository provides a proxy layer that allows you to redirect requests for specific Claude model families—Opus, Sonnet, and Haiku—to arbitrary backend providers. This per-model routing system relies on Pydantic-based configuration validation and automatic request rewriting to decouple the Anthropic API interface from the actual inference provider.

Configuration Layer: Environment Variables and Settings

The routing logic begins with the Settings class in config/settings.py, which declares optional override fields for each Claude family.

Defining Per-Model Overrides

At lines 126–128 of config/settings.py, the system captures environment variables as optional string fields:

model_opus: str | None = Field(default=None, validation_alias="MODEL_OPUS")
model_sonnet: str | None = Field(default=None, validation_alias="MODEL_SONNET")
model_haihu: str | None = Field(default=None, validation_alias="MODEL_HAIKU")

When the application starts, Pydantic's BaseSettings reads the environment. If MODEL_OPUS=open_router/deepseek/deepseek-r1 is present, self.model_opus stores that provider string; otherwise it remains None.

The Resolution Algorithm

The core routing decision happens in Settings.resolve_model(), defined at lines 300–313 in config/settings.py.

How resolve_model() Maps Claude Names to Providers

The method performs case-insensitive substring matching to identify which Claude family a request targets:

def resolve_model(self, claude_model_name: str) -> str:
    name_lower = claude_model_name.lower()
    if "opus" in name_lower and self.model_opus is not None:
        return self.model_opus
    if "sonnet" in name_lower and self.model_sonnet is not None:
        return self.model_sonnet
    if "haiku" in name_lower and self.model_haiku is not None:
        return self.model_haiku
    return self.model  # Fall back to generic MODEL

Case-insensitivity is handled by lowercasing the input first, so claude-OPUS-4 matches correctly. If no override is configured for the detected family, the system falls back to the global MODEL environment variable.

Request Validation and Automatic Routing

Once configuration is loaded, the application intercepts incoming API requests and rewrites the model parameter before forwarding to the backend provider.

Anthropic Request Models

In api/models/anthropic.py (lines 101–116), the MessagesRequest class uses a Pydantic model_validator to trigger resolution:

@model_validator(mode='after')
def resolve_model_name(self):
    if self.original_model:
        resolved_full = settings.resolve_model(self.original_model)
        self.model = Settings.parse_model_name(resolved_full)
    return self

The validator calls settings.resolve_model() with the user-supplied Claude name (e.g., "claude-3-opus"), receives the full provider string (e.g., "open_router/deepseek/deepseek-r1"), then extracts just the model portion via Settings.parse_model_name().

Token Count Validation

The same pattern appears in TokenCountRequest at lines 31–35 of api/models/anthropic.py, ensuring that even non-message endpoints respect the per-model routing configuration:

@field_validator('model')
def validate_model(cls, v):
    resolved_full = settings.resolve_model(v)
    return Settings.parse_model_name(resolved_full)

Complete Implementation Flow

  1. Startup: Settings reads environment variables into model_opus, model_sonnet, and model_haiku fields.
  2. Request receipt: An incoming payload contains "claude-3-opus" as the model parameter.
  3. Validation trigger: The Pydantic validator calls settings.resolve_model("claude-3-opus").
  4. Family detection: The method detects "opus" in the name and returns the configured override string.
  5. Parsing: Settings.parse_model_name() splits the provider/model string for downstream use.
  6. Dispatch: The provider-specific client receives the resolved model identifier and routes the request to the correct backend.

Configuration Examples

Set your overrides in the environment or .env file:


# .env

MODEL=nvidia_nim/fallback-model
MODEL_OPUS=open_router/deepseek/deepseek-r1
MODEL_SONNET=open_router/anthropic/claude-3-sonnet
MODEL_HAIKU=lmstudio/qwen2.5-7b

Access the routing logic programmatically:

from free_claude_code.config.settings import get_settings

settings = get_settings()

# Resolution with override present

assert settings.resolve_model("claude-3-opus") == "open_router/deepseek/deepseek-r1"

# Fallback when override is absent

settings.model_opus = None
assert settings.resolve_model("claude-3-opus") == "nvidia_nim/fallback-model"

When constructing API requests, the resolution happens automatically:

from free_claude_code.api.models.anthropic import MessagesRequest

req = MessagesRequest(
    model="claude-3-opus",  # Input: Anthropic model name

    messages=[{"role": "user", "content": "Hello"}]
)

# After validation: req.model == "deepseek/deepseek-r1"

Edge Cases and Validation

The implementation handles several edge cases to ensure robust routing:

  • Missing overrides: When MODEL_OPUS, MODEL_SONNET, or MODEL_HAIKU are unset, the system seamlessly falls back to the generic MODEL value.
  • Provider format validation: Unit tests in tests/config/test_config.py verify that override strings contain the required provider/model slash format and that the provider prefix is recognized, raising ValidationError for malformed inputs.
  • Case sensitivity: All family detection is case-insensitive through explicit lowercasing before substring matching.

Summary

  • Configuration: Per-model overrides are defined in config/settings.py as model_opus, model_sonnet, and model_haiku, sourced from environment variables.
  • Resolution: Settings.resolve_model() uses substring matching to map Claude family names to provider-specific strings.
  • Validation: Pydantic validators in api/models/anthropic.py automatically rewrite model parameters at request time.
  • Fallback: Unconfigured families default to the global MODEL environment variable.
  • Testing: Comprehensive validation in tests/config/test_config.py ensures proper formatting and provider recognition.

Frequently Asked Questions

How does free-claude-code determine which provider to use for a Claude model request?

The system inspects the incoming model name for substrings ("opus", "sonnet", or "haiku") and checks if a corresponding environment variable (MODEL_OPUS, MODEL_SONNET, or MODEL_HAIKU) is configured. If a match exists, it returns that provider string; otherwise it falls back to the generic MODEL setting.

Can I route different Claude families to completely different providers?

Yes. Each family can point to a distinct provider or model. For example, you can route Opus to OpenRouter, Sonnet to a local LMStudio instance, and Haiku to NVIDIA NIM by setting the respective environment variables to provider-specific strings like open_router/model-id, lmstudio/model-id, or nvidia_nim/model-id.

What happens if I don't set MODEL_OPUS, MODEL_SONNET, or MODEL_HAIKU?

If a specific override is not set, resolve_model() returns the value of the global MODEL environment variable. This ensures the application remains functional even without per-family configuration, using a single default provider for all requests.

Is the model name matching case-sensitive?

No. The resolution algorithm converts the input model name to lowercase before checking for "opus", "sonnet", or "haiku", so variations like Claude-3-OPUS or CLAUDE_SONNET are recognized correctly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →