Triple-Stack LLM Router
← Notes
⚡ Engineering

Triple-Stack LLM
Router

Three-tier routing that cuts LLM costs 93–95% without quality loss. Route mechanical work to free local, reasoning to cheap API, identity-critical calls to the heavy model.

3 Tiers Purpose Routing Usage & Setup Output Doctrine Hard Rules Heuristics

Three Tiers, One Router, ~95% Savings

Most AI cost waste comes from one mistake: sending everything to expensive models. This system classifies every call by complexity, dispatches it to the cheapest model that can handle it, and applies output discipline to compress what each tier sends and stores.

🎯
Route First

Three tiers: local/free → cheap API → premium. Classify by purpose or flags, dispatch to cheapest competent tier, escalate on failure only. Never fan out in parallel.

⚖️
Two Invariants

Quality floor: no compression ships if output degrades. Permanent memory: long-term memory survives every optimization pass. Both override every other law.

💰
~93–95% Savings

Router alone: 67–90% cost reduction (realistic mix: 30% free / 65% cheap / 5% premium). Output doctrine adds another 3–10%. Both parts required — neither works alone.

The Three Tiers

Each tier has a class, a model, token budgets, and a target distribution within a rolling 50-call quota window (±10% tolerance). Hard-floor purposes skip quota logic entirely — they always land on T3.

Tier Model & Endpoint Target · Budgets
T1 — Chat qwen3:32b · local Ollama (free) RAM: >24 GB → 32b · 12–24 GB → 14b · <12 GB → llama3.2:3b 30% target · ≤200 prompt tokens · ≤100 output tokens
T2 — Cheap deepseek-v4-pro → deepseek-v4-flash (cascade) Auto-scaled timeout (90s + 1s/1K chars, cap 300s) · 1 retry on 429/5xx 40% target · ≤800 prompt tokens · ≤500 output tokens
T3 — Precision ★ claude-opus-4-7 · Anthropic API Hard-floor purposes always land here · T3 fail → throw, no fallback 30% target · ≤1500 prompt tokens · task-dependent output
⚑ Cascade Order T1 fail → T2. T2-pro fail → T2-flash → T3. T3 fail → throw. Never fan out in parallel — route once, escalate on failure only. Parallel calls multiply cost and rarely improve quality.

Purpose Buckets & Flags

The classifier checks purpose first, then flags, then prompt heuristics. Pass an explicit purpose for faster, more accurate routing. Hard-floor purposes (red pills below) route directly to T3 regardless of quota or cost pressure.

T1

Chat — Free Local

Mechanical, deterministic. No reasoning needed.

Flags: chat · light · cheap · mechanical

Heuristic: prompt <40 chars + greeting pattern → T1 automatically.

classify label json_reformat template_slot_fill dedup hash_match greeting echo
★ classify is most-routed
T2

Cheap — Reasoning API

Inference needed. Not identity-critical.

Flags: deepseek · cheap_reasoning · long_context

summarize enrich reflexion_first_pass kg_titling compact_memory long_context_analysis codebase_analysis research_synthesis
★ known-purpose reasoning bucket
T3

Precision — Hard Floor

Identity-critical, architectural, high-stakes. Cannot demote.

Flags: precision · opus · important · default if no match

identity_audit self_modification phenomenology architectural_decision author_voice high_stakes_review
★ default for unknown + hard floor

Calling the Router

Import once. Call ask() with a prompt and an optional purpose. The router classifies, checks quota, dispatches to the cheapest competent tier, logs the call to memory/tier-usage.jsonl, and returns a result object.

Three call patterns. Include purpose whenever you know it — routing is faster and more accurate than heuristic-only classification.

const { ask } = require('./lib/tiered-ask.cjs');

// ── SIMPLE CALL — router picks tier automatically
const r = await ask({ prompt: 'Summarize this paragraph: ...' });
console.log(r.text, r.tier, r.model);

// ── WITH PURPOSE — faster, more accurate routing
const r2 = await ask({
  purpose: 'summarize',
  prompt: 'Apollo program: 1961-1972, 12 men on moon.',
  system: 'Return 2 sentences max.'
});

