Skip to main content

Provider-agnostic LLM router — picks the right model tier per prompt based on complexity, budget, or latency. Sub-1ms, no API calls, 35 built-in providers.

Project description

lc-shift

Provider-agnostic LLM router. Picks the right model tier for every prompt — under 1ms, no API calls, no ML models required.

PyPI version Python License: MIT CI


Why lc-shift?

Most LLM apps use one model for everything — which is either wasteful (paying Opus prices for "what's 2+2?") or limiting (using the cheap model when you need deep reasoning).

lc-shift sits between your app and your providers. You define tiers, pick a routing strategy, and the router decides which tier each prompt deserves. Routing decisions are pure CPU heuristics — no API calls, no ML models, <1ms overhead.

It does not call any LLM APIs. It tells you which tier to use, and you make the call with whatever SDK you're already using — OpenAI, Anthropic, Groq, Ollama, anything.


Install

pip install lc-shift
# or
uv add lc-shift

Requirements: Python 3.11+ · Pydantic v2 (only dependency)


Quick start

import asyncio
from lc_shift import RouterShifter, RouterConfig, ShiftRequest, Strategy, PRESETS

config = RouterConfig(
    tiers=PRESETS["anthropic-3tier"],   # Claude Opus / Sonnet / Haiku
    default_tier="balanced",
    strategy=Strategy.COMPLEXITY,
    complexity_threshold=0.4,
)

async def main():
    async with RouterShifter(config) as router:
        decision = await router.route(ShiftRequest(prompt="What is 2+2?"))
        print(f"{decision.tier_name}: {decision.reason} ({decision.overhead_ms:.2f}ms)")
        # economy: complexity=0.00 < threshold=0.4 (0.02ms)

        result = await call_your_llm(decision.tier)   # your code here
        router.record_usage(decision.tier_name, input_tokens=20, output_tokens=5)

asyncio.run(main())

Routing strategies

Strategy When to use
COMPLEXITY Score each prompt 0–1 (length + code + reasoning keywords + multi-step structure). Simple → cheap, complex → premium.
COST_AWARE Use the best tier while budget is healthy, downshift as spend grows. At 80% consumed → cheapest.
CASCADE Always start with the cheapest tier. Your app checks quality and escalates if needed.
LATENCY Pick the most capable tier that fits under your latency target (ms).

Pre-configured providers (35 models, 24 providers)

Use any model without manually entering cost and latency numbers.

from lc_shift import ANTHROPIC, OPENAI, GOOGLE, DEEPSEEK, GROQ, OLLAMA

config = RouterConfig(
    tiers={
        "performance": ANTHROPIC["claude-opus-4-6"],
        "balanced":    OPENAI["gpt-4o"],
        "economy":     GOOGLE["gemini-flash"],
    },
    default_tier="balanced",
    strategy=Strategy.COMPLEXITY,
)

Available providers

Provider Keys
ANTHROPIC claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5
OPENAI gpt-5.5, gpt-4o, gpt-4o-mini
GOOGLE gemini-3.1-pro-preview, gemini-flash, gemini-gemini-cli-flash
DEEPSEEK deepseek-v4-flash, deepseek-v3.2, deepseek-r1-huggingface, deepseek-v3.2-deepinfra
MISTRAL mistral-large-latest
XAI grok-4.3
MOONSHOT kimi-k2.6, kimi-for-coding, kimi-k2.5-together
QWEN qwen3.5-plus
MINIMAX minimax-m2.7
NVIDIA nemotron-super-120b
GROQ llama3-groq
CEREBRAS zai-glm-4.7
OPENROUTER auto
VERCEL claude-opus-via-vercel
QIANFAN deepseek-v3.2-qianfan
STEPFUN step-3.5-flash
XIAOMI mimo-v2-flash
VOLCENGINE ark-code-latest
BYTEPLUS ark-code-latest
GITHUB_COPILOT copilot-default
OLLAMA llama3.3 (local, free)
LMSTUDIO gpt-oss-20b (local, free)
VLLM custom (local, free)
SGLANG custom (local, free)

Ready-made presets

from lc_shift import PRESETS

PRESETS["anthropic-3tier"]   # Claude Opus / Sonnet / Haiku
PRESETS["openai-3tier"]      # GPT-5.5 / GPT-4o / GPT-4o-mini
PRESETS["mixed-frontier"]    # Claude Opus / GPT-4o / Gemini Flash
PRESETS["cost-optimized"]    # DeepSeek V3.2 / DeepSeek Flash / Ollama
PRESETS["speed-first"]       # Groq / DeepSeek Flash / vLLM
PRESETS["local-only"]        # Ollama / vLLM / sglang (zero cost)

Fallback chains

When a provider goes down, route_with_fallback() returns an ordered list of healthy tiers automatically.

from lc_shift import RouterShifter, TierHealth

health = TierHealth(cooldown_seconds=60)
router = RouterShifter(config, health=health)

chain = await router.route_with_fallback(request)

for decision in chain:
    try:
        result = await call_llm(decision.tier)
        router.record_usage(decision.tier_name, input_tokens=200, output_tokens=500)
        break
    except ProviderError as exc:
        router.mark_tier_failed(decision.tier_name)   # skip for 60s
else:
    raise RuntimeError("All tiers exhausted")

print(chain.skipped_tiers)   # ['performance']  — tiers that were degraded

Tiers auto-recover after cooldown_seconds. You can also call router.recover_tier("performance") to clear it manually.


Batch routing

Route multiple prompts concurrently in a single call.

decisions = await router.route_batch([
    ShiftRequest(prompt="Quick question"),
    ShiftRequest(prompt="Deep multi-step analysis with code review..."),
    ShiftRequest(prompt="Translate to French"),
])
# Returns list[RoutingDecision] in the same order, all routed concurrently.

