How lemon-code scores HTTP providers with readable local factors, alongside a separate configurable JEV layer
Published: October 26, 2026
Reading time: 9 min
Series: lemon-code in 30 days, part 4 of 13
"Why did it send this database migration to the cheapest model?" I asked myself that on a Tuesday night, staring at a wrong diff. There was no answer. The router had decided, and the decision left no trace.
On an HTTP-provider route, lemon-code can record a breakdown in this format. The values below are illustrative:
anthropic score 71.5 (order +50.0, risk +20.0, history +1.5)
This post explains the baseline score for HTTP providers: task signals, factors summed after readiness, and a breakdown of the terms that mattered. This format does not represent CLI runtime decisions or every dispatch layer.
Think of a hiring panel using a scorecard. Each criterion adds or subtracts points. At the end you know who won, by how much and why. If the choice looks wrong, you read the card and find the criterion with the bad weight.
The scorecard describes the local, deterministic baseline for HTTP providers. lemon-code also has a separate JEV path, configurable in shadow, advisory and enforce modes. It uses local inference through Laya when available and can call the Vercel Gateway when a key is configured; its effect depends on the selected mode.
This teaching example of the baseline score fits in a few lines. Its weights illustrate the format; they are not production defaults. The trick is keeping every term of the sum:
// minimal additive router (illustrative, no dependencies)
type Factor = { label: string; value: number };
type Task = { risk: number; vision: boolean };
type Provider = "anthropic" | "openai" | "google";
function scoreProvider(name: Provider, task: Task, position: number, historyBonus = 0) {
const factors: Factor[] = [{ label: "order", value: 50 - position * 0.5 }];
if (task.risk >= 7) factors.push({ label: "risk", value: name === "anthropic" ? 20 : -4 });
if (task.vision) factors.push({ label: "vision", value: name === "google" ? 22 : -18 });
if (historyBonus !== 0) factors.push({ label: "history", value: historyBonus });
const score = factors.reduce((s, f) => s + f.value, 0);
return { name, score, factors };
}
const ranked = [
scoreProvider("anthropic", { risk: 8, vision: false }, 0, 1.5),
scoreProvider("openai", { risk: 8, vision: false }, 1),
scoreProvider("google", { risk: 8, vision: false }, 2),
]
.sort((a, b) => b.score - a.score);
const describeFactor = (f: Factor) => f.label + " " + (f.value >= 0 ? "+" : "") + f.value.toFixed(1);
console.log(ranked.map((r) =>
r.name + " score " + r.score.toFixed(1) + " (" + r.factors.map(describeFactor).join(", ") + ")"
).join(" | "));
// anthropic score 71.5 (order +50.0, risk +20.0, history +1.5)
// openai score 45.5 (order +49.5, risk -4.0) | google score 45.0 (order +49.0, risk -4.0)
Run it with bun router.ts. A risk-8 task pushes the strong model to the top, and you know exactly which term did it.
Before looking at providers, lemon-code scores the task on five local dimensions:
| Dimension | Signals |
|---|---|
| Complexity | intent, prompt size, files and code blocks |
| Risk | words like auth, payment, database, migration, production, secret |
| Context | size, @-referenced files, code fences |
| Multimodal | image attachments |
| Cost sensitivity | explicit "cheap" or "fast" requests and local machine load |
Risk is the simplest and most useful. A list of twelve words (auth, billing, migration, deploy, delete, token, secret and others) already separates "rename this variable" from "change the payments migration".
Complexity falls into three documented bands: 1–3 for reads and short questions, 4–7 for localized refactors and ordinary debugging, 8–10 for multi-file changes and architecture.
Each ready provider gets a list of factors with the same shape as the minimal example: an order factor that keeps the configured preference, a risk factor that favors strong models on critical tasks, a vision factor when there's an image, and others for context, cost and strategy.
The golden rule: readiness comes before the baseline score. An unavailable provider never enters the sum and can produce a diagnostic with a setup hint.
Local SQLite stores per-provider successes, errors, cancellations, average latency, estimated cost and fallback rate. Those numbers become a history factor. The catch is not letting history dominate. It adjusts the order without erasing risk. Otherwise a cheap model that nails easy tasks ends up getting the hard ones.
Nobody reads a fifteen-factor breakdown. For HTTP-provider routes, lemon-code's formatter drops factors under 0.05 in absolute value, sorts by impact and shows the top five; the router stores the result in scoreBreakdown. CLI runtime routes do not use this same breakdown. The same data goes to decision history and, if enabled, to OpenTelemetry as lemon.dispatch.task.score (post 8).
Explainable routing is a sum that keeps its terms. When a choice looks wrong, you open the card, find the bad factor and fix it with a test.
Wednesday's post covers what happens after the choice: guardrails, permission modes and approvals.
If your team is building a model orchestrator and needs to justify each decision for audit or cost, that's one of the areas I help with at LEMON.DEV.
Free and paid products to turn ideas into a foundation you can actually ship.
13 products availableContinue exploring similar topics
I reached for an HTTP provider on tasks an installed CLI could handle. lemon-code's CLI-first rule fits in four lines of precedence.
lemon-code has six ADRs numbered 0049, all created on the same day. That mistake teaches more about project memory with multiple agents than any well-numbered ADR.
My agent's telemetry ships disabled, points at localhost by default and excludes prompts. It still shows duration, fallbacks and routing decisions.
A practical checklist to validate security, cost, evals, and observability before you ship your AI app to production.
Production-tested templates trusted by developers. Save weeks of setup on your next project.