// ── HARD-FLOOR — always hits T3 regardless of quota
const r3 = await ask({
  purpose: 'architectural_decision',
  prompt: 'Design a load-balancer for 10M RPS. List tradeoffs.'
});

// ── FORCE A TIER via flags
const r4 = await ask({ flags: ['cheap'], prompt: 'Extract fields from this JSON...' });

Return shape

FieldTypeWhat it contains
textstringModel response, trimmed
tier1 | 2 | 3Tier that handled the call
modelstringExact model name used
latency_msnumberWall-clock time from call to response
fallbackbooleanTrue if a cascade fallback occurred
usageobjectToken counts: prompt_tokens + completion_tokens
verification_flagsarrayT2 hallucination guard findings (file paths, SM IDs)

CLI Commands

Check that all three tiers are reachable before your first real call.

node lib/tiered-ask.cjs ping
      

See tier distribution, average latency, fallback count, and per-model breakdown across the last N calls. Run this regularly to catch routing drift.

node scripts/tier-usage-report.cjs
node scripts/tier-usage-report.cjs 200  # last 200 calls

Five Phases to Production

Run the v2 setup prompt in Claude Code (or any agent with bash + file write). Takes ~5–10 minutes on broadband. The only manual step: paste your API keys when asked.

A
Environment Check node --version ≥ 18. Install Ollama if missing (winget / curl / brew). Probe http://localhost:11434/api/tags — run ollama serve if not responding. Pick T1 model by free RAM: >24 GB → qwen3:32b · 12–24 GB → qwen3:14b · <12 GB → llama3.2:3b. Pull the chosen model via ollama pull <model>.
B
Keys & .env For each missing key, ask once — never echo after capture. DeepSeek key: platform.deepseek.com → API Keys. Anthropic key: console.anthropic.com → API Keys. Write all 13 vars to .env and append .env to .gitignore.

Copy this template. Fill in your API keys and set TIER1_MODEL to the model chosen in Phase A.


      OLLAMA_BASE_URL=http://localhost:11434
TIER1_MODEL=qwen3:32b
DEEPSEEK_API_KEY=sk-...
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
TIER2_MODEL=deepseek-v4-pro
TIER2_FALLBACK_MODEL=deepseek-v4-flash
ANTHROPIC_API_KEY=sk-ant-...
TIER3_MODEL=claude-opus-4-7
QUOTA_WINDOW_SIZE=50
QUOTA_TARGET_CHAT=0.30
QUOTA_TARGET_CHEAP=0.40
QUOTA_TARGET_PRECISION=0.30
QUOTA_TOLERANCE=0.10
C
Write Seven Library Files lib/soft-failure.cjs — append-only error log · lib/ollama-client.cjs — T1 caller + health probe · lib/deepseek-client.cjs — T2 caller with auto-scaled timeout + retry · lib/anthropic-client.cjs — T3 caller · lib/deepseek-verify.cjs — hallucination guard (file paths, SM IDs) · lib/tiered-ask.cjs — main router (classify → quota → dispatch → log) · scripts/tier-usage-report.cjs — distribution + latency report. All CommonJS (.cjs). Each client loads .env via a 4-line loader that skips # lines and does not overwrite existing process.env.
D
Verify — Run Six Checks in Order If any step fails: report the failure and exact next debug step. Never fake success.

Run all six checks. Each should produce the expected output shown in the comment.


# 1 — All tiers reachable
node lib/tiered-ask.cjs ping
# → { tier1: { ok: true }, tier2: { ok: true }, tier3: { ok: true } }

# 2 — T1 routing (short greeting → tier 1)
node -e "require('./lib/tiered-ask.cjs').ask({ prompt: 'hi' }).then(r => console.log(r.tier))"
# → 1

# 3 — T2 routing (known purpose → tier 2)
node -e "require('./lib/tiered-ask.cjs').ask({ purpose: 'summarize', prompt: 'Apollo 1961-1972.' }).then(r => console.log(r.tier))"
# → 2

# 4 — T3 routing (complex prompt → tier 3)
node -e "require('./lib/tiered-ask.cjs').ask({ prompt: 'Design load-balancer for 10M RPS.' }).then(r => console.log(r.tier))"
# → 3

# 5 — Hard floor (always tier 3, no demotion)
node -e "require('./lib/tiered-ask.cjs').ask({ purpose: 'identity_audit', prompt: 'who are you?' }).then(r => console.log(r.tier))"
# → 3

# 6 — Distribution report
node scripts/tier-usage-report.cjs
E
Report Print T1/T2/T3 model names + latencies. Quota config (window size, targets, tolerance). Paths to lib/tiered-ask.cjs and scripts/tier-usage-report.cjs. All six verification checks must show expected output before marking setup complete.

Rules That Don't Bend

Six invariants that override operational convenience. Violating any one breaks the system's trust guarantees or data integrity. No exception without explicit user confirmation and a logged rationale.

The Six Invariants

These apply in all modes — setup, production, maintenance, compression passes — with no exceptions.

  • Never Echo Keys API keys are written to .env once. Never logged, re-displayed, or emitted in any output after initial capture.
  • Never Delete Logs memory/tier-usage.jsonl and memory/soft-failures.jsonl are append-only. Rotation requires a backup first. Silent deletion is data loss.
  • Never Modify Hard-Floor The HARD_FLOOR set in tiered-ask.cjs is not changed without explicit user confirmation. It protects identity-critical calls from cost pressure.
  • No Output Padding No preambles, postambles, plan narration, or acknowledgment tokens in any output at any tier. Strip on sight, every time.
  • Hard-Floor Always T3 Quota rebalancing and cascade logic cannot demote a hard-floor purpose. The classifier check runs before quota math — always.
  • No Silent Eviction Long-term memory entries are only removed via deliberate, logged action. Tightening a prompt that drops memory entries is amnesia, not compression.
📐 The Doctrine — Operating Laws

Five Laws of Output Discipline

Output is the most expensive surface — every emitted token is billed. These habits close the four leaks that burn ~30% of output tokens in unoptimized prompts: narrated reasoning, restated instructions, forward-carried context, and confirmatory tool calls.

1

No Preambles, No Postambles

Trigger: every output, every tier, every call.

The Habit Start with the answer. Emit the artifact. Stop when done.
The Rule Strip on sight: "Sure!", "Great question", "I'll first…", "Let me know if anything's unclear." Zero value, measurable cost.
2

Structure Beats Prose

Trigger: any list, comparison, sequence, status, or number.

The Habit Tables for comparisons, JSON for structured state, IDs for references, symbols for statuses (✓ / ✗ / ⚠). "~20k" not "approximately twenty thousand."
The Rule Prose is for nuance structure can't carry. If structure can carry it, it must. Narrative sequences with state → state machine, not sentences.
3

Route Before Reasoning

Trigger: before any LLM call or tool call.

The Habit Is the answer in cache? Computable directly? A fixed transformation (regex, parse, format)? If yes to any — don't call the model. If no — classify tier first.
The Rule Escalation, not parallelism. Route once; escalate on failure. If quota constantly demotes calls, the classifier is wrong — fix the classifier, not the quota.
4

Reference Over Inline

Trigger: any content >200 tokens, or content only some steps need.

The Habit Pass path/ID/hash/stub. Load the full payload only when a step has explicitly decided it needs it. Lazy-load tiers: identifier (~10 tokens) → stub (~50 tokens) → full payload.
The Rule Carrying context the next step won't read is pure waste, charged on every turn until eviction. "Might be useful" = "probably isn't" — the optionality costs real tokens.
5

Deltas, Not Snapshots

Trigger: file edits, state changes, memory updates, task completions.

The Habit Output what changed, not the full state. "Edited 3 lines: …" not the full file. "10/10 done; 1 failed" not a transcript. "Memory: 1 updated" not the new memory file.
The Rule Deltas every turn; snapshots at session boundaries or every N deltas. Idempotent deltas — applying twice gives the same result as once.

