<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Semantic-Cache on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/semantic-cache/</link><description>Recent content in Semantic-Cache on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 02 Jun 2026 03:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/semantic-cache/index.xml" rel="self" type="application/rss+xml"/><item><title>The LLM inference router: the L7 switchboard we called a LoadBalancer in the canary post</title><link>https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/</link><pubDate>Tue, 02 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/</guid><description>&lt;blockquote>
&lt;p>This post is the natural continuation of &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a>. There the promotion mechanics dumped all the traffic-splitting complexity into a box we called &amp;ldquo;LoadBalancer&amp;rdquo;. The description was operational, good enough to understand the choreography, but structurally vague: what actually does that splitting is an L7 inference router with LLM awareness, a piece of the stack in its own right (&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">layer 1 of the seven layers&lt;/a>) that deserves its own post.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>In the previous post on &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary&lt;/a> we called &lt;strong>LoadBalancer&lt;/strong> the piece that splits traffic between the stable v1 and candidate v2 pools. The description was enough to understand the flow, but technically it was blurry: neither an L4 LoadBalancer (kube-proxy, MetalLB, IPVS) nor a generic L7 HTTP LoadBalancer (NGINX or HAProxy with no extension) knows what a model is, what a version is, how many tokens a request costs, what prefix the prompt has or which replica has which KV cache hot. The right piece is an &lt;strong>LLM inference router&lt;/strong>: an L7 proxy with explicit knowledge of the domain. It combines four functions: &lt;strong>model catalogue&lt;/strong> (resolving &lt;code>model=llama-70b@v2&lt;/code> → &lt;code>service.namespace:port&lt;/code>), &lt;strong>traffic splitting&lt;/strong> (applying the canary weight with a deterministic hash or deliberate stickiness for A/B), &lt;strong>cross-cutting policy&lt;/strong> (OIDC auth, per-tenant rate limit and quota, pre-prompt PII redaction, lightweight inline guardrails, &lt;code>gen_ai.*&lt;/code> tracing propagation) and &lt;strong>failover/degradation&lt;/strong> (if v2 falls over, redirect to v1; if the whole cluster is saturated, return 503 with &lt;code>Retry-After&lt;/code> instead of queueing forever). The &lt;strong>non-obvious&lt;/strong> piece that justifies its technical existence beyond the operational one is &lt;strong>prefix-aware routing&lt;/strong>: the router decides which fleet replica each request goes to based on the prompt prefix, so that a RAG system with the same system prompt plus the same block of retrieved documents systematically hits the prefix cache (RadixAttention in SGLang, PrefixCaching in vLLM, KV reuse in TensorRT-LLM) of the &lt;strong>same&lt;/strong> replica, multiplying the hit rate from &lt;strong>5–15 %&lt;/strong> (blind round-robin) to &lt;strong>60–85 %&lt;/strong> (prefix affinity). The concrete pieces in May 2026 are LiteLLM Proxy (the simplest option, OpenAI-compatible, declarative YAML catalogue), vLLM Production Stack router (specific to vLLM fleets, aware of the KV cache and the prefix), Envoy AI Gateway (LLM-aware Envoy filters, integrable with Istio), Kong AI Gateway (enterprise alternative with a plugin ecosystem), KGateway (CNCF, still incubating) and the NVIDIA Dynamo router (production-grade, aware of &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated prefill/decode serving&lt;/a>). In the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven-layer stack&lt;/a> it lives in &lt;strong>layer 1&lt;/strong> (gateway); in the &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">five maturity levels&lt;/a> it appears from &lt;strong>level 3&lt;/strong> onwards; in the &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">seven-phase deployment cycle&lt;/a> it is the last piece that &lt;strong>F6&lt;/strong> closes. This post includes a minimal manifest applicable to a generic 4×H100 SXM cluster.&lt;/p>
&lt;h2 id="you-are-here-deploy-layer-1-of-the-stack">You are here: DEPLOY (layer 1 of the stack)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy, layer 1 of the stack">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#rim)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#rim)}&lt;/style>
&lt;defs>&lt;marker id="rim" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · layer 1 of the stack (gateway / inference router)&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-background-what-the-canary-post-called-a-loadbalancer">The background: what the canary post called a &amp;ldquo;LoadBalancer&amp;rdquo;&lt;/h2>
&lt;p>In &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> we described the flow like this: &lt;em>&amp;ldquo;the LoadBalancer progressively splits traffic following a schedule: 1 % → 5 % → 25 % → 100 %&amp;rdquo;&lt;/em>. It was a correct &lt;strong>operational&lt;/strong> description, and the reader understood the choreography without needing more. But &lt;strong>technically&lt;/strong> it left unnamed a piece that deserves explicit treatment, because neither of the two usual senses of &amp;ldquo;LoadBalancer&amp;rdquo; does what that paragraph assumed:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>An L4 LoadBalancer&lt;/strong> (kube-proxy with iptables/IPVS, MetalLB, F5 BIG-IP in TCP mode) splits IP packets without looking inside the payload. It does not know which model is being requested, nor which version, nor how many tokens it carries, nor whether the client has quota. It cannot apply the canary weight &amp;ldquo;for model X version 2&amp;rdquo;: to it, every packet heading for the &lt;code>vllm-llama70b&lt;/code> VIP is indistinguishable.&lt;/li>
&lt;li>&lt;strong>A generic L7 HTTP LoadBalancer&lt;/strong> (NGINX or HAProxy in HTTP mode with no extension, a &lt;code>ClusterIP&lt;/code> Service with multiple backends) does split by URL and can route by header, but &lt;strong>it does not understand the OpenAI-compatible body&lt;/strong> of the request. It does not know that &lt;code>{&amp;quot;model&amp;quot;: &amp;quot;llama-70b&amp;quot;, &amp;quot;messages&amp;quot;: [...]}&lt;/code> carries the routing key in the &lt;code>model&lt;/code> field; it does not count tokens; it does not apply policies over LLM structures; it does not do prefix-aware routing because that requires parsing &lt;code>messages&lt;/code> and hashing the common prefix.&lt;/li>
&lt;/ul>
&lt;p>The piece the canary post assumed was doing this work is an &lt;strong>L7 inference router with LLM awareness&lt;/strong>. A layer in its own right, with its own configuration, its own CI/CD, its own metrics and its own pitfalls. This post names it and takes it apart.&lt;/p>
&lt;h2 id="the-analogy-the-switchboard-and-triage-of-a-hospital-with-multiple-specialities">The analogy: the switchboard and triage of a hospital with multiple specialities&lt;/h2>
&lt;p>A large hospital receives patients who arrive at A&amp;amp;E through different doors and who need different specialities: trauma, cardiology, paediatrics, oncology. There are three possible models of &amp;ldquo;front door&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>A single door with no triage.&lt;/strong> All patients wait in the same room and are sent in arrival order to the first free doctor, whatever their speciality. It works in a village surgery with a single general practitioner. When there are 200 patients a day and 12 specialities, it falls into dysfunction fast: the cardiologist treats sprains, the paediatrician treats heart attacks, specialised resources are wasted. This is the equivalent of the L4 LoadBalancer, which distributes bodies without understanding what they bring.&lt;/p>
&lt;p>&lt;strong>A door with a receptionist who asks about the symptom.&lt;/strong> Someone at the front desk asks &amp;ldquo;what&amp;rsquo;s wrong?&amp;rdquo; and directs the patient to the right corridor. The cardiologist sees only cardiology, the paediatrician only children. Better, but the receptionist is slow, does not gauge urgency and does not know the state of the rooms: they may send you to the cardiologist in corridor A when the one in B is free. This is the equivalent of a generic L7 HTTP proxy with &lt;code>path-based routing&lt;/code>, which distributes by category but with no information about internal state.&lt;/p>
&lt;p>&lt;strong>Professional triage with full awareness.&lt;/strong> A trained triage nurse who knows the catalogue of specialities, knows which bay is busy and which is free, remembers the recurring patient whose file is already open in the system (and sends them to the same doctor for continuity), applies cross-cutting policy (checks insurance cover, records allergens, redirects to paediatric A&amp;amp;E if the patient is a minor) and, if the cardiology room goes down because the electrocardiograph has broken, redirects to the hospital on the other side of the city. This is the piece a large hospital needs. In LLM terms it is called an &lt;strong>inference router&lt;/strong>.&lt;/p>
&lt;p>The analogy holds down to the last detail, including the &amp;ldquo;file already open&amp;rdquo; one: the patient who returns to the same doctor is exactly the client whose prompt shares a prefix with the one from five minutes ago. If the router sends it to the &lt;strong>same replica&lt;/strong>, that replica still has the KV cache hot and the request hits the prefix cache. If it sends it to a different replica because that one was &amp;ldquo;next in round-robin&amp;rdquo;, the KV cache has to be rebuilt from scratch and TTFT doubles. The triage nurse knows this. The blind LoadBalancer does not.&lt;/p>
&lt;h2 id="the-four-functions-of-the-inference-router">The four functions of the inference router&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="four functions of the LLM inference router">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.c{fill:#dfe9f5;stroke:#356}.t{fill:#eef0d0;stroke:#7a3}.p{fill:#f4e3cf;stroke:#a63}.f{fill:#ead8f5;stroke:#634}.title{font:600 13px sans-serif;fill:#222}.h{font:700 12px sans-serif;fill:#222}.l{font:11px sans-serif;fill:#222}.n{font:italic 10px sans-serif;fill:#444}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Four functions the inference router combines&lt;/text>
&lt;rect x="20" y="40" width="380" height="120" class="b c"/>
&lt;text x="30" y="62" class="h">1 · MODEL CATALOGUE&lt;/text>
&lt;text x="30" y="82" class="l">Resolve `model=llama-70b@v2` → service:port&lt;/text>
&lt;text x="30" y="102" class="l">Versioning, aliases, lifecycle (preview/stable/deprecated)&lt;/text>
&lt;text x="30" y="125" class="n">What stops the client from knowing the topology.&lt;/text>
&lt;text x="30" y="145" class="n">Without it, every client knows internal IPs/ports.&lt;/text>
&lt;rect x="420" y="40" width="380" height="120" class="b t"/>
&lt;text x="430" y="62" class="h">2 · TRAFFIC SPLITTING&lt;/text>
&lt;text x="430" y="82" class="l">Canary / blue-green / shadow weight&lt;/text>
&lt;text x="430" y="102" class="l">Deterministic per-request hash or deliberate stickiness&lt;/text>
&lt;text x="430" y="125" class="n">The splits from the canary post are applied here,&lt;/text>
&lt;text x="430" y="145" class="n">not in the inference engine.&lt;/text>
&lt;rect x="20" y="170" width="380" height="120" class="b p"/>
&lt;text x="30" y="192" class="h">3 · CROSS-CUTTING POLICY&lt;/text>
&lt;text x="30" y="212" class="l">OIDC auth · rate limit · per-tenant quota&lt;/text>
&lt;text x="30" y="232" class="l">Pre-prompt PII redaction · lightweight inline guardrails&lt;/text>
&lt;text x="30" y="252" class="l">Propagated gen_ai.* tracing · semantic cache&lt;/text>
&lt;text x="30" y="275" class="n">What is applied once across all models.&lt;/text>
&lt;rect x="420" y="170" width="380" height="120" class="b f"/>
&lt;text x="430" y="192" class="h">4 · FAILOVER · DEGRADATION&lt;/text>
&lt;text x="430" y="212" class="l">If v2 falls over → redirect to v1&lt;/text>
&lt;text x="430" y="232" class="l">If everything saturated → 503 with Retry-After&lt;/text>
&lt;text x="430" y="252" class="l">Circuit breaker · active health probes&lt;/text>
&lt;text x="430" y="275" class="n">What stops requests queueing forever.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="function-1--model-catalogue">Function 1 — Model catalogue&lt;/h3>
&lt;p>The router keeps a declarative catalogue mapping model identity to a concrete deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llama-70b&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># stable alias&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v2&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># canary version&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 5% of the traffic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;vllm-llama70b-v2.inference.svc.cluster.local:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">chat, tool_use]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lifecycle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">canary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llama-70b&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">95&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;vllm-llama70b-v1.inference.svc.cluster.local:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">chat, tool_use]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lifecycle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">stable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;embedding-multilingual&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;tei-bge-m3.inference.svc.cluster.local:8080&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">capabilities&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">embeddings]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lifecycle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">stable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The client sends &lt;code>{&amp;quot;model&amp;quot;: &amp;quot;llama-70b&amp;quot;, &amp;quot;messages&amp;quot;: [...]}&lt;/code> without knowing there are two replica pools behind it. The router resolves. If tomorrow you migrate from vLLM to SGLang for a specific version, the client never notices; you change the &lt;code>endpoint&lt;/code> in the catalogue and that is it.&lt;/p>
&lt;p>What this decoupling buys you is the freedom to move topology without breaking clients. What it costs is keeping the naming convention disciplined (&lt;code>llama-70b&lt;/code> is always the stable alias; &lt;code>llama-70b@v2&lt;/code> is the specific version for canary). Without that discipline, the aliases get dirty with &lt;code>llama-70b-prod-fixed-real-final-v3&lt;/code> and the catalogue stops being navigable within a few weeks.&lt;/p>
&lt;h3 id="function-2--traffic-splitting">Function 2 — Traffic splitting&lt;/h3>
&lt;p>The splits from the &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary post&lt;/a> (1 % → 5 % → 25 % → 100 %) materialise &lt;strong>here&lt;/strong>, not in the inference engine. The router computes a deterministic hash of the &lt;code>request_id&lt;/code> (or of the &lt;code>user_id&lt;/code>, if you want stickiness) and maps it to the weight range in the catalogue. For a weight of &lt;code>[v1: 95, v2: 5]&lt;/code>, 5 % of the hash space falls in v2 and 95 % in v1.&lt;/p>
&lt;p>Three design decisions that matter:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hash by random &lt;code>request_id&lt;/code>&lt;/strong> = independent sampling. Each request is an independent observation of the v1 vs v2 distribution. It is the right setting for statistically comparable canaries.&lt;/li>
&lt;li>&lt;strong>Hash by &lt;code>user_id&lt;/code>&lt;/strong> = per-user stickiness. The same client always sees the same pool. Useful for A/B testing with persisted conversational memory, but it &lt;strong>breaks the statistical comparability of the canary&lt;/strong> because user populations are not symmetric, a pitfall explained in the &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">previous post&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Hash by &lt;code>tenant_id&lt;/code>&lt;/strong> = hard partitioning. Tenant A goes to v1, tenant B to v2. This is the pattern for clients with different SLAs, or for validating v2 in an internal tenant before exposing it to external clients.&lt;/li>
&lt;/ul>
&lt;h3 id="function-3--cross-cutting-policy">Function 3 — Cross-cutting policy&lt;/h3>
&lt;p>Once, across all models, the router applies:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Auth&lt;/strong>: OIDC with JWT tokens validated against Keycloak / Authentik. &lt;code>Authorization: Bearer ...&lt;/code> headers translated into &lt;code>tenant_id&lt;/code> and &lt;code>roles&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Rate limit&lt;/strong>: token bucket per tenant (&lt;code>X req/min&lt;/code>) or per model (&lt;code>Y req/min&lt;/code> for llama-70b because it is expensive).&lt;/li>
&lt;li>&lt;strong>Quota&lt;/strong>: monthly quota of tokens consumed per tenant. The router counts &lt;code>gen_ai.usage.input_tokens&lt;/code> + &lt;code>gen_ai.usage.output_tokens&lt;/code> and rejects with &lt;code>429 Quota exceeded&lt;/code> when it runs out.&lt;/li>
&lt;li>&lt;strong>Pre-prompt PII redaction&lt;/strong>: Presidio or Llama Guard inline before the prompt touches the model. What the model does not see is not trained on, not logged, not leaked.&lt;/li>
&lt;li>&lt;strong>Lightweight inline guardrails&lt;/strong>: PromptGuard 2, Llama Guard 4, Granite Guardian, the ones covered in &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a>, run in the router because their latency (30–150 ms) fits within the TTFT budget.&lt;/li>
&lt;li>&lt;strong>&lt;code>gen_ai.*&lt;/code> tracing propagation&lt;/strong>: the router starts the parent span, propagates &lt;code>traceparent&lt;/code> to the engine and emits the &lt;code>gen_ai.system&lt;/code>, &lt;code>gen_ai.request.model&lt;/code> and &lt;code>gen_ai.request.version&lt;/code> attributes that &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">OTel GenAI tracing&lt;/a> consumes.&lt;/li>
&lt;li>&lt;strong>Semantic cache&lt;/strong>: for prompts that repeat exactly or with high semantic similarity (embedding cosine &amp;gt; 0.97 against the previous cache), it returns the cached answer without touching the engine. Typical saving in RAG with frequently asked questions: 20–40 % of requests.&lt;/li>
&lt;/ul>
&lt;h3 id="function-4--failover-and-degradation">Function 4 — Failover and degradation&lt;/h3>
&lt;p>The router knows the health state of each endpoint (active health probes every 5–15 s, recent TTFT latency) and decides:&lt;/p>
&lt;ul>
&lt;li>If v2 returns persistent 5xx or does not respond, the &lt;strong>circuit breaker&lt;/strong> opens: the router redirects the traffic that was going to v2 towards v1 until the probes go green again. This is automatic canary rollback in its simplest form.&lt;/li>
&lt;li>If the whole cluster is saturated (all replicas reporting &lt;code>num_requests_waiting &amp;gt; N&lt;/code> for T seconds), the router returns &lt;strong>&lt;code>503 Service Unavailable&lt;/code>&lt;/strong> with &lt;code>Retry-After: 30&lt;/code> instead of queueing forever. It is better to tell the client &amp;ldquo;come back in 30 seconds&amp;rdquo; than to keep them waiting four minutes and then time out.&lt;/li>
&lt;li>If there is multi-region or multi-cluster, &lt;strong>cross-cluster failover&lt;/strong> via DNS or L7: the primary region goes down and the secondary router takes over.&lt;/li>
&lt;/ul>
&lt;h2 id="the-non-obvious-piece-prefix-aware-routing">The non-obvious piece: prefix-aware routing&lt;/h2>
&lt;p>This is the function a conventional LoadBalancer cannot do and the one that justifies an LLM-specific router beyond the four generic ones.&lt;/p>
&lt;p>The KV cache in vLLM, SGLang and TensorRT-LLM can &lt;strong>reuse common prefixes across requests&lt;/strong>, see &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>. Specifically:&lt;/p>
&lt;ul>
&lt;li>vLLM with &lt;code>--enable-prefix-caching&lt;/code>: it detects that the current request shares a prefix (of a length that is a multiple of the block size, 16 tokens by default) with a previous request whose pages are still in HBM, and reuses those pages instead of reprocessing them.&lt;/li>
&lt;li>SGLang with RadixAttention: it structures the cache as a radix tree indexed by tokens; each request hits the common path of the tree and only computes the tail.&lt;/li>
&lt;li>TensorRT-LLM: a similar feature, called &lt;em>KV cache reuse&lt;/em>.&lt;/li>
&lt;/ul>
&lt;p>The prefix cache hit rate is the key metric: every token that hits is a token that is &lt;strong>not processed in prefill&lt;/strong>, reducing TTFT in direct proportion. For a typical RAG system, with a 400-token system prompt plus 2,000 tokens of retrieved documents plus a 50-token user question, the common prefix (&lt;code>system_prompt + docs&lt;/code>) is 2,400 of the 2,450 total tokens. If the cache hits, &lt;strong>prefill only processes 50 tokens instead of 2,450&lt;/strong>: TTFT falls to roughly a &lt;strong>twentieth&lt;/strong>.&lt;/p>
&lt;p>But the cache lives &lt;strong>per replica&lt;/strong>, not globally. If two requests with the same 2,400-token prefix land on different replicas, both do the full prefill: the first one&amp;rsquo;s cache is no use to the second. The second pays the full cost.&lt;/p>
&lt;p>With &lt;strong>blind round-robin&lt;/strong> (any conventional LB), requests are spread uniformly across N replicas. For a cluster of 4 replicas and 1,000 requests with the same &lt;code>system_prompt + docs&lt;/code>, &lt;strong>each replica receives ~250 requests&lt;/strong>, but all 4 do their own &amp;ldquo;first prefill&amp;rdquo; and the following 249 benefit within their replica. The global hit rate is decent but not optimal. For traffic with many different system prompts and little intra-prefix repetition, the hit rate hovers around &lt;strong>5–15 %&lt;/strong>.&lt;/p>
&lt;p>With &lt;strong>prefix-aware routing&lt;/strong>, the router computes a hash of the prompt prefix (the first N tokens, or the &lt;code>system_prompt&lt;/code> declared in &lt;code>messages[0]&lt;/code>) and keeps an &lt;strong>affinity table&lt;/strong> of &lt;code>hash → replica&lt;/code>. All requests with the same prefix land on the &lt;strong>same&lt;/strong> replica. The first pays the full prefill; the following 999 hit the cache. Global hit rate: &lt;strong>60–85 %&lt;/strong>.&lt;/p>
&lt;p>The cost of implementing it: the router has to parse the request body (not just the HTTP header), apply a lightweight tokeniser or a byte-based hash, and maintain an LRU/consistent-hash affinity table that rebalances when a replica comes in or out. That is server work, not generic proxy work. &lt;strong>vLLM Production Stack router&lt;/strong> implements it natively. &lt;strong>NVIDIA Dynamo&lt;/strong> does too. LiteLLM has a beta in its enterprise version. Envoy AI Gateway is adding it as an experimental filter.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Correction (September 2026).&lt;/strong> The previous sentence about LiteLLM is false: prefix-aware routing does not exist in any version, open or paid, verified against the code of 1.102.0. The detail, what LiteLLM does have, and the real state of the field are in &lt;a href="https://blog.lo0.es/en/posts/prefix-routing-what-litellm-does-not-do/">Prefix routing: what LiteLLM does not do&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;p>The operational difference for a production RAG system: with prefix-aware routing, the same cluster serves &lt;strong>2–4× more requests&lt;/strong> without adding GPUs, simply because prefill disappears in most cases.&lt;/p>
&lt;h2 id="token-aware-load-balancing">Token-aware load balancing&lt;/h2>
&lt;p>The second non-obvious piece. Classic round-robin splits by number of requests; but a 50-token prompt and an 8,000-token one cost radically different amounts (a factor of ~160× in prefill). Splitting equally by count severely unbalances the real load.&lt;/p>
&lt;p>&lt;strong>Token-aware load balancing&lt;/strong> adds up the expected prefill tokens (prompt length) and expected decode tokens (the client&amp;rsquo;s max_tokens) per active replica, and sends the new request to the replica with the lowest accumulated load. It is what both vLLM Production Stack and NVIDIA Dynamo implement as the default strategy when it is enabled.&lt;/p>
&lt;p>The metric feeding the calculation is, once again, &lt;code>vllm:num_requests_running&lt;/code> and &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, see &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a>, ideally complemented with an estimator of the incoming prompt&amp;rsquo;s tokens. Mature routers use &lt;code>tiktoken&lt;/code> or the model&amp;rsquo;s real tokeniser to count prompt tokens before choosing a replica.&lt;/p>
&lt;h2 id="comparison-of-concrete-pieces-may-2026">Comparison of concrete pieces (May 2026)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>LLM awareness&lt;/th>
&lt;th>Prefix-aware&lt;/th>
&lt;th>Token-aware LB&lt;/th>
&lt;th>Multi-model&lt;/th>
&lt;th>Semantic cache&lt;/th>
&lt;th>Plug &amp;amp; play&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LiteLLM Proxy&lt;/td>
&lt;td>High&lt;/td>
&lt;td>Beta (enterprise)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Excellent&lt;/td>
&lt;td>Yes (Redis)&lt;/td>
&lt;td>Very high&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM Production Stack router&lt;/td>
&lt;td>vLLM-specific&lt;/td>
&lt;td>Yes, native&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>vLLM only&lt;/td>
&lt;td>No (external)&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVIDIA Dynamo router&lt;/td>
&lt;td>High + disagg-aware&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>vLLM/SGLang/TRT-LLM&lt;/td>
&lt;td>No (external)&lt;/td>
&lt;td>Low&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Envoy AI Gateway&lt;/td>
&lt;td>Medium (filters)&lt;/td>
&lt;td>Experimental&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Via filter&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kong AI Gateway&lt;/td>
&lt;td>Medium (plugins)&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (plugin)&lt;/td>
&lt;td>Medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KGateway&lt;/td>
&lt;td>Medium&lt;/td>
&lt;td>Roadmap&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Roadmap&lt;/td>
&lt;td>Low (CNCF incubating)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NGINX + custom Lua&lt;/td>
&lt;td>Manual&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Manual&lt;/td>
&lt;td>Manual&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Low (build it yourself)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>LiteLLM Proxy&lt;/strong> is the default option to start with. OpenAI-compatible, simple YAML, supports the commercial providers plus any self-hosted OpenAI-compatible backend. The OSS version covers the four basic functions and semantic cache; prefix-aware routing and the enterprise version add advanced multi-tenancy.&lt;/p>
&lt;p>&lt;strong>vLLM Production Stack router&lt;/strong> is the right option if the fleet is 100 % vLLM. Aware of the KV cache, the prefix and the LoRA loaded per replica. It integrates better with native vLLM metrics.&lt;/p>
&lt;p>&lt;strong>NVIDIA Dynamo router&lt;/strong> is the most complete production-grade option, especially if you operate &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated serving&lt;/a> (separate prefill workers and decode workers). It requires an NVIDIA-aligned stack.&lt;/p>
&lt;p>&lt;strong>Envoy AI Gateway&lt;/strong> and &lt;strong>Kong AI Gateway&lt;/strong> are the options if the organisation already has Envoy or Kong as its corporate gateway and wants to extend it with LLM awareness without introducing another new piece.&lt;/p>
&lt;h2 id="minimal-manifest-litellm-proxy-on-a-generic-cluster">Minimal manifest: LiteLLM Proxy on a generic cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-config, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model_list:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - model_name: llama-70b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> litellm_params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model: openai/llama-70b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> api_base: http://vllm-llama70b-v1.inference.svc:8000/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> weight: 95
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model_info:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> version: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lifecycle: stable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - model_name: llama-70b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> litellm_params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model: openai/llama-70b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> api_base: http://vllm-llama70b-v2.inference.svc:8000/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> weight: 5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model_info:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> version: v2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lifecycle: canary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - model_name: embedding-multilingual
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> litellm_params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> model: openai/bge-m3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> api_base: http://tei-bge-m3.inference.svc:8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> router_settings:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> routing_strategy: least-busy # basic token-aware
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> num_retries: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout: 60
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> general_settings:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> master_key: &amp;#34;os.environ/LITELLM_MASTER_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> database_url: &amp;#34;os.environ/DATABASE_URL&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> litellm_settings:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cache: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cache_params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> type: redis
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> host: redis.inference.svc
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> port: 6379
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> similarity_threshold: 0.97
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> success_callback: [&amp;#34;langfuse&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> failure_callback: [&amp;#34;langfuse&amp;#34;]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-router, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/berriai/litellm:v1.55.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--config=/config/config.yaml&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--port=4000&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--num_workers=4&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">containerPort: 4000, name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http }, { containerPort: 4000, name: metrics }]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: LITELLM_MASTER_KEY, valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-secret, key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">master_key } } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: DATABASE_URL, valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-secret, key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">db_url } } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: LANGFUSE_PUBLIC_KEY, valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: langfuse-keys, key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">public } } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- {&lt;span class="w"> &lt;/span>&lt;span class="nt">name: LANGFUSE_SECRET_KEY, valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: langfuse-keys, key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret } } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: config, mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/config }]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">path: /health, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: config, configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm-config } }]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-router, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: http, port: 80, targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">monitoring.coreos.com/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PodMonitor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: litellm-metrics, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">litellm } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podMetricsEndpoints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The end client points at &lt;code>litellm-router.inference.svc:80/v1/chat/completions&lt;/code>, sets &lt;code>model=llama-70b&lt;/code>, and the router decides on each request whether it goes to v1 (95 %) or v2 (5 %), applies the rate limit, looks in the semantic cache, propagates tracing to Langfuse, and translates from OpenAI-compatible to the OpenAI-compatible API of the destination vLLM. Three router replicas for HA and so the gateway itself can scale horizontally with KEDA if needed, see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a>.&lt;/p>
&lt;h2 id="four-operational-pitfalls">Four operational pitfalls&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — the router becomes a SPoF if it is not replicated.&lt;/strong> Three or more replicas of the router itself, behind a &lt;code>LoadBalancer&lt;/code> Service (this one genuinely L4) with healthchecks. A single router replica means every configuration deploy closes the whole service for a few seconds.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — the router&amp;rsquo;s latency adds to the model&amp;rsquo;s.&lt;/strong> Each function adds milliseconds: body parsing (5–10 ms), JWT auth (2–5 ms), rate limit (1–2 ms), PII redaction with Presidio (20–80 ms), inline guardrails with Llama Guard (50–150 ms), prefix hash (5–10 ms), token counting with a tokeniser (10–30 ms). In total, &lt;strong>100–300 ms&lt;/strong> of overhead before touching the engine. If the model&amp;rsquo;s TTFT is 400 ms and the router&amp;rsquo;s is 200 ms, the client sees &lt;strong>600 ms&lt;/strong>, so it is worth measuring what each function costs and disabling the non-critical ones in the low-latency path.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — the catalogue drifts from the real state of the cluster.&lt;/strong> The router believes &lt;code>vllm-llama70b-v2&lt;/code> exists because it is in its YAML, but the deployment was withdrawn three days ago and nobody updated the config. The router returns 502 on 5 % of the traffic. Fix: validate the catalogue against &lt;code>kubectl get svc&lt;/code> in CI; no &lt;code>endpoint&lt;/code> in the catalogue may point at a non-existent Service. Or better: the router discovers the available endpoints dynamically via a label selector (&lt;code>app=vllm,model=llama-70b&lt;/code>) and applies the catalogue weights over the ones that are alive.&lt;/p>
&lt;p>&lt;strong>Pitfall 4 — semantic cache with an outdated embedding.&lt;/strong> The semantic cache compares the new prompt&amp;rsquo;s embedding against the embeddings of cached prompts. If you update the embedding model (see &lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">RAG corpus curation&lt;/a>), the distances are computed in a different space and the cache stops working correctly (false hits or false misses). Policy: the cache is &lt;strong>invalidated&lt;/strong> when the embedding model changes; generations are never mixed.&lt;/p>
&lt;h2 id="fit-within-the-stack-and-maturity">Fit within the stack and maturity&lt;/h2>
&lt;p>In the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven-layer stack&lt;/a>, the router is &lt;strong>layer 1&lt;/strong>: the front door that precedes the inference engine (layer 2), the KV cache + PagedAttention (layer 3) and the rest. It is the only piece that sees &lt;strong>all the traffic&lt;/strong> from the outside; any policy not applied here gets duplicated N times in the engines.&lt;/p>
&lt;p>In the &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">five maturity levels&lt;/a>, the router appears from &lt;strong>level 3&lt;/strong> (MANAGED) onwards: without OIDC + RBAC + cert-manager + default-deny NetworkPolicy, the router has nobody to authenticate and nobody to apply quotas to; before level 3, what you should build is a minimal proxy with no pretension of a catalogue. Platforms that try to have a polished router at level 1 end up with a large YAML nobody maintains.&lt;/p>
&lt;p>In the &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">seven deployment phases&lt;/a>, the router is what closes &lt;strong>F6&lt;/strong>: the last atomic step that puts the cluster into production. Without a router, F6 does not finish, because the catalogue, the quotas, the canaries and the failovers are a necessary condition for opening production traffic.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For a generic cluster of &lt;strong>4 nodes × 4×H100 SXM 80 GB&lt;/strong>, the inference router consumes modest resources: 3 replicas of the router pod (2 CPU cores, 4 GiB of memory each) are enough to sustain thousands of RPS because its work is light (parsing, hashing, routing, no inference). The router lives on non-GPU nodes of the cluster (control plane nodes or general workload nodes), and never consumes &lt;code>nvidia.com/gpu&lt;/code>.&lt;/p>
&lt;p>Traffic volume that a LiteLLM with 3 replicas and 4 workers each will sustain: &lt;strong>2,000–5,000 RPS&lt;/strong> routing to a vLLM backend, with &lt;strong>80–150 ms&lt;/strong> of overhead on the full path (auth + rate limit + cache check + propagation). If more is needed, scaling the router with KEDA on &lt;code>litellm:requests_per_second&lt;/code> is trivial.&lt;/p>
&lt;p>For larger clusters (16+ GPU nodes), consider the vLLM Production Stack router or the NVIDIA Dynamo router, which are more complex but squeeze out the prefix-aware routing and token-aware LB that LiteLLM OSS does not cover. For multi-region clusters, Envoy AI Gateway with Istio Service Mesh is the standard choice.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Deep comparison of LiteLLM vs vLLM PStack vs Dynamo&lt;/strong>, with prefix-aware benchmarks on a real on-premise cluster.&lt;/li>
&lt;li>&lt;strong>Semantic cache with Redis Stack + RedisVL&lt;/strong>: hit rate, false positives, TTL policy.&lt;/li>
&lt;li>&lt;strong>Multi-region routing&lt;/strong>: how the router decides between the DC1 and DC2 clusters based on latency, health and load.&lt;/li>
&lt;li>&lt;strong>AI Gateway specific features&lt;/strong>: token-bucket cost-based rate limiting (penalising long prompts), guardrails policy engine in the router.&lt;/li>
&lt;li>&lt;strong>Migration path&lt;/strong>: how to introduce a router into a cluster that already has clients pointing straight at the vLLM service, with no downtime.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy of the GPU cluster: who pays for what&lt;/a> — the gateway&amp;rsquo;s virtual keys as the basis for per-team chargeback.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — the previous post where we called this piece a &amp;ldquo;LoadBalancer&amp;rdquo;; this one names it and takes it apart.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Seven layers of the on-premise LLM inference stack&lt;/a> — the router is layer 1 of the stack.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels&lt;/a> — the router appears from level 3 onwards.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">Seven deployment phases&lt;/a> — the router is what closes F6.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a> — the router can scale with KEDA on its own metrics; it coexists with engine autoscaling.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> — the token-aware LB consumes &lt;code>vllm:num_requests_running&lt;/code> and &lt;code>vllm:gpu_cache_usage_perc&lt;/code> to choose a replica.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a> — what prefix-aware routing caches and why it multiplies the hit rate.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated prefill/decode serving&lt;/a> — production-grade routers (Dynamo) are aware of disaggregation and route prefill and decode to different pools.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the router emits the &lt;code>gen_ai.*&lt;/code> parent spans and propagates &lt;code>traceparent&lt;/code> to the engines.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — lightweight inline guardrails typically run in the router.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/mixed-nvidia-intel-environments-llm-inference/">Mixed NVIDIA + Intel environments for LLM inference&lt;/a> — capability-based routing makes full sense when there are heterogeneous backends (NVIDIA for the large LLM, Intel for embeddings/reranker, NUC for edge); the catalogue is extended with &lt;code>backend&lt;/code> and &lt;code>region&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/choosing-oss-gateway-llm-inference/">Choosing the switchboard: which OSS gateway to put in front&lt;/a> — the decision companion: this post explains &lt;em>what&lt;/em> a router is; that one picks &lt;em>which&lt;/em> with verified licences (LiteLLM, Envoy AI Gateway + Inference Extension, Higress, APISIX, Kong) and a recommendation for a K8s-native RKE2 + vLLM stack.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/litellm-langfuse-operational-pair/">LiteLLM and Langfuse: the operational pair&lt;/a> — day 2 of the router: cost per token on self-hosted models, trace correlation down to vLLM, content redaction and the measured overhead of the proxy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/litellm-day-2-high-availability/">LiteLLM on day 2: high availability&lt;/a> — operating the router once it is up, with the latency each routing strategy adds.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>LiteLLM project — &lt;code>litellm.ai&lt;/code> (documentación de Proxy, routing strategies, semantic cache).&lt;/li>
&lt;li>vLLM Production Stack — &lt;code>github.com/vllm-project/production-stack&lt;/code> (router con prefix-aware nativo).&lt;/li>
&lt;li>NVIDIA Dynamo — &lt;code>developer.nvidia.com/blog/nvidia-dynamo-1-production-ready/&lt;/code> (router production-grade con disaggregated-aware).&lt;/li>
&lt;li>Envoy AI Gateway — &lt;code>gateway.envoyproxy.io/docs/tasks/ai-gateway/&lt;/code> (proyecto en gestación dentro de Envoy).&lt;/li>
&lt;li>Kong AI Gateway — &lt;code>konghq.com/products/kong-ai-gateway&lt;/code> (proxy enterprise con plugin LLM).&lt;/li>
&lt;li>KGateway — &lt;code>kgateway.dev&lt;/code> (alternativa CNCF en gestación).&lt;/li>
&lt;li>Zheng et al. — &lt;em>SGLang: Efficient Execution of Structured Language Model Programs&lt;/em> (NeurIPS 2024) — RadixAttention y prefix caching.&lt;/li>
&lt;li>vLLM project — &lt;em>Automatic Prefix Caching&lt;/em> (&lt;code>docs.vllm.ai/en/latest/features/automatic_prefix_caching.html&lt;/code>).&lt;/li>
&lt;li>Patel et al. — &lt;em>SplitWise: Efficient Generative LLM Inference Using Phase Splitting&lt;/em> (ISCA 2024) — base teórica del routing prefill/decode aware.&lt;/li>
&lt;/ul></description></item></channel></rss>