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
- Startup:
Settingsreads environment variables intomodel_opus,model_sonnet, andmodel_haikufields. - Request receipt: An incoming payload contains
"claude-3-opus"as the model parameter. - Validation trigger: The Pydantic validator calls
settings.resolve_model("claude-3-opus"). - Family detection: The method detects
"opus"in the name and returns the configured override string. - Parsing:
Settings.parse_model_name()splits the provider/model string for downstream use. - 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, orMODEL_HAIKUare unset, the system seamlessly falls back to the genericMODELvalue. - Provider format validation: Unit tests in
tests/config/test_config.pyverify that override strings contain the requiredprovider/modelslash format and that the provider prefix is recognized, raisingValidationErrorfor 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.pyasmodel_opus,model_sonnet, andmodel_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.pyautomatically rewrite model parameters at request time. - Fallback: Unconfigured families default to the global
MODELenvironment variable. - Testing: Comprehensive validation in
tests/config/test_config.pyensures 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →