<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Router on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/router/</link><description>Recent content in Router on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 06 Jun 2026 08:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/router/index.xml" rel="self" type="application/rss+xml"/><item><title>Choosing the switchboard: which OSS gateway to put in front, and why the licence decides before the features</title><link>https://blog.lo0.es/en/posts/choosing-oss-gateway-llm-inference/</link><pubDate>Sat, 06 Jun 2026 08:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/choosing-oss-gateway-llm-inference/</guid><description>&lt;blockquote>
&lt;p>This post is the &lt;strong>decision companion&lt;/strong> to &lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">The LLM inference router&lt;/a>. That one explained &lt;em>what&lt;/em> an inference router is and &lt;em>why&lt;/em> it exists (catalogue, traffic splitting, cross-cutting policy, prefix-aware routing). This one answers the question that actually blocks a deployment: &lt;strong>which one to choose&lt;/strong>, with verified licences and an order of criteria that is not the one most people use.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The gateway is the piece &lt;strong>100 % of the traffic&lt;/strong> passes through and the most expensive to replace later: the client SDK, the auth and rate-limit policies, the tracing and the model catalogue all couple to it. That is why the &lt;strong>order of the criteria&lt;/strong> matters, and it is not the usual one. First the &lt;strong>licence&lt;/strong>: not &amp;ldquo;is it open source?&amp;rdquo; but &amp;ldquo;are the features I need under a permissive licence or behind an Enterprise wall?&amp;rdquo;. Second the &lt;strong>architectural fit&lt;/strong>: is it a citizen of your Kubernetes (Gateway API) or a separate process you have to operate on the side? Third, &lt;strong>maturity and documentation&lt;/strong>. Fourth, the features. With data verified as of June 2026: LiteLLM is MIT in the core, but SSO, audit logs, fine-grained RBAC and several guardrails are Enterprise; Kong has an Apache 2.0 core but the AI plugins that matter (semantic cache, prompt guard, AI proxy advanced) are gated; Envoy AI Gateway, Gateway API Inference Extension (GIE), Higress, APISIX and Bifrost are Apache 2.0 end to end. For an RKE2 + vLLM stack with a K8s-native priority (the case we assume here), the recommendation is to adopt the &lt;strong>Gateway API Inference Extension model&lt;/strong>, the Endpoint Picker that routes with awareness of prefix cache, KV and LoRA, which is exactly what multiplies the hit rate. Implement it with Envoy AI Gateway if the AI-native trajectory weighs more, or with Higress if today&amp;rsquo;s maturity weighs more; and put LiteLLM (MIT) behind it as an optional multi-provider control plane. With explicit scepticism about v0.x releases, about &amp;ldquo;OpenAI-compatible ≠ inference-aware&amp;rdquo; and about phone-home telemetry.&lt;/p>
&lt;h2 id="the-principle-that-reorders-the-criteria-the-gateway-is-a-marriage-not-a-date">The principle that reorders the criteria: the gateway is a marriage, not a date&lt;/h2>
&lt;p>Some pieces of the stack can be swapped on a Sunday afternoon: the embedding model, the reranker, even the inference engine behind an OpenAI-compatible interface. The gateway &lt;strong>cannot&lt;/strong>. It is the piece everything else couples to: every client SDK points at its URL, the security policies live in it, the tracing is born in it, it defines the model catalogue. Ripping it out two years later means touching &lt;strong>every&lt;/strong> consumer at once. It is the infrastructure decision with the highest cost of reversal in the whole serving layer.&lt;/p>
&lt;p>When a decision is expensive to reverse, the dominant criterion is not &amp;ldquo;what does it do today?&amp;rdquo; but &amp;ldquo;&lt;strong>can I own it and operate it for years without surprises?&lt;/strong>&amp;rdquo;. And that puts the &lt;strong>licence&lt;/strong> ahead of the features. A brilliant tool whose SSO, audit log or RBAC live behind an Enterprise contract is a tool that, the day your ENS deployment needs those controls, forces you to pay or to migrate, which is exactly the scenario the choice was supposed to avoid. That is why this post&amp;rsquo;s filter order is &lt;strong>licence → fit → maturity/docs → features&lt;/strong>, and not the other way round.&lt;/p>
&lt;div class="diagram" style="max-width:620px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 620 230" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Criteria funnel: licence, fit, maturity, features">
&lt;defs>&lt;marker id="ga" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="310" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">The decision funnel · in this order&lt;/text>
&lt;rect x="60" y="40" width="500" height="34" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="2"/>
&lt;text x="310" y="62" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#1f5c34">1 · Licence — truly permissive? features gated?&lt;/text>
&lt;rect x="110" y="86" width="400" height="34" rx="6" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.8"/>
&lt;text x="310" y="108" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#222">2 · Fit — a citizen of your Kubernetes?&lt;/text>
&lt;rect x="160" y="132" width="300" height="34" rx="6" fill="#f7efda" stroke="#c79a32" stroke-width="1.6"/>
&lt;text x="310" y="154" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#222">3 · Maturity + documentation&lt;/text>
&lt;rect x="210" y="178" width="200" height="34" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.4"/>
&lt;text x="310" y="200" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#333">4 · Features&lt;/text>
&lt;path d="M310,74 L310,86" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#ga)"/>
&lt;path d="M310,120 L310,132" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#ga)"/>
&lt;path d="M310,166 L310,178" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#ga)"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-field-of-candidates-june-2026">The field of candidates (June 2026)&lt;/h2>
&lt;p>The real OSS pieces anyone would put in front of an on-premise vLLM fleet:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Candidate&lt;/th>
&lt;th>What it is&lt;/th>
&lt;th>Language&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LiteLLM Proxy&lt;/td>
&lt;td>OpenAI-compatible gateway, 100+ providers, virtual keys, spend&lt;/td>
&lt;td>Python (FastAPI)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Envoy AI Gateway&lt;/td>
&lt;td>AI layer on top of Envoy Gateway; integrates GIE/EPP, InferencePool&lt;/td>
&lt;td>Go (Envoy)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway API Inference Extension (GIE)&lt;/td>
&lt;td>K8s-SIG extension: inference-aware Endpoint Picker&lt;/td>
&lt;td>Go&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Higress&lt;/td>
&lt;td>API gateway + AI plugins, Envoy/Istio, CNCF&lt;/td>
&lt;td>Go/C++&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Apache APISIX&lt;/td>
&lt;td>Mature API gateway with AI plugins&lt;/td>
&lt;td>Lua/Nginx&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kong (+ AI plugins)&lt;/td>
&lt;td>API gateway; Apache 2.0 core, Enterprise AI plugins&lt;/td>
&lt;td>Lua/Nginx&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bifrost&lt;/td>
&lt;td>High-performance AI-first gateway&lt;/td>
&lt;td>Go&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM Production Stack / Semantic Router&lt;/td>
&lt;td>vLLM-specific router, KV/prefix/intent-aware&lt;/td>
&lt;td>Go/Python&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="filter-1--licence-where-the-small-print-is">Filter 1 — Licence: where the small print is&lt;/h2>
&lt;p>This is where half the options die, and where &amp;ldquo;open source&amp;rdquo; misleads if you do not look at the detail. What has been verified:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Candidate&lt;/th>
&lt;th>Core licence&lt;/th>
&lt;th>What is gated (paid)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LiteLLM&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>SSO (Okta/Azure AD), audit logs, JWT auth, fine-grained RBAC, several guardrails (llmguard, llamaguard, prompt-injection) → Enterprise (~250 USD/month and rising)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Envoy AI Gateway&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>— (full AI layer is OSS)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GIE (Inference Extension)&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>— (K8s-SIG project)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Higress&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>— (AI plugins included)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>APISIX&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>— (more built-in AI than Kong OSS)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kong&lt;/td>
&lt;td>Apache 2.0 (core)&lt;/td>
&lt;td>AI Semantic Cache, AI Prompt Guard, AI Proxy Advanced, AI RAG Injector → Enterprise&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bifrost&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two conclusions that settle the field:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Kong falls out for an OSS AI deployment.&lt;/strong> Its core is Apache 2.0, but precisely the AI plugins that would justify choosing it (semantic cache, prompt guard, proxy advanced) are Enterprise, with contracts that the sector&amp;rsquo;s own comparisons put above five figures a year. For AI on an OSS budget, APISIX offers more built-in at no cost. Kong remains excellent as a classic API gateway; as an &lt;strong>OSS AI gateway&lt;/strong>, no.&lt;/li>
&lt;li>&lt;strong>LiteLLM really is MIT in the core&lt;/strong>, and that is genuine: you can fork it, modify it and use it commercially. But SSO, audit logs, fine-grained RBAC and several guardrails are Enterprise. Under the criterion we set (&lt;em>&amp;ldquo;a permissive core is enough, governance is solved with OIDC and external auditing in the stack&amp;rdquo;&lt;/em>) LiteLLM &lt;strong>stays in play&lt;/strong>; under an &amp;ldquo;all OSS or nothing&amp;rdquo; criterion it would be wounded. It pays to know exactly what falls on which side of the line before committing.&lt;/li>
&lt;/ul>
&lt;h3 id="litellm-what-is-gated-and-its-oss-equivalent">LiteLLM: what is gated and its OSS equivalent&lt;/h3>
&lt;p>The nuance that decides whether LiteLLM&amp;rsquo;s gating is a real problem or not: &lt;strong>&amp;ldquo;paid built-in feature&amp;rdquo; is not the same as &amp;ldquo;capability impossible in OSS&amp;rdquo;&lt;/strong>. In almost every case the capability is achieved by changing the &lt;em>how&lt;/em>, wiring OSS through LiteLLM&amp;rsquo;s open hook, or solving it in the layer next door (the gateway, the observability). The breakdown, verified against the docs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capability&lt;/th>
&lt;th>In LiteLLM Enterprise&lt;/th>
&lt;th>OSS equivalent&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Guardrails&lt;/td>
&lt;td>prebuilt integrations (&lt;code>llmguard&lt;/code>, &lt;code>llamaguard&lt;/code>, &lt;code>lakera&lt;/code>, &lt;code>aporia&lt;/code>, &lt;code>hide_secrets&lt;/code>)&lt;/td>
&lt;td>&lt;strong>the framework + custom guardrail hook + Presidio are OSS (MIT core)&lt;/strong>; you invoke &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>, Llama Guard or Presidio from the hook yourself&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Audit log&lt;/td>
&lt;td>turnkey UI with retention policies&lt;/td>
&lt;td>request/response logging + &lt;strong>custom callbacks + OTel/Langfuse exporters&lt;/strong> (official OSS integration) → you build the trail and you own it&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RBAC&lt;/td>
&lt;td>fine-grained (&lt;code>enforce_rbac&lt;/code>, org/team/user roles)&lt;/td>
&lt;td>coarse (virtual keys per team/budget/model) is OSS; fine-grained is done &lt;strong>at the edge in the gateway&lt;/strong> (&lt;code>ext_authz&lt;/code> + OPA)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SSO&lt;/td>
&lt;td>Admin UI SSO (Okta/Azure/Google/OIDC)&lt;/td>
&lt;td>user/API SSO is solved by fronting with OSS OIDC (Keycloak + oauth2-proxy) or in the gateway itself; JWT auth + JWT→virtual-key mapping are already in the core&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The conclusion that closes the decision: in a &lt;strong>K8s-native design with a gateway in front&lt;/strong>, OIDC and authorisation live in the gateway (Envoy/Higress + OPA) and the audit trail in &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">Langfuse/OTel&lt;/a>, both Apache 2.0 and already in the stack, so &lt;strong>LiteLLM placed behind barely needs its Enterprise tier&lt;/strong>: the governance pieces sit where they architecturally belong and happen to be OSS. The only real cost is the &lt;strong>DIY integration and maintenance&lt;/strong>, and, for ENS, designing the guarantees of the audit trail (retention, immutability/WORM) is on you, it does not come out of the box.&lt;/p>
&lt;p>The rest, Envoy AI Gateway, GIE, Higress, APISIX and Bifrost, pass the licence filter clean: &lt;strong>Apache 2.0 end to end&lt;/strong>, with no critical feature behind a wall.&lt;/p>
&lt;h2 id="filter-2--fit-kubernetes-citizen-or-tenant">Filter 2 — Fit: Kubernetes citizen or tenant?&lt;/h2>
&lt;p>With a &lt;strong>K8s-native&lt;/strong> priority (RKE2), the question is whether the gateway is modelled as &lt;strong>cluster resources&lt;/strong>, versioned with GitOps, observed with the same tools and integrated with the scheduler, or whether it is a separate process you have to operate on the side. This is where the structural novelty of 2025-2026 appears: the &lt;strong>Gateway API Inference Extension (GIE)&lt;/strong>.&lt;/p>
&lt;p>GIE is an extension of the standard Kubernetes Gateway API, from SIG-Network, which adds two pieces: the &lt;strong>InferencePool&lt;/strong> (a pool of replicas of a model as a native resource) and the &lt;strong>Endpoint Picker (EPP)&lt;/strong>, a scheduler that decides which replica each request goes to &lt;strong>according to the inference state&lt;/strong>: queue length, available LoRA adapters, and, the key piece, the &lt;strong>prefix cache state&lt;/strong> of each replica. It is exactly the &lt;em>prefix-aware routing&lt;/em> that the &lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">router post&lt;/a> explained as the reason hit rate goes from 5-15 % to 60-85 %, now as a &lt;strong>community standard&lt;/strong> instead of a proprietary feature of each product.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 820 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Gateway API Inference Extension model over vLLM">
&lt;defs>&lt;marker id="gx" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">Gateway API Inference Extension · inference-aware routing&lt;/text>
&lt;rect x="30" y="120" width="120" height="50" rx="7" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.4"/>
&lt;text x="90" y="142" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">Client&lt;/text>
&lt;text x="90" y="158" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">OpenAI-compatible&lt;/text>
&lt;rect x="190" y="116" width="140" height="58" rx="8" fill="#dceede" stroke="#3c8c54" stroke-width="1.8"/>
&lt;text x="260" y="138" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">Gateway&lt;/text>
&lt;text x="260" y="154" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">Envoy AI GW / Higress&lt;/text>
&lt;text x="260" y="166" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">(Gateway API impl.)&lt;/text>
&lt;rect x="370" y="60" width="150" height="64" rx="9" fill="#e6ddf3" stroke="#7a5aa5" stroke-width="2"/>
&lt;text x="445" y="84" text-anchor="middle" font-family="sans-serif" font-size="11.5" font-weight="700" fill="#222">Endpoint Picker&lt;/text>
&lt;text x="445" y="100" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">prefix cache · KV · queue&lt;/text>
&lt;text x="445" y="114" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">· LoRA available&lt;/text>
&lt;rect x="370" y="150" width="150" height="40" rx="7" fill="#f7efda" stroke="#c79a32" stroke-width="1.4"/>
&lt;text x="445" y="174" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#222">InferencePool&lt;/text>
&lt;rect x="580" y="70" width="210" height="40" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="1.4"/>
&lt;text x="685" y="94" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#222">vLLM replica A (hot KV)&lt;/text>
&lt;rect x="580" y="120" width="210" height="40" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="1.4"/>
&lt;text x="685" y="144" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#222">vLLM replica B&lt;/text>
&lt;rect x="580" y="170" width="210" height="40" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="1.4"/>
&lt;text x="685" y="194" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#222">vLLM replica C&lt;/text>
&lt;path d="M150,145 L190,145" fill="none" stroke="#666" stroke-width="1.5" marker-end="url(#gx)"/>
&lt;path d="M330,135 L370,100" fill="none" stroke="#7a5aa5" stroke-width="1.5" marker-end="url(#gx)"/>
&lt;text x="350" y="112" text-anchor="middle" font-family="sans-serif" font-size="8.5" fill="#7a5aa5">queries&lt;/text>
&lt;path d="M445,124 L445,150" fill="none" stroke="#666" stroke-width="1.3" marker-end="url(#gx)"/>
&lt;path d="M520,90 L580,90" fill="none" stroke="#3c8c54" stroke-width="1.8" marker-end="url(#gx)"/>
&lt;text x="550" y="82" text-anchor="middle" font-family="sans-serif" font-size="8.5" font-weight="700" fill="#3c8c54">cache hit&lt;/text>
&lt;path d="M520,170 L580,140" fill="none" stroke="#999" stroke-width="1" marker-end="url(#gx)" stroke-dasharray="3 2"/>
&lt;text x="445" y="240" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#444">The EPP routes to the replica whose prefix is already hot → 60-85% hit rate instead of blind round-robin&lt;/text>
&lt;text x="445" y="262" text-anchor="middle" font-family="sans-serif" font-size="9.5" font-style="italic" fill="#999">all as native Kubernetes resources (Gateway API), versionable with GitOps&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Why this settles the decision when the priority is K8s-native: GIE turns inference-aware routing into a &lt;strong>standard interface&lt;/strong> that several implementations satisfy (Envoy AI Gateway, Higress, kgateway, Istio, GKE Inference Gateway). You pick an implementation today and, if you change it tomorrow, the resource model (&lt;code>InferencePool&lt;/code>, &lt;code>HTTPRoute&lt;/code>) is preserved. That is precisely the property you wanted from a decision that is expensive to reverse: &lt;strong>the coupling is to a standard, not to a product&lt;/strong>. A standalone proxy like LiteLLM, however good it is, lives &lt;em>outside&lt;/em> this model. It is one more Deployment, with its own config, its own catalogue format and its own management plane.&lt;/p>
&lt;h2 id="filter-3--maturity-and-documentation-the-uncomfortable-honesty">Filter 3 — Maturity and documentation: the uncomfortable honesty&lt;/h2>
&lt;p>Here comes the tension no marketing comparison admits: &lt;strong>what is K8s-native-correct and what is battle-tested-today do not quite coincide in June 2026&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Envoy AI Gateway: the AI layer is at v0.5, pre-1.0 and young. &lt;em>But&lt;/em> it runs on Envoy and Envoy Gateway, which are among the most mature things that exist in proxies. It has the most complete GIE/EPP integration, model virtualisation and OpenInference tracing. Risk: the AI layer still moves fast (breaking changes are possible). Docs are growing, and good.&lt;/li>
&lt;li>GIE: the project is reaching GA (one of the first standardised inference-aware extensions). Standard maturity is high and rising; the maturity of each implementation varies.&lt;/li>
&lt;li>Higress: the most mature today among the K8s-native Apache-2.0 options with AI. Production at Alibaba scale, CNCF, Envoy/Istio base, AI plugins included, Gateway API support. If &amp;ldquo;mature&amp;rdquo; weighs more than &amp;ldquo;cutting-edge AI-native&amp;rdquo;, it is the safe bet.&lt;/li>
&lt;li>APISIX: a very mature classic gateway, with growing AI plugins; less specialised in inference-aware routing (prefix/KV) than the GIE model. Battle-tested at massive scale.&lt;/li>
&lt;li>LiteLLM: the one with the best documentation and the widest adoption in the field, by a distance. Mature as a proxy. Its ceiling is the language (Python/FastAPI: ~250-300 RPS per instance, scaling out by replicas) and the governance gating already covered.&lt;/li>
&lt;li>Bifrost: Apache 2.0, Go, the highest performer (~11 µs overhead at 5,000 RPS), with semantic caching and governance built in. Younger and less proven over years, but technically strong.&lt;/li>
&lt;/ul>
&lt;h2 id="decision-matrix">Decision matrix&lt;/h2>
&lt;p>Weighting the four filters for the case of &lt;strong>RKE2 + vLLM, K8s-native priority, permissive core acceptable&lt;/strong>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Candidate&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>K8s-native&lt;/th>
&lt;th>Maturity&lt;/th>
&lt;th>Docs&lt;/th>
&lt;th>Inference-aware&lt;/th>
&lt;th>Verdict&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Envoy AI GW + GIE/EPP&lt;/td>
&lt;td>✅ Apache 2.0&lt;/td>
&lt;td>✅✅ standard&lt;/td>
&lt;td>⚠️ v0.5 (mature Envoy)&lt;/td>
&lt;td>✅ good&lt;/td>
&lt;td>✅✅ prefix/KV/LoRA&lt;/td>
&lt;td>Primary (trajectory)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Higress (+ GIE)&lt;/td>
&lt;td>✅ Apache 2.0&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>✅✅ production&lt;/td>
&lt;td>✅ good&lt;/td>
&lt;td>✅ via GIE/plugins&lt;/td>
&lt;td>Primary (maturity today)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>APISIX&lt;/td>
&lt;td>✅ Apache 2.0&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>⚠️ less specialised&lt;/td>
&lt;td>Solid alternative&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LiteLLM&lt;/td>
&lt;td>⚠️ MIT core, gov. gated&lt;/td>
&lt;td>❌ separate proxy&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>✅✅✅&lt;/td>
&lt;td>⚠️ basic&lt;/td>
&lt;td>Control plane behind&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bifrost&lt;/td>
&lt;td>✅ Apache 2.0&lt;/td>
&lt;td>⚠️&lt;/td>
&lt;td>⚠️ young&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>One to watch (perf)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kong&lt;/td>
&lt;td>⚠️ AI plugins Enterprise&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>✅✅&lt;/td>
&lt;td>❌ gated&lt;/td>
&lt;td>Ruled out (OSS AI)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="the-recommendation">The recommendation&lt;/h2>
&lt;p>For the case we fixed, &lt;strong>RKE2 + vLLM, K8s-native priority, permissive licence with a sufficient core&lt;/strong>, the recommendation has two layers, not one:&lt;/p>
&lt;p>&lt;strong>Data layer (what sits in front of the replicas): adopt the Gateway API Inference Extension model.&lt;/strong> It is the decision that ages well because it couples you to an Apache 2.0 &lt;strong>standard&lt;/strong>, not to a product, and because it brings the prefix/KV/LoRA-aware routing that really moves the hit rate. For the implementation:&lt;/p>
&lt;ul>
&lt;li>If the &lt;strong>AI-native trajectory&lt;/strong> weighs more and you accept operating a v0.x layer on top of a mature Envoy: &lt;strong>Envoy AI Gateway + GIE/EPP&lt;/strong>. That is where the cutting edge and the most complete integration are.&lt;/li>
&lt;li>If &lt;strong>today&amp;rsquo;s maturity&lt;/strong> outweighs everything else (ENS, critical production, zero appetite for v0.x in the path of 100 % of the traffic): &lt;strong>Higress&lt;/strong> with GIE. CNCF, proven at scale, Apache 2.0, and migratable to the standard EPP.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Control layer (optional, behind): LiteLLM (MIT)&lt;/strong> if you need a single multi-provider point with virtual keys, spend tracking and fallbacks to external models. It goes &lt;strong>behind&lt;/strong> the data gateway, not in its place, and you accept that SSO, audit and RBAC are solved with the cluster&amp;rsquo;s own OIDC and auditing (as we set out in the criterion). If your front end is &lt;strong>only&lt;/strong> on-premise vLLM with no external providers, this layer is probably unnecessary.&lt;/p>
&lt;p>&lt;strong>What I would not recommend here&lt;/strong>: Kong (its AI features are gated, which contradicts the licence criterion); and starting with LiteLLM &lt;strong>as the main data gateway&lt;/strong> in a stack that will be multi-replica and K8s-native, since its Python ceiling and its position outside the Gateway API model turn it into debt the day you scale.&lt;/p>
&lt;h2 id="applied-to-our-infrastructure-rke2--vllm--4h100">Applied to our infrastructure: RKE2 + vLLM + 4×H100&lt;/h2>
&lt;p>The concrete deployment on the generic reference cluster:&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="c"># The InferencePool groups the replicas of a model (GIE resource)&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">inference.networking.x-k8s.io/v1alpha2&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">InferencePool&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&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-70b-pool }&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">vllm-llama70b }&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">extensionRef&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">llama-70b-epp } &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the Endpoint Picker&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="c"># The standard Gateway API route points at the pool&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">gateway.networking.k8s.io/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">HTTPRoute&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&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-70b }&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">parentRefs&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="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">ai-gateway } ]&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">rules&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">backendRefs&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">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inference.networking.x-k8s.io&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">InferencePool&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">llama-70b-pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three notes on how this fits the blog series:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>It lives in layer 1&lt;/strong> of the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven-layer stack&lt;/a>, in front of the replicas pinned by the &lt;a href="https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/">RKE2 resource managers&lt;/a>. The gateway routes; the kubelet pins; both are resources of the same GitOps cluster.&lt;/li>
&lt;li>&lt;strong>The EPP reads the prefix cache state&lt;/strong> of each replica, which connects directly with the engineering of &lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">prefix cache hit rate&lt;/a>: the gateway is what turns that affinity into actual routing.&lt;/li>
&lt;li>&lt;strong>The gateway&amp;rsquo;s &lt;code>gen_ai.*&lt;/code> tracing&lt;/strong> feeds the &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">OTel observability&lt;/a> pipeline and lands in Langfuse, closing the loop of the Observe layer.&lt;/li>
&lt;/ul>
&lt;h2 id="traps-and-things-that-are-not-what-they-seem">Traps and things that are not what they seem&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;It is open source&amp;rdquo; without reading what falls under Enterprise.&lt;/strong> The mistake this post tries to avoid: Kong and LiteLLM are OSS, but the features that justify choosing them for AI (AI plugins in Kong; SSO/audit/RBAC/guardrails in LiteLLM) are partly or wholly gated. &amp;ldquo;Open source&amp;rdquo; is the wrong question; &amp;ldquo;is what I need permissive?&amp;rdquo; is the right one.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;OpenAI-compatible&amp;rdquo; ≠ &amp;ldquo;inference-aware&amp;rdquo;.&lt;/strong> Almost all of them expose the OpenAI API. That does not mean they understand the KV cache state, the queue of each replica or the LoRA adapters. API compatibility is table stakes; intelligent routing (EPP) is a different league. Do not confuse &amp;ldquo;speaks OpenAI&amp;rdquo; with &amp;ldquo;routes well&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Choosing by the RPS benchmark.&lt;/strong> Bifrost wins on raw overhead, but the bottleneck of an on-premise vLLM fleet is rarely the proxy: it is the GPUs. Optimising the gateway for 5,000 RPS when your replicas serve 300 req/s is solving the problem you do not have. The latency the gateway adds matters; its peak throughput almost never does, in this context.&lt;/p>
&lt;p>&lt;strong>Phone-home telemetry in a sovereign deployment.&lt;/strong> Several gateways send telemetry home by default. In an ENS or sovereignty context, that is an audit finding, not a detail. Verify and disable the phone-home &lt;strong>before&lt;/strong> putting the gateway in the path of 100 % of the traffic; document it for the &lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">ENS/42001/EU AI Act controls file&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Betting on a v0.x in the critical path with no rollback plan.&lt;/strong> Envoy AI Gateway v0.5 is promising, but it sits in the path of 100 % of the traffic. If you choose it, have the rollback rehearsed and follow the changelog: v0.x releases break things. The GIE standard mitigates this (you can change implementation), but the specific implementation has to be operated with a safety net.&lt;/p>
&lt;p>&lt;strong>Confusing the data gateway with the control plane.&lt;/strong> LiteLLM in front of everything &lt;em>seems&lt;/em> to simplify, but it mixes two roles: routing data traffic (where you want K8s-native + inference-aware) and managing multi-provider keys and spend (where LiteLLM shines). Separate them: a GIE-native data gateway, with LiteLLM as a control plane behind it if needed.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The gateway is the piece with the highest cost of reversal in the serving layer, and that forces you to invert the intuitive order of criteria. &lt;strong>First the licence&lt;/strong>, not &amp;ldquo;is it OSS?&amp;rdquo; but &amp;ldquo;is what I need permissive or is it gated?&amp;rdquo;, which already rules Kong out for OSS AI and puts an asterisk on LiteLLM for its Enterprise governance. &lt;strong>Then the fit&lt;/strong>, where the Gateway API Inference Extension turns inference-aware routing into an Apache 2.0 standard you can couple to without marrying a product. And &lt;strong>only after that&lt;/strong> maturity, docs and features, where honesty forces you to admit that the cutting-edge AI-native option (Envoy AI Gateway v0.5) and the battle-tested-today one (Higress) are still not the same thing. For an RKE2 + vLLM stack with a K8s-native priority, the answer is not a product but a &lt;strong>model&lt;/strong>, GIE/EPP, implemented with Envoy AI Gateway if you look at the trajectory or with Higress if you look at maturity, and LiteLLM as an optional control plane behind. The decision that ages well is the one that couples you to a standard, not to a vendor; in gateways, in June 2026, that standard finally has a name.&lt;/p>
&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> — budgets and gateway spend tracking for multi-tenancy.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">The LLM inference router: the L7 switchboard&lt;/a> — the post explaining &lt;em>what&lt;/em> an inference router is and &lt;em>why&lt;/em> it exists; this one picks &lt;em>which&lt;/em> with verified licences. Read them in order.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/">RKE2 resource managers&lt;/a> — the gateway routes towards the replicas these managers pin to the right NUMA node; both are resources of the same GitOps cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/prefix-cache-hit-rate-engineering/">Prefix cache hit rate engineering&lt;/a> — the affinity the Endpoint Picker turns into actual routing; the gateway is what converts a hot cache into hit rate.&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/">The on-premise LLM inference stack in seven layers&lt;/a> — the gateway is layer 1; this post picks the concrete piece that occupies it.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">OSS vs hyperscalers&lt;/a> — the gateway inside the full OSS map and its translation to the public clouds.&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 &lt;code>gen_ai.*&lt;/code> tracing born in the gateway that feeds the Observe layer.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls ENS × 42001 × EU AI Act&lt;/a> — why the licence, the sovereignty of the deployment and the gateway&amp;rsquo;s phone-home are audit matters, not details.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving&lt;/a> — the EPP routes with awareness of which LoRA adapter each replica has loaded; the gateway and multi-adapter serving coordinate here.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Kubernetes, &lt;em>Introducing Gateway API Inference Extension&lt;/em> (blog oficial, jun 2025): &lt;a href="https://kubernetes.io/blog/2025/06/05/introducing-gateway-api-inference-extension/">https://kubernetes.io/blog/2025/06/05/introducing-gateway-api-inference-extension/&lt;/a>.&lt;/li>
&lt;li>Gateway API Inference Extension, doc del proyecto (Endpoint Picker, InferencePool): &lt;a href="https://gateway-api-inference-extension.sigs.k8s.io/">https://gateway-api-inference-extension.sigs.k8s.io/&lt;/a>.&lt;/li>
&lt;li>Envoy AI Gateway, sitio y release notes (v0.x, GIE integration): &lt;a href="https://aigateway.envoyproxy.io/">https://aigateway.envoyproxy.io/&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;em>Enterprise Features&lt;/em> (qué está gated): &lt;a href="https://docs.litellm.ai/docs/proxy/enterprise">https://docs.litellm.ai/docs/proxy/enterprise&lt;/a> · licencia MIT: &lt;a href="https://github.com/BerriAI/litellm/blob/main/LICENSE">https://github.com/BerriAI/litellm/blob/main/LICENSE&lt;/a>.&lt;/li>
&lt;li>Kong, &lt;em>Announcing Kong&amp;rsquo;s Open Source AI Gateway&lt;/em> y matriz de plugins Enterprise: &lt;a href="https://konghq.com/blog/product-releases/announcing-kong-ai-gateway">https://konghq.com/blog/product-releases/announcing-kong-ai-gateway&lt;/a>.&lt;/li>
&lt;li>Higress (Apache 2.0, CNCF, AI gateway): &lt;a href="https://higress.cn/en/">https://higress.cn/en/&lt;/a>.&lt;/li>
&lt;li>Apache APISIX, &lt;em>APISIX vs Kong&lt;/em> (cobertura de IA OSS): &lt;a href="https://apisix.apache.org/learning-center/apisix-vs-kong/">https://apisix.apache.org/learning-center/apisix-vs-kong/&lt;/a>.&lt;/li>
&lt;li>vLLM, &lt;em>Semantic Router v0.1 Iris&lt;/em> y &lt;em>Production Stack&lt;/em> (routing específico de vLLM): &lt;a href="https://blog.vllm.ai/2026/01/05/vllm-sr-iris.html">https://blog.vllm.ai/2026/01/05/vllm-sr-iris.html&lt;/a>.&lt;/li>
&lt;/ul></description></item><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>