Skip to main content

LLM request router - picks the right model tier based on prompt complexity, budget, or latency.

Project description

lc-shift

LLM request router that picks the right model tier for each prompt.

Why this exists

Most LLM-powered apps use one model for everything. That's either wasteful (paying frontier prices for "what's 2+2?") or limiting (using the cheap model when you actually need deep reasoning). This is sometimes called the "Two-Language Problem" — borrowed from scientific computing where you prototype in Python but ship in C++. Same idea here: you want the cheap fast thing most of the time and the expensive smart thing when it matters.

lc-shift sits between your app and your providers. You define tiers (e.g. Haiku for simple stuff, Opus for hard stuff), pick a routing strategy, and the router figures out which tier to use. The routing is all CPU heuristics — no API calls, no ML models — so overhead is well under 1ms in practice.

It doesn't call any LLM APIs itself. It just tells you which tier to use, and you make the call with whatever SDK you're already using.

Install

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

Usage

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

config = RouterConfig(
    tiers={
        "performance": ModelTier(
            name="Performance",
            provider="anthropic",
            model_id="claude-opus-4-6",
            cost_per_1k_input=0.015,
            cost_per_1k_output=0.075,
            avg_latency_ms=2500,
        ),
        "economy": ModelTier(
            name="Economy",
            provider="anthropic",
            model_id="claude-haiku-4-5",
            cost_per_1k_input=0.0008,
            cost_per_1k_output=0.004,
            avg_latency_ms=400,
        ),
    },
    default_tier="economy",
    strategy=Strategy.COMPLEXITY,
    complexity_threshold=0.4,
)

router = RouterShifter(config)

async def handle(prompt: str):
    decision = await router.route(ShiftRequest(prompt=prompt))
    print(f"{decision.tier_name}: {decision.reason} ({decision.overhead_ms:.2f}ms)")

    # call your provider, then record usage for cost tracking
    router.record_usage(decision.tier_name, input_tokens=200, output_tokens=500)

asyncio.run(handle("What is 2+2?"))
# economy: complexity=0.00 < threshold=0.4 (0.02ms)

asyncio.run(handle("Analyze this code and explain the trade-off between..."))
# performance: complexity=0.55 >= threshold=0.4 (0.03ms)

Strategies

  • COMPLEXITY — Scores the prompt (length, code blocks, reasoning keywords, multi-step structure). Simple stuff goes cheap, complex stuff goes premium.
  • COST_AWARE — Uses the best tier while budget is healthy, then downshifts as you spend. At 80% consumed it drops to the cheapest tier.
  • CASCADE — Always starts with the cheapest tier. Your app checks the response and escalates if it's not good enough.
  • LATENCY — Picks the most capable tier that fits under your latency target.

Cost tracking

router.record_usage("economy", input_tokens=1000, output_tokens=500)
snap = router.snapshot()
# snap.total_requests, snap.estimated_cost_usd, snap.budget_remaining_usd

Force a tier

decision = await router.route(ShiftRequest(prompt="...", force_tier="performance"))

Development

git clone https://github.com/Saimoguloju/lc-shift.git
cd lc-shift
uv sync --dev
uv run pytest -v
uv run ruff check src/ tests/
uv run mypy src/

What's next

Near-term stuff we're thinking about:

  • Replace the regex heuristics with a small trained classifier (still CPU-only, still fast)
  • Optional provider integrations (lc-shift[anthropic], lc-shift[openai]) so you don't have to wire up the SDK calls yourself
  • Quality feedback loop — if economy tier keeps producing bad results for certain prompt patterns, auto-escalate
  • Multi-provider failover (Anthropic down? reroute to OpenAI)
  • Semantic caching for near-duplicate prompts
  • Some kind of dashboard or OpenTelemetry export for cost/routing visibility

Longer term:

  • A/B routing for model evaluation
  • Mid-stream rerouting during streaming responses
  • Plugin system so you can write custom strategies without forking
  • Multi-modal routing (images/audio have different cost profiles)

Ideas and PRs welcome.

License

MIT

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.0.tar.gz (35.5 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.0-py3-none-any.whl (8.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for lc_shift-0.1.0.tar.gz
Algorithm Hash digest
SHA256 85e842621bc3f9e0b0ab16826b43184a77dea5c39d7ac9fbba26bf6a43d08500
MD5 609fcee8cdaf14c2adade1dd0b736b06
BLAKE2b-256 54b5df03174b34e84752d936548d9d499b045e9c8fad24ed0e85329ff63f1da2

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for lc_shift-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 42c46ef4634170ebeb9a425a5168c4366e16c32658665c6bb119174c45f6dc61
MD5 bda2d9d6d83845e0ec5e176b4acc4880
BLAKE2b-256 a90ea62e02f6a19b977baa6d191b912a1594b855bc7e9f75a59e9e1874989dcd

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