Skip to main content

Intelligent multi-model LLM router with auto-calibration and multilingual support

Project description

Ecate.LLM

Ecate.LLM

Ecate (Ἑκάτη) — Greek goddess of crossroads, guiding travelers to the right path.

An OpenAI-compatible proxy that learns to route requests across your models — you bring the models, Ecate learns which one handles what best.

Tests

Features

  • Calibrate your own models — no pre-built rankings, learns routing from your actual model lineup
  • Multilingual routing — automatic language detection (55+ languages) with per-model language filters
  • Provider-agnostic — OpenAI, Anthropic, Ollama, vLLM, or any OpenAI-compatible endpoint
  • Embedding-based routing — fast local inference (~10ms), no external API calls for routing
  • Cost-aware strategies — balance quality vs cost with configurable auto, best, or cheap modes
  • Automatic fallback — circuit breaker detects failures and routes around unavailable models
  • Real-time dashboard — monitor costs, latency, and routing decisions per model
  • OpenAI-compatible API — drop-in replacement, works with any OpenAI SDK

Why

You're using your most capable and expensive model on every request, but 60% of your queries could be handled by a model that costs 50x less. Ecate fixes this automatically.

Without Ecate With Ecate
Every request → your most expensive model Simple requests → cheapest capable model
Manual model selection per endpoint One endpoint, automatic routing
No visibility into what you're spending Real-time dashboard with cost breakdown
One model fails → your app fails Automatic fallback to next-best model

Bring Your Own Models

Ecate doesn't ship with hardcoded model rankings. You configure the models you have access to — whether that's OpenAI, Anthropic, local Ollama, or any OpenAI-compatible endpoint — and Ecate learns how to route between them.

# config.yaml — your models, your providers, your costs
providers:
  - id: anthropic
    models: [claude-sonnet-4, claude-haiku]
  - id: openai  
    models: [gpt-4o, gpt-4o-mini]
  - id: local
    base_url: http://localhost:11434/v1
    models: [llama3, mistral]

When you run calibration, Ecate evaluates your specific models on benchmark tasks and learns a routing vector for each one. This means:

  • No generic rankings — routing is tuned to how your models actually perform
  • Mix any providers — compare Claude vs GPT vs local models fairly
  • Your costs, your tradeoffs — calibration respects the pricing you configure
┌─────────────────────────────────────────────────────────────┐
│  Calibration: "How does each of MY models handle this?"    │
│                                                             │
│  Task: "Write a SQL query..."                               │
│    → claude-sonnet: ✓ pass                                  │
│    → gpt-4o-mini:   ✓ pass                                  │
│    → llama3:        ✗ fail                                  │
│                                                             │
│  Result: learned vectors that reflect YOUR model lineup    │
└─────────────────────────────────────────────────────────────┘

After calibration, Ecate routes each request to the best model from your configured set:

Your app  →  Ecate  →  Best model (from YOUR models)
                ↓
         "What's 2+2?"  →  gpt-4o-mini ($0.0002)
         "Design a distributed system"  →  claude-sonnet ($0.015)
         "Analizza questa architettura"  →  gpt-4o ($0.008)

How Routing Works

Ecate uses embedding-based routing with vectors learned from calibration:

  1. Embed the prompt locally (~10ms) using a multilingual model (512d)
  2. Score each model via dot product with that model's learned vector
  3. Apply strategy to balance quality vs cost
  4. Route to optimal model from your configured set
Prompt → embed(512d) → dot(model_vectors) → score - λ*log(cost) → select

The key insight: models that excel at certain prompt types will have vectors that align with those prompts in embedding space. Calibration learns these vectors for your specific models.

Dashboard

Access at http://localhost:8000/dashboard:

  • Overview — Cost breakdown, request count, latency
  • Models — Add/edit/remove models, view costs
  • Routing — Recent routing decisions, model distribution
  • Calibrate — Per-model calibration controls, run history

Overview Dashboard

Quick Start

1. Configure

cp config.example.yaml config.yaml

Add your API keys and models:

