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

> Discover how Free-Claude-Code uses environment overrides and Settings.resolve_model() for per-model routing across MODEL_OPUS, MODEL_SONNET, and MODEL_HAIKU. Learn to map Claude models efficiently.

- Repository: [Ali Khokhar/free-claude-code](https://github.com/Alishahryar1/free-claude-code)
- Tags: internals
- Published: 2026-04-24

---

**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`](https://github.com/Alishahryar1/free-claude-code/blob/main/config/settings.py), which declares optional override fields for each Claude family.

### Defining Per-Model Overrides

At lines 126–128 of [`config/settings.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/config/settings.py), the system captures environment variables as optional string fields:

```python
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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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:

```python
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`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/models/anthropic.py) (lines 101–116), the `MessagesRequest` class uses a Pydantic `model_validator` to trigger resolution:

```python
@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`](https://github.com/Alishahryar1/free-claude-code/blob/main/api/models/anthropic.py), ensuring that even non-message endpoints respect the per-model routing configuration:

```python
@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:

```bash

# .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:

```python
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:

```python
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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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.