Route Between a Small and a Large Model with Jev
Hosted · ide
Beta

Route Between a Small and a Large Model with Jev

Build the router that decides, per request, whether a cheap fast model can answer or the large model has to. Ask Jev, TypeSafe's decision model, whether a support request combines policy rules, replay recorded answers from GLM-5.3-flash and GLM-5.3 to measure accuracy and cost, tune the threshold on a dev split, confirm it on a test split, and make the router safe for production.

60 min5 steps3 domainsIntermediate

Hands-on labs require Pro · $29.99/mo · cancel anytime

What you'll learn

  1. 1
    Two models, one policy
    Tallyboard's support assistant answers billing questions from
  2. 2
    Ask Jev what the request needs
    A router needs a signal it can read before any model answers. Jev
  3. 3
    Route and score against recorded answers
    Calling both models on every request each time you change a threshold
  4. 4
    Tune on dev, confirm on test
    Picking the threshold that looks best on all 110 requests, then quoting
  5. 5
    Ready for production
    The router now sits in front of every support request, so it needs two

Step 1, as you will see it

This is the lab’s own text. Each step ends with a check that runs your work in the lab environment; the hint and the solution stay inside the lab.

Step 1: Two models, one policy

Tallyboard's support assistant answers billing questions from policy.md: eleven numbered rules about prices, seats, refunds and failed payments. Two models on OpenRouter can do the job:

GLM-5.3-flash (SMALL)GLM-5.3 (LARGE)
Right on the lab's 110 requestsabout 91%about 96%
Cost per answerabout $0.00007about $0.00096

The large model costs fourteen times more per answer. Most requests are lookups ("How much is Team per seat?") that both models get right. The small model's misses are the requests that combine rules, such as a nonprofit discount applied on top of annual pricing, or a refund counted in used months.

Both models reason before they answer. llm.chat(..., reasoning_effort="low") keeps the small model's reasoning short, which is how you would run a cheap tier in production.

Do this

Open router.py.

1. Write extract(text). The model ends with ANSWER: <value>. Return the text after the last ANSWER: (strip spaces and * around it). The small model sometimes drops that line on one-word replies, so fall back to the last non-empty line. Return None for empty or missing text.

2. Write answer(model, request, chat=llm.chat). Make one chat call with a system message (SYSTEM, the instructions plus the policy) and a user message (the request). Use reasoning_effort="low" for SMALL and None for LARGE. Return {"model", "answer", "text", "cost", "ms"}.

3. Run. Five requests go to both models. The lookups come back right from both. Watch #44 (a nonprofit's saving from annual billing) and #56 (seats removed and added on the same day): the small model usually misses at least one of them, for about a fifteenth of the price.

Starter file: jev.py

"""A small client for Jev, TypeSafe's decision model, through the lab's model proxy.

You send a state and some typed questions; Jev sends back one typed answer per
question with probabilities attached. It never writes text.

    jev.post({"model": jev.MODEL, "state": {...}, "questions": {...}})

Identical requests are answered from .cache/jev/, so running a step twice, or
the check after a run, does not ask the model again. Delete .cache/ to start
fresh.
"""
import hashlib
import json
import os
import time
import urllib.error
import urllib.request

# The pod's proxy holds the API key; this lab never sees it.
BASE = os.environ.get("OPENAI_BASE_URL", "http://nim-proxy.labs.svc:8080/v1").rstrip("/")
URL = BASE + "/decisions"
MODEL = "~typesafe/jev-latest"
CACHE_DIR = os.path.join(".cache", "jev")


class JevError(Exception):
    """Jev rejected the request (a malformed question, usually) or could not be reached."""


def _cache_path(payload):
    key = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()[:24]
    return os.path.join(CACHE_DIR, key + ".json")


def post(payload, use_cache=True):
    """Send one decision request and return the parsed response:
    {"model": ..., "answers": {name: {...}}, "usage": {...}}."""
    path = _cache_path(payload)
    if use_cache and os.path.exists(path):
        with open(path, encoding="utf-8") as f:
            return json.load(f)

    data = json.dumps(payload).encode()
    for attempt in range(6):
        req = urllib.request.Request(URL, data=data, headers={"Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = json.loads(resp.read())
            break
        except urllib.error.HTTPError as e:
            detail = e.read().decode(errors="replace")
            if e.code in (429, 500, 502, 503, 529) and attempt < 5:
                # 429 is the proxy's per-minute budget: wait for the next window.
                time.sleep((5 if e.code == 429 else 2) * (attempt + 1))
                continue
            raise JevError(f"Jev answered HTTP {e.code}: {_readable(detail)}") from None
        except (urllib.error.URLError, TimeoutError) as e:
            if attempt < 5:
                time.sleep(2 * (attempt + 1))
                continue
            raise JevError(f"Could not reach the model proxy at {URL}: {e}") from None

    if use_cache:
        os.makedirs(CACHE_DIR, exist_ok=True)
        tmp = path + ".tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(body, f)
        os.replace(tmp, path)
    return body


def _readable(detail):
    """Validation errors arrive as a JSON list inside a JSON string; show the useful part."""
    try:
        msg = json.loads(detail)["error"]["message"]
        issues = json.loads(msg)
        return "; ".join(f"{'.'.join(str(p) for p in i.get('path', []))}: {i.get('message')}" for i in issues)
    except Exception:
        return detail[:300]

Prerequisites

  • Basic Python: functions, dictionaries, lists, lambdas
  • No API keys: the lab's proxy provides access to Jev and both GLM models

Exam domains covered

LLM RoutingCost OptimizationDecision Models

Skills & technologies you'll practice

This intermediate-level ai/ml lab gives you real-world reps across:

JevTypeSafeLLM routingmodel routingGLMOpenRoutercost optimizationdecision modelsintermediate

How to route LLM requests between a small and a large model

Most requests to an LLM feature are easy, and a small model answers them as well as a large one for a fraction of the price. The hard ones are where the small model makes mistakes. A router decides per request. It is only worth having if it spots the hard ones reliably, costs little and adds almost no latency. A decision model like Jev fits that job: one typed question about the request, a probability back in a few hundred milliseconds, and the choice stays in your code. In this lab you route billing questions between GLM-5.3-flash and GLM-5.3 on OpenRouter. You ask Jev whether each request needs several policy rules applied together, and you learn why "would a small model get this wrong?" is the wrong question to ask. You measure routers against recorded answers from both models, including one that routes on message length. Then you tune the threshold on a dev split, check it on a test split it never saw, and add the production parts: what happens when Jev is down, and what happens when the small model returns no usable answer.

Frequently asked questions

Do I need an OpenRouter or Jev API key for this lab?

No. The lab sandbox reaches Jev and both GLM models through Preporato's model proxy, which holds the keys.

Why ask Jev instead of letting the small model decide?

The router runs on every request, so it has to be cheap and fast. Jev answers a typed question about the request in a few hundred milliseconds, for about a twentieth of a cent per thousand tokens, and the routing rule stays in your code where you can test it.

Why replay recorded answers instead of calling the models?

Each model answered every request three times, and the lab stores how often each answer was right, what it cost and how long it took. Replaying those makes every router comparison exact and repeatable. You still call both models live in the first and last steps.

How much does routing save?

On the lab's held-out test split, a threshold tuned on the dev split matches the large model's accuracy for roughly two thirds of its cost. The exact numbers come out of your own run.