providers:
  - id: anthropic
    type: anthropic
    base_url: https://api.anthropic.com
    api_key: ${ANTHROPIC_API_KEY}
    models:
      - id: claude-sonnet-4-20250514
        input_cost_per_mtok: 3.0
        output_cost_per_mtok: 15.0
        max_context: 200000
        supports_tools: true
        supports_vision: true

  - id: openai
    type: openai
    base_url: https://api.openai.com/v1
    api_key: ${OPENAI_API_KEY}
    models:
      - id: gpt-4o
        input_cost_per_mtok: 2.5
        output_cost_per_mtok: 10.0
        max_context: 128000
        supports_tools: true
        supports_vision: true
      - id: gpt-4o-mini
        input_cost_per_mtok: 0.15
        output_cost_per_mtok: 0.6
        max_context: 128000
        supports_tools: true

2. Run

# Docker
docker run -v ./config.yaml:/app/config.yaml -p 8000:8000 ghcr.io/nullcline-labs/ecate-llm

# Or install directly
pip install ecate-llm[router]
ecate --config config.yaml

3. Calibrate

Before routing works, you need to calibrate your models:

# Via dashboard: http://localhost:8000/dashboard → Calibrate tab
# Or via API:
curl -X POST http://localhost:8000/api/calibration/start \
  -H "Authorization: Bearer ek-..." \
  -H "Content-Type: application/json"

Calibration evaluates each model on ~200 tasks and learns a routing vector.

4. Use

Replace your OpenAI base URL. That's it.

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="ek-..."  # Your Ecate API key (printed on first run)
)

# Ecate routes automatically
response = client.chat.completions.create(
    model="auto",  # Let Ecate decide
    messages=[{"role": "user", "content": "What's the capital of France?"}]
)
print(response.headers["X-Ecate-Routed-To"])  # → openai/gpt-4o-mini

Routing Strategies

Use the model field to control routing:

Model Value Behavior
"auto" Best tradeoff: score - λ*log(cost)
"auto:best" Highest embedding score regardless of cost
"auto:cheap" Cheapest model above score threshold
"openai/gpt-4o" Direct passthrough — bypass routing

The cost_weight parameter (λ, default 0.3) controls the cost/quality tradeoff in auto mode.

Calibration

Calibration teaches Ecate how each model performs:

  1. Run tasks — Each model answers ~200 benchmark tasks
  2. Judge responses — An LLM judge scores each response (pass/fail)
  3. Learn vectors — Logistic regression learns a 512d vector per model
  4. Save vectors — Stored in data/model_vectors.json

Per-Model Calibration

In the dashboard, each model card shows:

  • Calibration toggle — Include in batch calibration
  • Samples setting — Tasks per model (default 200)
  • Calibrate button — Run single-model calibration
  • Status badge — "Ready" (green) or "Not calibrated" (orange)

Adding a New Model

To add a new model without re-calibrating everything:

  1. Add the model to config.yaml
  2. Go to Dashboard → Calibrate
  3. Click "Calibrate" on the new model card
  4. Wait ~5 minutes for ~200 evaluations
  5. Model is now routable

Custom Tasks

Add domain-specific tasks:

# my_tasks/customer_support.yaml
- id: support_001
  category: instruction_following
  difficulty: moderate
  language: it
  prompt: |
    Rispondi al cliente che chiede un rimborso per un prodotto
    difettoso acquistato 3 mesi fa.
  expected_behavior: |
    Should acknowledge the issue, express empathy, and offer a solution.
# config.yaml
calibration:
  custom_tasks_dir: ./my_tasks

Multilingual Support

Ecate supports routing across languages:

  • Embedder — Uses distiluse-base-multilingual-cased-v1 (50+ languages)
  • Language detection — Automatically detects the language of incoming prompts
  • Per-model language filter — Restrict which languages route to which models

Language Filtering

By default, all models are considered for all languages. To restrict a model to specific languages:

models:
  - id: gpt-4o
    # No supported_languages → routes for any language
    
  - id: local-italian-model
    supported_languages: [it]  # Only Italian prompts route here
    
  - id: multilingual-model
    supported_languages: [en, fr, de, es]  # Explicit allowlist

When a prompt comes in, Ecate:

  1. Detects the language
  2. Filters to models that support that language
  3. Routes among the remaining candidates

This lets you mix specialized single-language models with general multilingual ones.

Architecture

Client (any OpenAI SDK)
       │
       ▼
