DEV Community

Anushka Shukla
Anushka Shukla

Posted on

Building an LLM Router with Jev: A Practical Approach

When building applications with LLMs, using one model for everything is a reasonable starting point.

But not every request needs the same level of reasoning.

Converting a list into JSON is very different from debugging a race condition in a distributed system. Sending both requests to an expensive model might work, but it may not be the most efficient approach.

What if we could evaluate a request first and then choose an appropriate model?

That is the idea behind LLM routing with Jev.

In this article, we'll explore a practical architecture for routing requests between model tiers, implementing fallback logic in Python, and measuring whether the approach actually improves costs.

1. The architecture

Imagine an application with three model tiers:

Route Best suited for Example
small Simple, repetitive tasks JSON formatting, field extraction
standard Everyday requests Summaries, editing, general questions
advanced Complex reasoning Difficult debugging, system design

The routing pipeline looks like this:

User Request
     |
     v
Jev Evaluation
     |
     v
Application Routing Policy
     |
     v
Selected LLM
     |
     v
Response
Enter fullscreen mode Exit fullscreen mode

The important part is the separation of responsibilities.

  • Jev evaluates the request and provides structured decision outputs.
  • The application decides how to interpret those outputs, applies routing rules, checks availability, and handles fallbacks.
  • The selected LLM performs the actual task.

This separation makes the routing policy easier to test and modify independently of the model provider.

2. Understanding the routing decision

Jev supports typed questions such as choice, score, and noul. These can be used to represent decision-making logic with a shared state and defined options or rubrics.

For the application example below, imagine we normalize the evaluation result into this structure:

{
  "winner": "small",
  "distribution": {
    "small": 0.92,
    "standard": 0.06,
    "advanced": 0.02
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: This JSON is an illustrative application-level representation, not a claim about Jev's exact SDK response format. Map the actual documented output into your application's own schema.

In this example:

  • winner is the leading route.
  • distribution contains the normalized scores associated with the available choices.
  • The application's routing policy uses the winning option's score to decide whether to accept the result.

Conceptually:

routing_score = distribution[winner]
Enter fullscreen mode Exit fullscreen mode

A threshold can help the application decide when to accept a routing decision.

For example, a threshold of 0.80 means the policy accepts a winning score of at least 0.80.

It does not mean the selected LLM has an 80% chance of answering correctly. That interpretation would require empirical calibration and validation.

3. Implementing the routing policy in Python

Let's assume the application has already normalized the evaluation result into the structure shown above.

We also maintain a dictionary of configured model routes:

available_routes = {
    "small": "provider-small-model",
    "standard": "provider-standard-model",
    "advanced": "provider-advanced-model",
}
Enter fullscreen mode Exit fullscreen mode

The model identifiers are placeholders. Replace them with the identifiers supported by your LLM provider.

Now, let's implement route selection with explicit fallback handling.

from typing import Any

ROUTING_THRESHOLD = 0.80
DEFAULT_FALLBACK_ROUTE = "standard"


def choose_route(
    decision: dict[str, Any],
    available_routes: dict[str, str],
    fallback_route: str = DEFAULT_FALLBACK_ROUTE,
) -> str:
    """Select a configured route from normalized decision output."""

    if not available_routes:
        raise RuntimeError("No LLM routes are configured.")

    if fallback_route not in available_routes:
        raise RuntimeError(
            f"Configured fallback route '{fallback_route}' is unavailable."
        )

    winner = decision.get("winner")
    distribution = decision.get("distribution", {})

    if not isinstance(distribution, dict):
        return fallback_route

    score = (
        distribution.get(winner, 0.0)
        if isinstance(winner, str)
        else 0.0
    )

    valid_score = (
        isinstance(score, (int, float))
        and not isinstance(score, bool)
        and 0.0 <= score <= 1.0
    )

    if (
        valid_score
        and score >= ROUTING_THRESHOLD
        and winner in available_routes
    ):
        return winner

    return fallback_route
Enter fullscreen mode Exit fullscreen mode

Here's what this implementation does:

  1. Checks that at least one model route is configured.
  2. Verifies that the designated fallback exists.
  3. Accepts the winning route only when its score meets the threshold and the route is available in the configuration.
  4. Uses the configured fallback when the decision is missing, invalid, or below the threshold.

Notice that the function does not silently select an arbitrary model if the fallback is missing. It raises an error instead.

In production, you should also validate the entire normalized input schema and handle provider health, rate limits, timeouts, user permissions, and model-specific constraints.

4. What happens when the decision is ambiguous?

Consider two illustrative cases.

Case A: No route meets the threshold

small:     0.20
standard:  0.35
advanced:  0.45
Enter fullscreen mode Exit fullscreen mode

The leading option is advanced, but its score is below the configured threshold of 0.80.

Under our example policy, the application selects the configured fallback.

If the fallback is standard, the request goes to standard. If advanced is explicitly configured as the fallback, it goes to advanced.

The choice is determined by the application's policy, not by silently assuming that the leading option is always suitable.

Case B: One route clearly leads

small:     0.94
standard:  0.05
advanced:  0.01
Enter fullscreen mode Exit fullscreen mode

Here, small meets the threshold. The application selects it if the route is configured.

These examples illustrate the mechanics of the routing policy. They do not establish how accurately Jev classifies real requests. That needs to be measured using representative evaluation data.

5. Connecting the policy to an application

The orchestration layer can remain separate from the routing function.

def handle_request(user_prompt, available_routes, model_client):
    # Application-specific integration:
    # Normalize the documented Jev output first.
    decision = evaluate_with_jev(user_prompt)

    route = choose_route(
        decision=decision,
        available_routes=available_routes,
        fallback_route="standard",
    )

    model_id = available_routes[route]

    return model_client.generate(
        model=model_id,
        prompt=user_prompt,
    )
Enter fullscreen mode Exit fullscreen mode

evaluate_with_jev and model_client.generate are placeholders for your actual integration. This is an architectural example, not a drop-in implementation for a specific SDK.

Keeping evaluation, routing, and dispatch separate has a practical benefit: changing the model provider or adjusting the routing policy does not require rewriting the entire request-handling pipeline.

6. Does LLM routing actually save money?

Not necessarily.

The routing step introduces additional computation and latency. A smaller model may also produce an inadequate answer, leading to retries or escalation to a larger model.

To evaluate the trade-off, compare three approaches using the same representative dataset:

Approach What it measures
Always use the advanced model Baseline quality, cost, and latency
Use a simple rule-based router Whether basic routing is sufficient
Use Jev-based routing Whether decision-based routing improves the trade-off

Track these metrics:

  • Total cost: Include the routing step, model calls, retries, and escalations.
  • Task success rate: Measure whether the response meets task-specific correctness criteria.
  • End-to-end latency: Measure the full request lifecycle, not just the final LLM call.
  • Fallback rate: Measure how often the routing policy falls back.
  • Quality by task category: Ensure that savings on simple tasks do not come at the expense of difficult ones.

A useful first step is to run the router in shadow mode. Continue serving requests through the existing pipeline while evaluating proposed routes in the background. Compare the decisions against actual task outcomes before introducing automated routing into live traffic.

The metric I'd pay most attention to is cost per successfully completed task, rather than cost per token alone.

Conclusion

LLM routing offers a way to match model capability to request complexity. But the router is only one part of the system.

A robust implementation needs a clearly defined routing policy, explicit fallback behavior, endpoint validation, and benchmarks that include both cost and output quality.

Jev provides an interesting approach to structured decision-making in this architecture. Whether it produces meaningful savings depends on the workload and the results of the evaluation.

I'd benchmark it against a single-model baseline and a simple rule-based router before making any claims about cost reduction.

Further reading


Top comments (0)