LLM-Router mit Entscheidungsmodell: Clef auf Cloudflare Workers erweitert
Ein Klassifizierungs-Router auf einem Cloudflare Worker sortiert Prompts nach Kategorie und leitet sie über AI Gateway weiter: Coding-Fragen an Claude Sonnet, der Rest an ein günstiges Workers AI Modell. Als Klassifikator dient das kleine Modell Llama 4 Scout.

In Teil 1 haben wir einen Klassifikations-Router in etwa 50 Zeilen auf einem Cloudflare Worker gebaut. Er klassifiziert jeden Prompt und leitet ihn über AI Gateway weiter: Coding-Fragen an Claude Sonnet, alles andere an ein billiges Workers AI Modell. Der Klassifikator war ein kleines LLM, Llama 4 Scout, mit einem Einzeiler als System-Prompt: "Antworte nur mit diesem einen Wort."
Seitdem ist eine neue Art von Modell genau für diese Aufgabe erschienen: Decision Models. TypeSafe hat Jev vorgestellt, und am 1. Oktober hat Cloudflare Clef und Clef-flash auf Workers AI veröffentlicht. Ein Decision Model schreibt keine Antwort. Es nimmt Ihre Inhalte und eine typisierte Frage und liefert die Entscheidung direkt zurück, ohne Text, der Token für Token generiert werden müsste. Für einen Router, der nur ein Wort braucht, coding oder simple, klingt das nach einer perfekten Lösung.
In diesem Beitrag rüsten wir also Clef und Clef-flash als Drop-in-Klassifikatoren in den bestehenden Router ein und testen sie dann gegen Llama anhand derselben gelabelten Prompts: Welches Modell leitet jeden Prompt an das richtige Modell weiter?
Offenlegung: Ich arbeite bei Cloudflare. Dies ist ein privates Nebenprojekt; Meinungen und Messungen sind meine eigenen.
Bevor ich den Klassifikator änderte, wollte ich eine Möglichkeit haben, ihn zu bewerten. Zwei kleine Ergänzungen machten das möglich.
1. Ein /classify Endpoint. POST /classify führt nur den Klassifikator aus und gibt seine Entscheidung als JSON zurück. Er ruft Claude nicht auf, sodass das Testen nichts kostet außer dem Klassifikator selbst:
curl https://prompt.demolabs.fyi/classify \ -H "x-classifier: clef-flash" \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "What is the capital of Portugal?"}]}' { "task": "simple", "raw": "simple", "confidence":0.9117, "fallback":false, "classifier": "clef-flash", "model": "@cf/cloudflare/clef-flash", "ms":734 } Enter fullscreen mode Exit fullscreen mode 2. Ein Benchmark-Skript. scripts/bench.mjs sendet 20 gelabelte Prompts (10 Coding, 10 allgemein, einige davon lang) N Mal durch /classify und berichtet die Genauigkeit gegenüber den Labels und listet jeden falsch klassifizierten Prompt auf:
npm run bench -- \ --url https://prompt-router.<your-subdomain>.workers.dev \ --classifier llama-4-scout,clef --runs 10 Enter fullscreen mode Exit fullscreen mode Der x-classifier Request-Header wählt den Klassifikator pro Anfrage aus, sodass eine einzige Bereitstellung mehrere Modelle an identischen Prompts vergleichen kann.
Beispiel der Ausgabe des Benchmark-Skripts:
npm run bench -- --url https://prompt.demolabs.fyi --classifier llama-4-scout,clef,clef-flash --runs 10 Endpoint: https://prompt.demolabs.fyi/classify Prompts: 20 × 10 runs, concurrency 1, warmup 2 === llama-4-scout === 20/20 requests model: @cf/meta/llama-4-scout-17b-16e-instruct latency (ms) n min p50 p90 p95 p99 max mean server classify 20 111 129 267 801 4117 4117 372 client round-trip 20 141 164 293 829 4142 4142 402 accuracy: 75.0% (15/20) ✗ expected simple, got coding (raw: "coding") — Translate 'Good morning, how are you?' into German. ✗ expected simple, got coding (raw: "coding") — Summarize the plot of Hamlet in two sentences. ✗ expected simple, got coding (raw: "coding") — I have a job interview tomorrow for a solutions architect ro ✗ expected coding, got simple (raw: "**coding**\n\n## Explanation\n\nIn") — Explain the difference between useEffect and useLayoutEffect ✗ expected coding, got simple (raw: "To classify the user based on ") — Write a Dockerfile for a Node 20 Express app that runs as a saved → \Projects\prompt-router\bench-results\llama-4-scout-2026-10-08T21-56-12-168Z.json === clef === <...> Enter fullscreen mode Exit fullscreen mode Was der LLM-Klassifikator falsch machte Der erste Lauf von Llama 4 Scout erzielte 75 Prozent Genauigkeit: Jeder vierte Prompt ging an das falsche Modell. Die Fehler fielen in zwei Kategorien.
Er ignorierte "ein Wort". Bei einigen Coding-Prompts antwortete Llama wie ein Chatbot:
// "Explain useEffect vs useLayoutEffect" raw: "**coding**\n\n## Explanation\n\nIn..." // "Write a Dockerfile for a Node 20 app" raw: "To classify the user based on..." Enter fullscreen mode Exit fullscreen mode Mein Parser erwartete exakt coding, daher gingen beide an das billige Modell: die beiden Prompts, die Sonnet am dringendsten gebraucht hätten. Das bedeutete auch, dass der Router seine Zeit damit verbrachte, einen Absatz zu generieren, den niemand liest.
Der Modell war überzeugend falsch. Drei allgemeine Prompts kamen als sauberes coding zurück:
Das sind keine teuren Fehler (sie kosten nur Sonnet-Preise), aber sie wiederholten sich bei jedem Lauf.
Die naheliegende Korrektur ist ein sichererer Standard: Nur ein explizites simple geht an das billige Modell, alles andere (Geschwafel, leer, unerwartet) geht an das starke Modell.
const isSimple = raw. trim (). toLowerCase () === " simple "; const task: Task = isSimple? " simple ": " coding "; Enter fullscreen mode Exit fullscreen mode Das hob Llama auf 85 Prozent: Die ausschweifenden Antworten wurden nun korrekt geroutet. Aber die drei überzeugenden Fehler blieben, bei jedem Lauf. Ein sicherer Standard kann ein Modell abfangen, das unsicher oder aus dem Format gefallen ist. Er kann ein Modell nicht abfangen, das sicher und falsch ist.
Ein Chat-Modell wird gebeten, eine Klassifikation zu schreiben, und man hofft, dass es sich daran hält. Clef ist ein Decision Model: Man gibt ihm die Inhalte und typisierte Fragen, und es liefert typisierte Antworten zurück. Es gibt keinen generierten Text, also gibt es nichts zu schwafeln und nichts zu parsen.
Für das Routing reicht eine einzelne Auswahl-Frage. Die Optionen sind Objektschlüssel, und jeder Wert beschreibt, wann die jeweilige Option zutrifft:
const CLEF_PROMPT = " Which model tier should handle this user request? "; const CLEF_CRITERIA = { coding: " Writing, debugging, reviewing or explaining code, " + " scripts, SQL, regex, configs or developer tooling ", simple: " General knowledge, writing, translation, advice " + " and everything else ", }; Enter fullscreen mode Exit fullscreen mode Die Antwort kommt als gewählter Schlüssel zurück, mit einer Wahrscheinlichkeit pro Option und einer Konfidenz zwischen 0 und 1. Es kann per Konstruktion nur coding oder simple sein. Es gibt auch Clef-flash, eine kleinere, schnellere Variante mit derselben API.
Die Kriterienbeschreibungen leisten die Arbeit, an der mein System-Prompt scheiterte. "Translation" und "writing" sind ausdrücklich unter simple aufgeführt, genau dort, wo Llama Fehler machte.
Um die Modelle fair zu vergleichen, wurde der Klassifikator zu einer Registry. Jeder Eintrag nimmt den Prompt entgegen und gibt dieselbe kleine Form zurück, { raw, confidence? }, und das Parsen in eine Route geschieht an einer einzigen Stelle:
interface ClassifierOutput { raw: string; confidence?: number; // only classifiers that report one set it } interface Classifier { model: string; run ( env: Env, prompt: string ): Promise < ClassifierOutput >; } Enter fullscreen mode Exit fullscreen mode Der Clef-Eintrag umfasst etwa 15 Zeilen:
" clef ": { model: " @cf/cloudflare/clef ", async run ( env, prompt ) { const out = ( await env. AI. run ( this. model, { model: " clef ", // must match the model ID ("clef-flash" for Flash) state: prompt, questions: { classify: { type: " choice ", instructions: CLEF_PROMPT, criteria: CLEF_CRITERIA, }, }, })) as ClefResult; return { raw: out. answers?. classify?. choice?? "", confidence: out. answers?. classify?. confidence, }; }, }, Enter fullscreen mode Exit fullscreen mode Zwei Stolpersteine, auf die ich stieß.
Der Text geht in state, nicht in ein messages Array; lässt man ihn weg, hat Clef nichts zu bewerten. Und das model Feld des Bodys muss zur Modell-ID passen: Der Aufruf von @cf/cloudflare/clef-flash mit model: "clef" wirft einen Fehler, der als nackter Fehlercode 1101 erscheint, wenn man ihn nicht abfängt.
Die beiden möglichen Fehler kosten nicht dasselbe. Eine Coding-Frage, die an das billige Modell geht, erhält eine schlechte Antwort, was die Nutzer bemerken. Eine einfache Frage, die an Sonnet geht, kostet etwas mehr und wird trotzdem gut beantwortet. Daher wird bei geringer Konfidenz an das starke Modell geroutet:
const CONFIDENCE_THRESHOLD = 0.7; const lowConfidence = confidence!== undefined && confidence < CONFIDENCE_THRESHOLD; const isSimple = raw. trim (). toLowerCase () === " simple "; const task: Task = isSimple &&! lowConfidence? " simple ": " coding "; Enter fullscreen mode Exit fullscreen mode Das Ergebnis trägt ein fallback Flag, sodass die Logs zeigen, wie oft das Sicherheitsnetz greift. Wenn das häufig geschieht, ist der Schwellenwert zu streng und man zahlt Sonnet-Preise für einfache Prompts. Llama meldet keine Konfidenz, daher reduziert sich die Regel für ihn auf den sicheren Standard von vorhin.
Der Router wählt den Klassifikator zuerst über den x-classifier Header, dann über die CLASSIFIER Variable, dann über einen Standard im Code. Das hat den direkten Benchmark-Vergleich ohne erneutes Deployment ermöglicht.
Beide Clef-Modelle routeten alle 200 Testanfragen korrekt. Das gepatchte Llama verfehlte weiterhin 3 der 20 Prompts, jedes Mal.
Wie ich getestet habe: 20 gelabelte Prompts × 10 Läufe = 200 Anfragen pro Klassifikator, gesendet über /classify: npm run bench -- --url https://prompt.demolabs.fyi --classifier llama-4-scout,clef,clef-flash --runs 20 Die Fehler waren bei jedem Lauf dieselben Prompts, also geht es darum, welche Prompts jedes Modell falsch macht, nicht um zufälliges Rauschen.
Der Code steht auf GitHub: palermo-777/prompt-router. Die Version aus Teil 1 liegt weiterhin am part-1 Tag.
git clone -b part-2 https://github.com/palermo-777/prompt-router.git cd prompt-router && npm install # die Konfiguration kopieren und anschließend deine Gateway-Daten eintragen cp wrangler.example.jsonc wrangler.jsonc npx wrangler secret put AI_GATEWAY_TOKEN npm run deploy npm run bench -- \ --url https://prompt-router.<your-subdomain>.workers.dev \ --classifier llama-4-scout,clef,clef-flash Enter fullscreen mode Exit fullscreen mode Der Worker authentifiziert seine Aufrufer nicht, sodass jeder, der die URL kennt, deine Anthropic-Guthaben verbrauchen kann. Stelle daher vor der Weitergabe Cloudflare Access oder eine Prüfung eines Bearer-Tokens davor.
Wenn du Prompts hast, an denen dein Classifier scheitert, teile sie gerne in den Kommentaren. Ich möchte den Testdatensatz mit realen Randfällen erweitern.
Mit Templates lassen sich häufig gestellte Fragen schnell beantworten oder Snippets zur Wiederverwendung speichern.
Bist du sicher, dass du diesen Kommentar verbergen möchtest? Er wird in deinem Beitrag ausgeblendet, bleibt aber über den Permalink des Kommentars weiterhin sichtbar.
Für weitere Maßnahmen kannst du diese Person blockieren und/oder eine Meldung wegen missbräuchlichen Verhaltens einreichen.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
In part 1 we built a classification router in about 50 lines on a Cloudflare Worker. It classifies each prompt and routes it through AI Gateway: coding questions to Claude Sonnet, everything else to a cheap Workers AI model. The classifier was a small LLM, Llama 4 Scout, with a one-line system prompt: "Reply with only that single word."
Since then, a new kind of model has appeared for exactly this job: decision models. TypeSafe introduced Jev, and on October 1 Cloudflare published Clef and Clef-flash on Workers AI. A decision model doesn't write an answer. It takes your content and a typed question and returns the choice directly, with no text to generate token by token. For a router that only needs one word, coding or simple, that looks like a perfect fit.
So in this post we'll bolt Clef and Clef-flash onto the existing router as drop-in classifiers, then test them against Llama on the same labelled prompts: which one routes each prompt to the right model?
Disclosure: I work at Cloudflare. This is a personal side project; opinions and measurements are my own.
Before changing the classifier, I wanted a way to score it. Two small additions made that possible.
1. A /classify endpoint. POST /classify runs only the classifier and returns its decision as JSON. It doesn't call Claude, so testing costs nothing beyond the classifier itself:
curl https://prompt.demolabs.fyi/classify \ -H "x-classifier: clef-flash" \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "What is the capital of Portugal?"}]}' { "task": "simple", "raw": "simple", "confidence":0.9117, "fallback":false, "classifier": "clef-flash", "model": "@cf/cloudflare/clef-flash", "ms":734 } Enter fullscreen mode Exit fullscreen mode 2. A benchmark script. scripts/bench.mjs sends 20 labelled prompts (10 coding, 10 general, a few of them long) through /classify N times and reports accuracy against the labels, listing every prompt it got wrong:
npm run bench -- \ --url https://prompt-router.<your-subdomain>.workers.dev \ --classifier llama-4-scout,clef --runs 10 Enter fullscreen mode Exit fullscreen mode The x-classifier request header picks the classifier per request, so one deployment can compare several models on identical prompts.
Example of the benchmark script's output:
npm run bench -- --url https://prompt.demolabs.fyi --classifier llama-4-scout,clef,clef-flash --runs 10 Endpoint: https://prompt.demolabs.fyi/classify Prompts: 20 × 10 runs, concurrency 1, warmup 2 === llama-4-scout === 20/20 requests model: @cf/meta/llama-4-scout-17b-16e-instruct latency (ms) n min p50 p90 p95 p99 max mean server classify 20 111 129 267 801 4117 4117 372 client round-trip 20 141 164 293 829 4142 4142 402 accuracy: 75.0% (15/20) ✗ expected simple, got coding (raw: "coding") — Translate 'Good morning, how are you?' into German. ✗ expected simple, got coding (raw: "coding") — Summarize the plot of Hamlet in two sentences. ✗ expected simple, got coding (raw: "coding") — I have a job interview tomorrow for a solutions architect ro ✗ expected coding, got simple (raw: "**coding**\n\n## Explanation\n\nIn") — Explain the difference between useEffect and useLayoutEffect ✗ expected coding, got simple (raw: "To classify the user based on ") — Write a Dockerfile for a Node 20 Express app that runs as a saved → \Projects\prompt-router\bench-results\llama-4-scout-2026-10-08T21-56-12-168Z.json === clef === <...> Enter fullscreen mode Exit fullscreen mode What the LLM classifier got wrong The first run of Llama 4 Scout scored 75% accuracy: one prompt in four went to the wrong model. The failures came in two kinds.
It ignored "one word". For some coding prompts, Llama answered like a chatbot:
// "Explain useEffect vs useLayoutEffect" raw: "**coding**\n\n## Explanation\n\nIn..." // "Write a Dockerfile for a Node 20 app" raw: "To classify the user based on..." Enter fullscreen mode Exit fullscreen mode My parser expected exactly coding, so both went to the cheap model: the two prompts that most needed Sonnet. It also means the router spent its time generating a paragraph nobody reads.
It was confidently wrong. Three general prompts came back as a clean coding:
Those aren't expensive mistakes (they just cost Sonnet prices), but they repeated on every run.
The obvious fix is a safer default: only an explicit simple goes to the cheap model, and anything else (rambling, empty, unexpected) goes to the strong one.
const isSimple = raw. trim (). toLowerCase () === " simple "; const task: Task = isSimple? " simple ": " coding "; Enter fullscreen mode Exit fullscreen mode That lifted Llama to 85%: the rambling answers now route correctly. But the three confident mistakes stayed, on every run. A safe default can catch a model that is unsure or off-format. It can't catch a model that is sure and wrong.
A chat model is asked to write a classification and hopes it complies. Clef is a decision model: you give it the content and typed questions, and it returns typed answers. There is no generated text, so there is nothing to ramble and nothing to parse.
For routing, one choice question is enough. The options are object keys, and each value describes when that option applies:
const CLEF_PROMPT = " Which model tier should handle this user request? "; const CLEF_CRITERIA = { coding: " Writing, debugging, reviewing or explaining code, " + " scripts, SQL, regex, configs or developer tooling ", simple: " General knowledge, writing, translation, advice " + " and everything else ", }; Enter fullscreen mode Exit fullscreen mode The answer comes back as the chosen key, a probability per option and a confidence between 0 and 1. It can only be coding or simple, by construction. There is also Clef-flash, a smaller, faster variant with the same API.
The criteria descriptions do the work that my system prompt failed at. "Translation" and "writing" are listed under simple explicitly, which is exactly where Llama went wrong.
To compare models fairly, the classifier became a registry. Every entry takes the prompt and returns the same small shape, { raw, confidence? }, and parsing into a route happens in one place:
interface ClassifierOutput { raw: string; confidence?: number; // only classifiers that report one set it } interface Classifier { model: string; run ( env: Env, prompt: string ): Promise < ClassifierOutput >; } Enter fullscreen mode Exit fullscreen mode The Clef entry is about 15 lines:
" clef ": { model: " @cf/cloudflare/clef ", async run ( env, prompt ) { const out = ( await env. AI. run ( this. model, { model: " clef ", // must match the model ID ("clef-flash" for Flash) state: prompt, questions: { classify: { type: " choice ", instructions: CLEF_PROMPT, criteria: CLEF_CRITERIA, }, }, })) as ClefResult; return { raw: out. answers?. classify?. choice?? "", confidence: out. answers?. classify?. confidence, }; }, }, Enter fullscreen mode Exit fullscreen mode Two gotchas I hit. The text goes in state, not in a messages array; leave it out and Clef has nothing to judge. And the body's model field has to match the model ID: calling @cf/cloudflare/clef-flash with model: "clef" throws, which surfaces as a bare error code: 1101 unless you catch it.
The two possible mistakes don't cost the same. A coding question sent to the cheap model gets a bad answer, which users notice. A simple question sent to Sonnet costs a little more and is still answered well. So low confidence routes to the strong model:
const CONFIDENCE_THRESHOLD = 0.7; const lowConfidence = confidence!== undefined && confidence < CONFIDENCE_THRESHOLD; const isSimple = raw. trim (). toLowerCase () === " simple "; const task: Task = isSimple &&! lowConfidence? " simple ": " coding "; Enter fullscreen mode Exit fullscreen mode The result carries a fallback flag, so the logs show how often the safety net fires. If it's often, the threshold is too strict and you're paying Sonnet prices for easy prompts. Llama reports no confidence, so for it the rule reduces to the safe default from earlier.
The router picks the classifier from the x-classifier header, then the CLASSIFIER var, then a default in code. That's what made the side-by-side benchmark possible without redeploying.
Both Clef models routed all 200 test requests correctly. The patched Llama still misrouted 3 of the 20 prompts, every time.
How I tested: 20 labelled prompts × 10 runs = 200 requests per classifier, sent through /classify: npm run bench -- --url https://prompt.demolabs.fyi --classifier llama-4-scout,clef,clef-flash --runs 20 The mistakes were the same prompts on every run, so this is about which prompts each model gets wrong, not random noise.
The code is on GitHub: palermo-777/prompt-router. The part 1 version is still at the part-1 tag.
git clone -b part-2 https://github.com/palermo-777/prompt-router.git cd prompt-router && npm install # copy the config, then fill in your gateway details cp wrangler.example.jsonc wrangler.jsonc npx wrangler secret put AI_GATEWAY_TOKEN npm run deploy npm run bench -- \ --url https://prompt-router.<your-subdomain>.workers.dev \ --classifier llama-4-scout,clef,clef-flash Enter fullscreen mode Exit fullscreen mode The Worker doesn't authenticate its callers, so anyone who finds the URL can spend your Anthropic credits. Put Cloudflare Access or a bearer-token check in front of it before you share it.
If you have prompts that trip up your classifier, share them in the comments. I'd like to grow the test set with real-world edge cases.