┌─────────────────────────────┐
│     Ecate Proxy :8000       │
│                             │
│  Auth → Language Detect     │
│           ↓                 │
│     Local Embedder (10ms)   │  ← distiluse 512d
│           ↓                 │
│     Vector Router           │  ← dot(prompt, model_vectors)
│           ↓                 │
│     Format Bridge           │  ← OpenAI ↔ Anthropic
│           ↓                 │
│     Usage Logger            │  ← Async SQLite writes
│                             │
│  Dashboard + API            │
│  data/model_vectors.json    │
│  data/ecate.db              │
└─────────────────────────────┘
       │
  ┌────┼────┐
  ▼    ▼    ▼
 Any  Any  Any     ← Anthropic, OpenAI, Ollama, vLLM, ...

Single container. No Redis, no Postgres, no external dependencies.

Configuration Reference

server:
  host: 0.0.0.0
  port: 8000

routing:
  default_strategy: auto          # auto | best | cheap
  vectors_path: data/model_vectors.json
  embedder_model: sentence-transformers/distiluse-base-multilingual-cased-v1
  cost_weight: 0.3                # λ for auto strategy
  score_threshold: 0.0            # Min score for cheap strategy
  enable_fallback: true
  max_retries: 1
  circuit_breaker_threshold: 3

calibration:
  judge_model: anthropic/claude-sonnet-4-20250514
  concurrency_per_provider: 3
  samples_per_model: 200
  custom_tasks_dir: null
  languages: [en, de, fr]          # Filter calibration tasks by language (null = all)

providers:
  - id: anthropic
    type: anthropic               # anthropic | openai | openai-compatible
    base_url: https://api.anthropic.com
    api_key: ${ANTHROPIC_API_KEY}
    models:
      - id: claude-sonnet-4-20250514
        input_cost_per_mtok: 3.0
        output_cost_per_mtok: 15.0
        max_context: 200000
        supports_tools: true
        supports_vision: true
        supported_languages: [en, de]  # Optional: only route these languages to this model
        calibration_enabled: true      # Include in calibration
        calibration_samples: 200       # Tasks for this model

Resilience

  • Automatic fallback — If selected model fails, routes to next-best
  • Circuit breaker — 3 failures in 5 minutes → model temporarily removed
  • Per-key budgets — Daily/monthly cost limits per API key

Development

git clone https://github.com/nullcline-labs/ecate.llm
cd ecate.llm

# Install with router dependencies
pip install -e ".[router,dev]"

# Run tests
pytest tests/ -v

# Run locally
python -m ecate.app --config config.yaml

API Reference

# Proxy
POST /v1/chat/completions    # OpenAI-compatible
GET  /v1/models              # List available models

# Dashboard
GET  /api/overview
GET  /api/cost/by-model
GET  /api/cost/over-time
GET  /api/routing-table
GET  /api/requests

# Models
GET    /api/models
POST   /api/models
PUT    /api/models/{provider}/{model}
DELETE /api/models/{provider}/{model}
PUT    /api/models/{provider}/{model}/calibration

# Calibration
POST /api/calibration/start
GET  /api/calibration/progress
GET  /api/calibration/runs
GET  /api/calibration/runs/{id}/results

# Routing
GET  /api/routing/vectors

# Health
GET  /health

License

AGPL-3.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ecate_llm-0.1.0.tar.gz (92.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ecate_llm-0.1.0-py3-none-any.whl (89.3 kB view details)

Uploaded Python 3

File details

Details for the file ecate_llm-0.1.0.tar.gz.

File metadata

  • Download URL: ecate_llm-0.1.0.tar.gz
  • Upload date:
  • Size: 92.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for ecate_llm-0.1.0.tar.gz
Algorithm Hash digest
SHA256 50cf82de7e43a1ada3acd260b8dd04db5a17cc4567cb204dedc0f869d5442e34
MD5 c22cd96502e9c31ffb2d08e58be5e9c0
BLAKE2b-256 0ccd8b5e9b2050aa3821299ba6caccb2e52767f20ff51d47fcce89c3730e80dc

See more details on using hashes here.

File details

Details for the file ecate_llm-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ecate_llm-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 89.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for ecate_llm-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5dfade8154074932645a1e4042acdcf8941ee493e0c81b24c20b351222ec7782
MD5 c6d301c0e59b2233cbf79b5040a3e3d9
BLAKE2b-256 7c8ba789c698af021ca9b5c6ccfb31966b829390beec36325c1831fc4285f012

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page