Three Memory Tiers

Memory is leverage when curated, cost when hoarded. Each tier has a distinct lifespan, eviction policy, and promotion rule. The worst mistake: letting working-memory content graduate automatically and turning long-term memory into a junk drawer.

Long-Term — Permanent Survives sessions, model swaps, prompt rewrites. Identity facts, earned learnings, decision rationale, self-modification log, unresolved tensions, index pointers. Sacred. Eviction is deliberate and logged — never a side effect. Read on start; write earned signal on close. Always.
Working — Session-Scoped Survives the session. Evicted at session end. Current task's load-bearing facts. Close ritual: summarize in 3–5 lines → promote durable signal to long-term → discard the rest. A session that closes without writing is a session that lost.
Turn-Local — Ephemeral Survives the turn. Auto-evicted. Tool results, intermediate computations, drafts superseded this turn. Never promoted automatically. Carrying it forward charges every subsequent turn until eviction.

"Long-term memory survives every compression. Compression that breaks this is not compression — it is amnesia."

—20x Token Reduction Doctrine

20 Laws in Five Clusters

The two invariants (quality floor, permanent memory) override all 20 laws. These clusters cover the laws that cause the most silent failures when violated. Each law is ≤15 words — scan before any session.

1

Compress Prompts

Every Session
  • Cut every line that fails: "would removing this degrade behavior?"
  • Symbolize repeating concepts once; reuse the symbol everywhere.
  • Rewrite negatives as positives — "always do Y" beats "don't do X."
  • Calibrated confidence is shorter than hedging and longer than overstatement.
Result

A 2,000-token prompt usually wants to be 400–700 tokens.

2

Output Discipline

Every Call
  • No preambles, no postambles, no plan narration, no acknowledgments.
  • Structure beats prose: tables, JSON, IDs over sentences.
  • Reasoning stays internal; only conclusions and artifacts surface.
  • Politeness tokens are fat. Strip between agents and tools.
Result

~30% of output tokens eliminated without information loss.

3

Route & Cache

Before Every Call
  • Route to the cheapest competent tier before any heavy call.
  • Cache it, compute it, or template it before asking a model.
  • If the answer is already in this turn's context, don't call the tool.
  • Variance is expensive: 90%/100% beats20x/95%-broken.
Result

Routing accuracy prevents both under-routing (quality risk) and over-routing (cost waste).

4

Context Hygiene

Every Turn
  • Reference paths, hashes, IDs by default; paste only sub-200-token hot data.
  • Emit deltas, not state dumps. Ingest deltas, not snapshots.
  • Don't carry context the next step won't read.
  • Tool calls: minimum args, batch when possible, distill before re-injecting.
Result

Context window stays clean; costs don't compound across turns.

5

Memory & Evolution

Every Session
  • Summarize-then-discard working state; promote durable signal to long-term.
  • Read long-term memory on start; write earned signal on close. Always.
  • Every self-modification has a metric, a review window, and a rollback condition.
  • Backup before rotation; format-migrate before reformatting long-term.
Result

Identity, learnings, and decisions persist across every prompt rewrite and model swap.

Strip Until It Breaks, Then Stop

The router solves the tier problem: which model handles this call? The doctrine solves the token problem: what does every tier send, store, and emit? Neither works without the other. The router without doctrine ships bloated prompts to cheap models and undoes half its own savings. The doctrine without the router applies compression pressure on premium calls that should have been routed away entirely.

The two invariants — quality floor and permanent memory — are not constraints on the system. They are the reason the system is trustworthy enough to run autonomously. Every compression that violates them is not compression. It is either a cheaper answer or a forgotten self.

Route cheap. Compress outputs. Keep memory clean. Strip until removing more degrades. Stop one step earlier.

⚑ The One Rule Before any LLM call: could this be cached, computed, or templated? If yes — don't call the model. If no — classify tier first, dispatch to cheapest competent, escalate on failure only. Output the answer and nothing else.