Observability hooks

Plug in your own logging, alerting, or OpenTelemetry tracing with zero coupling.

from lc_shift import HookRegistry, RouterShifter

hooks = HookRegistry()

@hooks.on_route
def log_decision(request, decision):
    print(f"[{decision.tier_name}] {decision.overhead_ms:.2f}ms — {decision.reason}")

@hooks.on_usage
async def push_metrics(tier_name, input_tokens, output_tokens):
    await metrics.record(tier_name, input_tokens, output_tokens)

@hooks.on_fallback
async def alert_on_degraded(failed_tier, next_tier, exc):
    await slack.send(f"Provider degraded: {failed_tier} -> {next_tier}")

@hooks.on_error
def log_error(request, exc):
    logger.error(f"Routing failed: {exc}")

router = RouterShifter(config, hooks=hooks)

Hooks can be sync or async — both work.


Routing cache

Cache routing decisions for identical prompts to eliminate repeated scoring overhead.

from lc_shift import RoutingCache, RouterShifter

cache = RoutingCache(ttl_seconds=120, max_size=2000)
router = RouterShifter(config, cache=cache)

d1 = await router.route(ShiftRequest(prompt="Explain transformers"))
# overhead: 0.04ms, cache_hit: False

d2 = await router.route(ShiftRequest(prompt="Explain transformers"))
# overhead: 0.00ms, cache_hit: True

print(cache.size)        # 1
print(cache.total_hits)  # 1

Cost tracking and metrics

router.record_usage("balanced", input_tokens=500, output_tokens=1200)

snap = router.snapshot()
snap.total_requests          # int
snap.estimated_cost_usd      # float
snap.budget_remaining_usd    # float | None
snap.cache_hit_rate          # float  (0.0 – 1.0)
snap.degraded_tiers          # list[str]
snap.tier_metrics            # dict[str, TierMetrics]

# Per-tier breakdown
m = snap.tier_metrics["balanced"]
m.requests           # int
m.input_tokens       # int
m.output_tokens      # int
m.estimated_cost_usd # float

Budget-aware routing

config = RouterConfig(
    tiers=PRESETS["mixed-frontier"],
    default_tier="balanced",
    strategy=Strategy.COST_AWARE,
    cost_budget_usd=10.00,    # auto-downshift as spend grows
)

Custom ModelTier

You can define any provider not in the built-in catalog:

from lc_shift import ModelTier, RouterConfig

config = RouterConfig(
    tiers={
        "my-model": ModelTier(
            name="My Custom Model",
            provider="my-provider",
            model_id="my-model-v1",
            cost_per_1k_input=0.001,
            cost_per_1k_output=0.003,
            avg_latency_ms=800,
            max_tokens=8192,
        ),
    },
    default_tier="my-model",
    strategy=Strategy.CASCADE,
)

Force a specific tier

decision = await router.route(ShiftRequest(prompt="...", force_tier="performance"))
# reason: "force_tier override"

All features together

from lc_shift import (
    RouterShifter, RouterConfig, ShiftRequest, Strategy,
    HookRegistry, RoutingCache, TierHealth,
    PRESETS,
)

hooks = HookRegistry()

@hooks.on_route
def log(request, decision):
    print(f"[{decision.tier_name}] cache={decision.cache_hit}")

router = RouterShifter(
    RouterConfig(tiers=PRESETS["mixed-frontier"], default_tier="balanced"),
    hooks=HookRegistry(),
    cache=RoutingCache(ttl_seconds=60),
    health=TierHealth(cooldown_seconds=30),
)

async with router:
    chain = await router.route_with_fallback(ShiftRequest(prompt="..."))
    decisions = await router.route_batch([ShiftRequest(prompt=p) for p in prompts])
    snap = router.snapshot()

Development

git clone https://github.com/Saimoguloju/lc-shift.git
cd lc-shift
uv sync --dev

uv run pytest -v          # 36 tests, <1s
uv run ruff check src/ tests/
uv run mypy src/

CI runs the full matrix: Ubuntu × macOS × Windows × Python 3.11 / 3.12 / 3.13.


Contributing

Issues and PRs are welcome. If you're adding a new provider preset, edit src/lc_shift/providers.py — follow the existing pattern and include approximate cost/latency values.

If you're adding a new routing strategy, subclass BaseStrategy in src/lc_shift/strategies.py and register it in STRATEGY_MAP.


License

MIT — see LICENSE.

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

lc_shift-0.1.1.tar.gz (47.6 kB view details)

Uploaded Source

Built Distribution

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

lc_shift-0.1.1-py3-none-any.whl (20.5 kB view details)

Uploaded Python 3

File details

Details for the file lc_shift-0.1.1.tar.gz.

File metadata

  • Download URL: lc_shift-0.1.1.tar.gz
  • Upload date:
  • Size: 47.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.4

File hashes

Hashes for lc_shift-0.1.1.tar.gz
Algorithm Hash digest
SHA256 90d7ea7f685db27b65c4794edb7e9579f70825000e91599a25e5f2984623d51c
MD5 f8fbdb116af8457c5d098eccbb5bb12c
BLAKE2b-256 68be208961c7b3fc0807362911dbb4d1515a79ab4464611bddad17812a4754eb

See more details on using hashes here.

File details

Details for the file lc_shift-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: lc_shift-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.4

File hashes

Hashes for lc_shift-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 399e28bdc4a01c2a68222185a799d632be48bd2edd44c8623ac16a64b0c57ce5
MD5 dfabf8a05e1344e6ff0a4c7d09f97e95
BLAKE2b-256 a06794e29b4e8cc752c8e504806dc3c3f7b0b00bcefe4b1379952d27b6f71dbd

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