<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Semantic-Cache on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/semantic-cache/</link><description>Recent content in Semantic-Cache on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Tue, 02 Jun 2026 03:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/semantic-cache/index.xml" rel="self" type="application/rss+xml"/><item><title>El router de inferencia LLM: la centralita L7 que en el post de canary llamábamos LoadBalancer</title><link>https://blog.lo0.es/posts/router-inferencia-llm-gateway-l7/</link><pubDate>Tue, 02 Jun 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/router-inferencia-llm-gateway-l7/</guid><description>&lt;blockquote>
&lt;p>Este post es la continuación natural de &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow para modelos LLM&lt;/a>. Allí la mecánica de promoción depositó toda la complejidad de reparto de tráfico en una caja a la que llamamos &amp;ldquo;LoadBalancer&amp;rdquo;. La descripción era operacional —servía para entender la coreografía— pero estructuralmente vaga: lo que de verdad hace ese reparto es un router de inferencia L7 con awareness LLM, una pieza de pleno derecho del stack (&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">capa 1 de las siete capas&lt;/a>) que merece su propio post.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>En el post anterior sobre &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">canary&lt;/a> llamamos &lt;strong>LoadBalancer&lt;/strong> a la pieza que reparte tráfico entre los pools v1 estable y v2 candidato. La descripción servía para entender el flujo, pero técnicamente era borrosa: ni un LoadBalancer L4 (kube-proxy, MetalLB, IPVS) ni un LoadBalancer L7 HTTP genérico (NGINX o HAProxy sin extensión) saben qué es un modelo, qué es una versión, cuántos tokens cuesta una request, qué prefijo tiene el prompt o qué KV cache tiene caliente cada réplica. La pieza correcta es un &lt;strong>router de inferencia LLM&lt;/strong>: un proxy L7 con conocimiento explícito del dominio. Combina cuatro funciones: &lt;strong>catálogo de modelos&lt;/strong> (resolver &lt;code>model=llama-70b@v2&lt;/code> → &lt;code>service.namespace:port&lt;/code>), &lt;strong>traffic splitting&lt;/strong> (aplicar el weight de canary con hash determinista o sticky deliberado para A/B), &lt;strong>política transversal&lt;/strong> (auth OIDC, rate limit y quota por tenant, redact PII pre-prompt, guardrails ligeros inline, propagación de tracing &lt;code>gen_ai.*&lt;/code>) y &lt;strong>failover/degradación&lt;/strong> (si v2 cae, redirigir a v1; si todo el cluster está saturado, devolver 503 con &lt;code>Retry-After&lt;/code> en vez de encolar para siempre). La pieza &lt;strong>no obvia&lt;/strong> que justifica su existencia técnica más allá de la operacional es el &lt;strong>prefix-aware routing&lt;/strong>: el router decide a qué réplica de la flota va cada request en función del prefijo del prompt, para que un sistema RAG con el mismo system prompt + el mismo bloque de documentos recuperados acierte sistemáticamente en el prefix cache (RadixAttention en SGLang, PrefixCaching en vLLM, KV reuse en TensorRT-LLM) de la &lt;strong>misma&lt;/strong> réplica, multiplicando el hit rate del &lt;strong>5–15 %&lt;/strong> (round-robin ciego) al &lt;strong>60–85 %&lt;/strong> (afinidad por prefix). Las piezas concretas en mayo 2026 son &lt;strong>LiteLLM Proxy&lt;/strong> (la opción más simple, OpenAI-compatible, catálogo declarativo YAML), &lt;strong>vLLM Production Stack router&lt;/strong> (específico para flotas vLLM, aware del KV cache y del prefix), &lt;strong>Envoy AI Gateway&lt;/strong> (filtros Envoy LLM-aware, integrable con Istio), &lt;strong>Kong AI Gateway&lt;/strong> (alternativa empresarial con plugin ecosystem), &lt;strong>KGateway&lt;/strong> (CNCF en gestación) y &lt;strong>NVIDIA Dynamo router&lt;/strong> (production-grade, aware de &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">disaggregated serving prefill/decode&lt;/a>). En el stack de &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">siete capas&lt;/a> vive en la &lt;strong>capa 1&lt;/strong> (gateway); en el de &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">cinco niveles de madurez&lt;/a> aparece a partir del &lt;strong>nivel 3&lt;/strong>; en el ciclo de &lt;a href="https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/">siete fases de despliegue&lt;/a> es la última pieza que &lt;strong>F6&lt;/strong> cierra. Este post incluye un manifest mínimo aplicable a un cluster genérico de 4×H100 SXM.&lt;/p>
&lt;h2 id="estás-aquí-deploy-capa-1-del-stack">Estás aquí: DEPLOY (capa 1 del 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="estás aquí: Deploy, capa 1 del 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">Estás aquí: DEPLOY · capa 1 del stack (gateway / router de inferencia)&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="el-antecedente-lo-que-el-post-de-canary-llamaba-loadbalancer">El antecedente: lo que el post de canary llamaba &amp;ldquo;LoadBalancer&amp;rdquo;&lt;/h2>
&lt;p>En &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow para modelos LLM&lt;/a> describimos el flujo así: &lt;em>&amp;ldquo;el LoadBalancer reparte progresivamente el tráfico siguiendo un cronograma: 1 % → 5 % → 25 % → 100 %&amp;rdquo;&lt;/em>. Era una descripción &lt;strong>operacional&lt;/strong> correcta — el lector entendía la coreografía sin necesitar más. Pero &lt;strong>técnicamente&lt;/strong> dejaba sin nombre a una pieza que merece tratamiento explícito, porque ninguno de los dos sentidos habituales de &amp;ldquo;LoadBalancer&amp;rdquo; hace lo que ese párrafo asumía:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Un LoadBalancer L4&lt;/strong> —kube-proxy con iptables/IPVS, MetalLB, F5 BIG-IP en modo TCP— reparte paquetes IP sin mirar dentro del payload. No sabe qué modelo se pide, ni qué versión, ni cuántos tokens lleva, ni si el cliente tiene quota. No puede aplicar el weight del canary &amp;ldquo;para el modelo X versión 2&amp;rdquo;: para él todos los paquetes hacia el VIP &lt;code>vllm-llama70b&lt;/code> son indistinguibles.&lt;/li>
&lt;li>&lt;strong>Un LoadBalancer L7 HTTP genérico&lt;/strong> —NGINX o HAProxy en modo HTTP sin extensión, una Service de tipo &lt;code>ClusterIP&lt;/code> con backend múltiple— sí reparte por URL y puede hacer routing por header, pero &lt;strong>no entiende el cuerpo OpenAI-compatible&lt;/strong> de la request. No sabe que &lt;code>{&amp;quot;model&amp;quot;: &amp;quot;llama-70b&amp;quot;, &amp;quot;messages&amp;quot;: [...]}&lt;/code> lleva en el campo &lt;code>model&lt;/code> la clave de routing; no cuenta tokens; no aplica políticas sobre estructuras LLM; no hace prefix-aware routing porque eso exige parsear el &lt;code>messages&lt;/code> y hashear el prefijo común.&lt;/li>
&lt;/ul>
&lt;p>La pieza que el post de canary asumía haciendo este trabajo es un &lt;strong>router de inferencia L7 con awareness LLM&lt;/strong>. Una capa de pleno derecho, con su propia configuración, su propio CI/CD, sus propias métricas y sus propios pitfalls. Este post la nombra y la desmonta.&lt;/p>
&lt;h2 id="la-analogía-la-centralita-y-triage-de-un-hospital-con-múltiples-especialidades">La analogía: la centralita y triage de un hospital con múltiples especialidades&lt;/h2>
&lt;p>Un hospital grande recibe pacientes que llegan a urgencias por puertas distintas y que necesitan especialidades distintas: traumatología, cardiología, pediatría, oncología. Hay tres modelos posibles de &amp;ldquo;puerta de entrada&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Puerta única sin triage.&lt;/strong> Todos los pacientes esperan en la misma sala y los van pasando por orden de llegada al primer médico libre, sea su especialidad la que sea. Funciona en un consultorio de aldea con un único médico generalista. Cuando hay 200 pacientes al día y 12 especialidades, cae rápido en disfunción: el cardiólogo atiende esguinces, el pediatra atiende infartos, los recursos especializados se desperdician. Es el equivalente del LoadBalancer L4 — reparte cuerpos sin entender qué traen.&lt;/p>
&lt;p>&lt;strong>Puerta con receptionist que pregunta el síntoma.&lt;/strong> Una persona en mesa de entrada pregunta &amp;ldquo;¿qué le pasa?&amp;rdquo; y dirige al paciente al pasillo correcto. El cardiólogo ve solo cardiología, el pediatra solo niños. Mejor, pero el receptionist es lento, no calibra urgencias y no conoce el estado de las salas: puede mandar al cardiólogo del pasillo A cuando el del B está libre. Es el equivalente de un L7 HTTP genérico con &lt;code>path-based routing&lt;/code> — reparte por categoría pero sin información del estado interno.&lt;/p>
&lt;p>&lt;strong>Triage profesional con awareness completo.&lt;/strong> Una enfermera de triage formada que conoce el catálogo de especialidades, sabe qué box está ocupado y cuál libre, recuerda al paciente recurrente cuyo expediente ya está abierto en el sistema (manda al mismo médico para continuidad), aplica política transversal (verifica cobertura del seguro, registra alérgenos, redirige a urgencias pediátricas si el paciente es menor) y, si la sala de cardiología cae por una avería del electrocardiograma, redirige al hospital del otro lado de la ciudad. Esta es la pieza que un hospital grande necesita. En LLM se llama &lt;strong>router de inferencia&lt;/strong>.&lt;/p>
&lt;p>La analogía sostiene hasta el último detalle, incluido el del &amp;ldquo;expediente ya abierto&amp;rdquo;: el paciente que vuelve al mismo médico es exactamente el cliente cuyo prompt comparte prefijo con el de hace 5 minutos. Si el router lo manda a la &lt;strong>misma réplica&lt;/strong>, esa réplica todavía tiene el KV cache caliente y la request acierta el prefix cache. Si lo manda a una réplica distinta porque iba &amp;ldquo;la siguiente en round-robin&amp;rdquo;, el KV cache hay que reconstruirlo desde cero y la TTFT se va al doble. La enfermera de triage sabe esto. El LoadBalancer ciego no.&lt;/p>
&lt;h2 id="las-cuatro-funciones-del-router-de-inferencia">Las cuatro funciones del router de inferencia&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="cuatro funciones del router de inferencia LLM">
&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">Cuatro funciones que el router de inferencia combina&lt;/text>
&lt;rect x="20" y="40" width="380" height="120" class="b c"/>
&lt;text x="30" y="62" class="h">1 · CATÁLOGO DE MODELOS&lt;/text>
&lt;text x="30" y="82" class="l">Resolver `model=llama-70b@v2` → service:port&lt;/text>
&lt;text x="30" y="102" class="l">Versionado, aliases, lifecycle (preview/stable/deprecated)&lt;/text>
&lt;text x="30" y="125" class="n">Lo que evita que el cliente conozca topología.&lt;/text>
&lt;text x="30" y="145" class="n">Sin esto, cada cliente sabe IPs/puertos internos.&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">Weight de canary / blue-green / shadow&lt;/text>
&lt;text x="430" y="102" class="l">Hash determinista por request o sticky deliberado&lt;/text>
&lt;text x="430" y="125" class="n">Las particiones del post de canary se aplican aquí,&lt;/text>
&lt;text x="430" y="145" class="n">no en el motor de inferencia.&lt;/text>
&lt;rect x="20" y="170" width="380" height="120" class="b p"/>
&lt;text x="30" y="192" class="h">3 · POLÍTICA TRANSVERSAL&lt;/text>
&lt;text x="30" y="212" class="l">Auth OIDC · rate limit · quota por tenant&lt;/text>
&lt;text x="30" y="232" class="l">Redact PII pre-prompt · guardrails ligeros inline&lt;/text>
&lt;text x="30" y="252" class="l">Tracing gen_ai.* propagado · semantic cache&lt;/text>
&lt;text x="30" y="275" class="n">Lo que se aplica una vez por todos los modelos.&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 · DEGRADACIÓN&lt;/text>
&lt;text x="430" y="212" class="l">Si v2 cae → redirige a v1&lt;/text>
&lt;text x="430" y="232" class="l">Si todo saturado → 503 con Retry-After&lt;/text>
&lt;text x="430" y="252" class="l">Circuit breaker · health probes activos&lt;/text>
&lt;text x="430" y="275" class="n">Lo que evita encolar para siempre.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="función-1--catálogo-de-modelos">Función 1 — Catálogo de modelos&lt;/h3>
&lt;p>El router mantiene un catálogo declarativo que mapea identidad de modelo a deployment concreto:&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"># alias estable&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"># versión 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">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% del tráfico&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>El cliente envía &lt;code>{&amp;quot;model&amp;quot;: &amp;quot;llama-70b&amp;quot;, &amp;quot;messages&amp;quot;: [...]}&lt;/code> sin saber que detrás hay dos pools de réplicas. El router resuelve. Si mañana migras de vLLM a SGLang para una versión concreta, el cliente no se entera; cambias el &lt;code>endpoint&lt;/code> en el catálogo y listo.&lt;/p>
&lt;p>Lo que se gana con este desacoplamiento es la libertad de mover topología sin romper clientes. Lo que cuesta es mantener disciplinada la convención de nombres (&lt;code>llama-70b&lt;/code> siempre es el alias estable; &lt;code>llama-70b@v2&lt;/code> es la versión específica para canary). Sin esa disciplina, los aliases se ensucian con &lt;code>llama-70b-prod-fixed-real-final-v3&lt;/code> y el catálogo deja de ser navegable a las pocas semanas.&lt;/p>
&lt;h3 id="función-2--traffic-splitting">Función 2 — Traffic splitting&lt;/h3>
&lt;p>Las particiones del &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">post de canary&lt;/a> (1 % → 5 % → 25 % → 100 %) se materializan &lt;strong>aquí&lt;/strong>, no en el motor de inferencia. El router calcula un hash determinista del &lt;code>request_id&lt;/code> (o del &lt;code>user_id&lt;/code>, si se quiere sticky) y lo mapea al rango de weights del catálogo. Para un weight &lt;code>[v1: 95, v2: 5]&lt;/code>, el 5 % del espacio hash cae en v2 y el 95 % en v1.&lt;/p>
&lt;p>Tres decisiones de diseño que importan:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hash por &lt;code>request_id&lt;/code> aleatorio&lt;/strong> = muestreo independiente. Cada request es una observación independiente de la distribución v1 vs v2. Es el setting correcto para canary estadísticamente comparables.&lt;/li>
&lt;li>&lt;strong>Hash por &lt;code>user_id&lt;/code>&lt;/strong> = sticky por usuario. El mismo cliente ve siempre el mismo pool. Útil para A/B testing con memoria conversacional persistida, pero &lt;strong>rompe la comparabilidad estadística del canary&lt;/strong> porque las poblaciones de usuarios no son simétricas — pitfall explicado en el &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">post anterior&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Hash por &lt;code>tenant_id&lt;/code>&lt;/strong> = particionado fuerte. Tenant A va a v1, tenant B a v2. Es el patrón para clientes con SLA distintos o para validar v2 en un tenant interno antes de exponerlo a clientes externos.&lt;/li>
&lt;/ul>
&lt;h3 id="función-3--política-transversal">Función 3 — Política transversal&lt;/h3>
&lt;p>Una vez por encima de todos los modelos, el router aplica:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Auth&lt;/strong>: OIDC con tokens JWT validados contra Keycloak / Authentik. Headers &lt;code>Authorization: Bearer ...&lt;/code> traducidos a &lt;code>tenant_id&lt;/code> y &lt;code>roles&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Rate limit&lt;/strong>: token bucket por tenant (&lt;code>X req/min&lt;/code>) o por modelo (&lt;code>Y req/min&lt;/code> para llama-70b porque es caro).&lt;/li>
&lt;li>&lt;strong>Quota&lt;/strong>: cuota mensual de tokens consumidos por tenant. El router cuenta &lt;code>gen_ai.usage.input_tokens&lt;/code> + &lt;code>gen_ai.usage.output_tokens&lt;/code> y rechaza con &lt;code>429 Quota exceeded&lt;/code> cuando se agota.&lt;/li>
&lt;li>&lt;strong>Redact PII pre-prompt&lt;/strong>: Presidio o Llama Guard en línea antes de que el prompt toque el modelo. Lo que el modelo no ve, no se entrena con ello, no se loguea, no se filtra.&lt;/li>
&lt;li>&lt;strong>Guardrails ligeros inline&lt;/strong>: PromptGuard 2, Llama Guard 4, Granite Guardian — los que aparecen en &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails y safety en LLMs&lt;/a>— se ejecutan en el router porque su latencia (30–150 ms) cabe en el presupuesto de TTFT.&lt;/li>
&lt;li>&lt;strong>Propagación de tracing &lt;code>gen_ai.*&lt;/code>&lt;/strong>: el router inicia el span padre, propaga &lt;code>traceparent&lt;/code> al motor y emite los atributos &lt;code>gen_ai.system&lt;/code>, &lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.request.version&lt;/code> que el &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">tracing OTel GenAI&lt;/a> consume.&lt;/li>
&lt;li>&lt;strong>Semantic cache&lt;/strong>: para prompts repetidos exactos o con similitud semántica alta (embedding cosine &amp;gt; 0.97 contra cache previa), devuelve la respuesta cacheada sin tocar el motor. Ahorro típico en RAG con preguntas frecuentes: 20–40 % de las requests.&lt;/li>
&lt;/ul>
&lt;h3 id="función-4--failover-y-degradación">Función 4 — Failover y degradación&lt;/h3>
&lt;p>El router conoce el estado de salud de cada endpoint (health probes activos cada 5–15 s, latencia de TTFT recientes) y decide:&lt;/p>
&lt;ul>
&lt;li>Si v2 devuelve 5xx persistente o no responde, &lt;strong>circuit breaker&lt;/strong> abierto: el router redirige el tráfico que iba a v2 hacia v1 hasta que las probes vuelvan a verde. Esto es el rollback automático del canary en su forma más simple.&lt;/li>
&lt;li>Si todo el cluster está saturado (todas las réplicas reportan &lt;code>num_requests_waiting &amp;gt; N&lt;/code> durante T segundos), el router devuelve &lt;strong>&lt;code>503 Service Unavailable&lt;/code>&lt;/strong> con &lt;code>Retry-After: 30&lt;/code> en vez de encolar para siempre. Mejor decirle al cliente &amp;ldquo;vuelve en 30 segundos&amp;rdquo; que tenerlo esperando 4 minutos y luego dar timeout.&lt;/li>
&lt;li>Si hay multi-region o multi-cluster, &lt;strong>failover cross-cluster&lt;/strong> vía DNS o L7: la región primaria cae, el router de la secundaria asume.&lt;/li>
&lt;/ul>
&lt;h2 id="la-pieza-no-obvia-prefix-aware-routing">La pieza no obvia: prefix-aware routing&lt;/h2>
&lt;p>Esta es la función que un LoadBalancer convencional no puede hacer y que justifica un router específico de LLM más allá de las cuatro genéricas.&lt;/p>
&lt;p>El KV cache de vLLM, SGLang y TensorRT-LLM puede &lt;strong>reusar prefijos comunes entre requests&lt;/strong> —ver &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a>—. Concretamente:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>vLLM&lt;/strong> con &lt;code>--enable-prefix-caching&lt;/code>: detecta que la request actual comparte un prefijo (longitud múltiplo del block size, default 16 tokens) con una request anterior cuyas páginas todavía están en HBM, y reutiliza esas páginas en vez de reprocesarlas.&lt;/li>
&lt;li>&lt;strong>SGLang&lt;/strong> con &lt;strong>RadixAttention&lt;/strong>: estructura el cache como un árbol radix indexado por tokens; cada request acierta el camino común del árbol y solo computa la cola.&lt;/li>
&lt;li>&lt;strong>TensorRT-LLM&lt;/strong>: feature similar, llamado &lt;em>KV cache reuse&lt;/em>.&lt;/li>
&lt;/ul>
&lt;p>El hit rate del prefix cache es la métrica clave: cada token acertado es un token que &lt;strong>no se procesa en prefill&lt;/strong>, reduciendo TTFT en proporción directa. Para un sistema RAG típico —system prompt de 400 tokens + documentos retrieved de 2 000 tokens + pregunta del usuario de 50 tokens— el prefijo común (&lt;code>system_prompt + docs&lt;/code>) son 2 400 de los 2 450 tokens totales. Si el cache acierta, &lt;strong>el prefill solo procesa 50 tokens en vez de 2 450&lt;/strong>: TTFT cae aproximadamente a la &lt;strong>vigésima parte&lt;/strong>.&lt;/p>
&lt;p>Pero el cache vive &lt;strong>por réplica&lt;/strong>, no globalmente. Si dos requests con el mismo prefix de 2 400 tokens caen en réplicas distintas, ambas hacen el prefill completo: el cache de la primera no sirve a la segunda. La segunda paga el coste íntegro.&lt;/p>
&lt;p>Con &lt;strong>round-robin ciego&lt;/strong> (cualquier LB convencional), las requests se reparten uniformemente entre N réplicas. Para un cluster de 4 réplicas y 1 000 requests con el mismo &lt;code>system_prompt + docs&lt;/code>, &lt;strong>cada réplica recibe ~250 requests&lt;/strong>, pero las 4 hacen su propio &amp;ldquo;primer prefill&amp;rdquo; y los siguientes 249 se benefician dentro de su réplica. El hit rate global es decente pero no óptimo. Para tráfico con muchos sistemas prompts distintos y poca repetición intra-prefix, el hit rate ronda el &lt;strong>5–15 %&lt;/strong>.&lt;/p>
&lt;p>Con &lt;strong>prefix-aware routing&lt;/strong>, el router calcula un hash del prefijo del prompt (los primeros N tokens, o el &lt;code>system_prompt&lt;/code> declarado en &lt;code>messages[0]&lt;/code>) y mantiene una &lt;strong>tabla de afinidad&lt;/strong> &lt;code>hash → réplica&lt;/code>. Todas las requests con el mismo prefijo caen en la &lt;strong>misma&lt;/strong> réplica. La primera paga el prefill completo; las 999 siguientes aciertan el cache. Hit rate global: &lt;strong>60–85 %&lt;/strong>.&lt;/p>
&lt;p>El coste de implementarlo: el router debe parsear el body de la request (no solo el header HTTP), aplicar un tokenizer ligero o un hash basado en bytes, y mantener una tabla LRU/consistent-hash de afinidad que se rebalancea cuando una réplica entra o sale. Es trabajo de servidor, no de proxy genérico. &lt;strong>vLLM Production Stack router&lt;/strong> lo implementa nativamente. &lt;strong>NVIDIA Dynamo&lt;/strong> también. LiteLLM en su versión enterprise tiene un beta. Envoy AI Gateway lo está incorporando como filtro experimental.&lt;/p>
&lt;p>La diferencia operativa para un RAG productivo: con prefix-aware routing, el mismo cluster sirve &lt;strong>2–4× más requests&lt;/strong> sin añadir GPUs, simplemente porque el prefill desaparece en la mayoría de los casos.&lt;/p>
&lt;h2 id="token-aware-load-balancing">Token-aware load balancing&lt;/h2>
&lt;p>La segunda pieza no obvia. El round-robin clásico reparte por número de requests; pero un prompt de 50 tokens y otro de 8 000 tokens cuestan radicalmente distinto (factor ~160× en prefill). Repartir igualmente por count desequilibra severamente la carga real.&lt;/p>
&lt;p>&lt;strong>Token-aware load balancing&lt;/strong> suma tokens de prefill esperados (longitud del prompt) y decode esperados (max_tokens del cliente) por réplica activa, y manda la nueva request a la réplica con menor carga acumulada. Es lo que tanto vLLM Production Stack como NVIDIA Dynamo implementan como estrategia por defecto cuando se activa.&lt;/p>
&lt;p>La métrica que alimenta el cálculo es —otra vez— &lt;code>vllm:num_requests_running&lt;/code> y &lt;code>vllm:gpu_cache_usage_perc&lt;/code> —ver &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a>—, idealmente complementadas con un estimador de tokens del prompt entrante. Los routers maduros usan &lt;code>tiktoken&lt;/code> o el tokenizer real del modelo para contar tokens del prompt antes de elegir réplica.&lt;/p>
&lt;h2 id="comparativa-de-piezas-concretas-mayo-2026">Comparativa de piezas concretas (mayo 2026)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza&lt;/th>
&lt;th>Awareness LLM&lt;/th>
&lt;th>Prefix-aware&lt;/th>
&lt;th>Token-aware LB&lt;/th>
&lt;th>Multi-modelo&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>&lt;strong>LiteLLM Proxy&lt;/strong>&lt;/td>
&lt;td>Alta&lt;/td>
&lt;td>Beta (enterprise)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Excelente&lt;/td>
&lt;td>Sí (Redis)&lt;/td>
&lt;td>Muy alto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>vLLM Production Stack router&lt;/strong>&lt;/td>
&lt;td>Específico vLLM&lt;/td>
&lt;td>Sí, nativo&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Solo vLLM&lt;/td>
&lt;td>No (externa)&lt;/td>
&lt;td>Medio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NVIDIA Dynamo router&lt;/strong>&lt;/td>
&lt;td>Alta + disagg-aware&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>vLLM/SGLang/TRT-LLM&lt;/td>
&lt;td>No (externa)&lt;/td>
&lt;td>Bajo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Envoy AI Gateway&lt;/strong>&lt;/td>
&lt;td>Media (filtros)&lt;/td>
&lt;td>Experimental&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Vía filtro&lt;/td>
&lt;td>Medio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kong AI Gateway&lt;/strong>&lt;/td>
&lt;td>Media (plugins)&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí (plugin)&lt;/td>
&lt;td>Medio&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>KGateway&lt;/strong>&lt;/td>
&lt;td>Media&lt;/td>
&lt;td>Roadmap&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Roadmap&lt;/td>
&lt;td>Bajo (CNCF gestación)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NGINX + custom Lua&lt;/strong>&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>Bajo (build it yourself)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>LiteLLM Proxy&lt;/strong> es la opción por defecto para empezar. OpenAI-compatible, YAML simple, soporta los providers comerciales + cualquier OpenAI-compatible self-hosted. La versión OSS cubre las cuatro funciones básicas y semantic cache; el prefix-aware y la versión enterprise añaden multi-tenancy avanzado.&lt;/p>
&lt;p>&lt;strong>vLLM Production Stack router&lt;/strong> es la opción correcta si la flota es 100 % vLLM. Aware del KV cache, del prefix, del LoRA loaded por réplica. Integra mejor con métricas vLLM nativas.&lt;/p>
&lt;p>&lt;strong>NVIDIA Dynamo router&lt;/strong> es la opción production-grade más completa, especialmente si se opera &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">disaggregated serving&lt;/a> (prefill workers vs decode workers separados). Requiere stack NVIDIA-aligned.&lt;/p>
&lt;p>&lt;strong>Envoy AI Gateway&lt;/strong> y &lt;strong>Kong AI Gateway&lt;/strong> son las opciones si la organización ya tiene Envoy/Kong como gateway corporativo y quiere extenderlo con LLM-awareness sin introducir otra pieza nueva.&lt;/p>
&lt;h2 id="manifest-mínimo-litellm-proxy-sobre-cluster-genérico">Manifest mínimo: LiteLLM Proxy sobre cluster genérico&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 # token-aware basic
&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>El cliente final apunta a &lt;code>litellm-router.inference.svc:80/v1/chat/completions&lt;/code>, pone &lt;code>model=llama-70b&lt;/code>, y el router decide en cada request si va a v1 (95 %) o v2 (5 %), aplica el rate limit, busca en semantic cache, propaga tracing a Langfuse, y traduce de OpenAI-compatible a OpenAI-compatible del vLLM de destino. Tres réplicas del router para HA y para que el propio gateway escale horizontalmente con KEDA si hace falta —ver &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a>—.&lt;/p>
&lt;h2 id="cuatro-pitfalls-operacionales">Cuatro pitfalls operacionales&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — el router se convierte en SPoF si no se replica.&lt;/strong> Tres o más réplicas del propio router, detrás de un Service &lt;code>LoadBalancer&lt;/code> (este sí, L4) con healthchecks. Una sola réplica del router significa que cada deploy de la configuración cierra el servicio entero unos segundos.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — la latencia del router se suma a la del modelo.&lt;/strong> Cada función añade milisegundos: parsing del body (5–10 ms), auth JWT (2–5 ms), rate limit (1–2 ms), redact PII con Presidio (20–80 ms), guardrails con Llama Guard inline (50–150 ms), prefix hash (5–10 ms), token counting con tokenizer (10–30 ms). En total &lt;strong>100–300 ms&lt;/strong> de overhead antes de tocar el motor. Si el TTFT del modelo es 400 ms y el del router 200 ms, el cliente ve &lt;strong>600 ms&lt;/strong> — vale la pena medir cuánto cuesta cada función y desactivar las no críticas en el path de baja latencia.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — el catálogo deriva del estado real del cluster.&lt;/strong> El router cree que &lt;code>vllm-llama70b-v2&lt;/code> existe porque está en su YAML, pero el deployment fue retirado hace tres días y nadie actualizó el config. El router devuelve 502 en el 5 % del tráfico. Solución: validar el catálogo contra &lt;code>kubectl get svc&lt;/code> en CI; ningún &lt;code>endpoint&lt;/code> del catálogo puede apuntar a un Service inexistente. O mejor: el router descubre dinámicamente los endpoints disponibles vía label selector (&lt;code>app=vllm,model=llama-70b&lt;/code>) y aplica weights del catálogo sobre los que están vivos.&lt;/p>
&lt;p>&lt;strong>Pitfall 4 — semantic cache con embedding outdated.&lt;/strong> El semantic cache compara embedding del prompt nuevo contra embeddings de prompts cacheados. Si actualizas el modelo de embeddings (ver &lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation&lt;/a>), las distancias se calculan en un espacio distinto y el cache deja de funcionar correctamente (falsos hits o falsos misses). Política: el cache se &lt;strong>invalida&lt;/strong> al cambiar el modelo de embeddings; nunca se mezclan generaciones.&lt;/p>
&lt;h2 id="encaje-en-el-stack-y-la-madurez">Encaje en el stack y la madurez&lt;/h2>
&lt;p>En el &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">stack de siete capas&lt;/a>, el router es la &lt;strong>capa 1&lt;/strong>: la puerta de entrada que precede al motor de inferencia (capa 2), al KV cache + PagedAttention (capa 3) y al resto. Es la única pieza que ve &lt;strong>todo el tráfico&lt;/strong> desde fuera; cualquier política que no se aplique aquí, se duplica N veces en los motores.&lt;/p>
&lt;p>En los &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">cinco niveles de madurez&lt;/a>, el router aparece a partir del &lt;strong>nivel 3&lt;/strong> (GESTIONADO): sin OIDC + RBAC + cert-manager + NetworkPolicy default deny, el router no tiene a quien autenticar ni a quien aplicar quotas; antes del nivel 3 lo que toca es montar un proxy mínimo sin pretensión de catálogo. Plataformas que intentan tener router pulido en nivel 1 acaban con un yaml grande que nadie mantiene.&lt;/p>
&lt;p>En las &lt;a href="https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/">siete fases de despliegue&lt;/a>, el router es lo que cierra &lt;strong>F6&lt;/strong>: el último paso atómico que pone al cluster en producción. Sin router, F6 no termina — el catálogo, las quotas, los canaries y los failovers son condición necesaria para abrir tráfico productivo.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;p>Para un cluster genérico de &lt;strong>4 nodos × 4×H100 SXM 80 GB&lt;/strong>, el router de inferencia consume &lt;strong>recursos modestos&lt;/strong>: 3 réplicas del router-pod (CPU 2 cores, memoria 4 GiB cada una) bastan para soportar miles de RPS porque su trabajo es ligero (parsing, hashing, routing, no inferencia). El router vive en nodos &lt;strong>no-GPU&lt;/strong> del cluster (nodos de control plane o de workload general), nunca consume &lt;code>nvidia.com/gpu&lt;/code>.&lt;/p>
&lt;p>Volumen de tráfico que un LiteLLM con 3 réplicas y 4 workers cada una sostiene: &lt;strong>2 000–5 000 RPS&lt;/strong> routing a backend vLLM, con overhead de &lt;strong>80–150 ms&lt;/strong> en path completo (auth + rate limit + cache check + propagación). Si se necesita más, escalar el router con KEDA sobre &lt;code>litellm:requests_per_second&lt;/code> es trivial.&lt;/p>
&lt;p>Para clusters más grandes (16+ nodos GPU), considerar &lt;strong>vLLM Production Stack router&lt;/strong> o &lt;strong>NVIDIA Dynamo router&lt;/strong> que son más complejos pero exprimen el prefix-aware routing y el token-aware LB que LiteLLM OSS no cubre. Para clusters multi-region, &lt;strong>Envoy AI Gateway&lt;/strong> con Istio Service Mesh es la elección estándar.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Comparativa profunda LiteLLM vs vLLM PStack vs Dynamo&lt;/strong> con benchmarks de prefix-aware sobre cluster on-premise real.&lt;/li>
&lt;li>&lt;strong>Semantic cache con Redis Stack + RedisVL&lt;/strong>: hit rate, falsos positivos, política de TTL.&lt;/li>
&lt;li>&lt;strong>Multi-region routing&lt;/strong>: cómo el router decide entre clúster DC1 y DC2 según latencia, salud y carga.&lt;/li>
&lt;li>&lt;strong>AI Gateway specific features&lt;/strong>: token-bucket cost-based rate limiting (penaliza prompts largos), guardrails policy engine en el router.&lt;/li>
&lt;li>&lt;strong>Migration path&lt;/strong>: cómo introducir un router en un cluster que ya tiene clientes apuntando directo al servicio vLLM, sin downtime.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow para modelos LLM&lt;/a> — el post anterior donde llamamos &amp;ldquo;LoadBalancer&amp;rdquo; a esta pieza; este post la nombra y la desmonta.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Siete capas del stack de inferencia LLM on-premise&lt;/a> — el router es la capa 1 del stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">Cinco niveles de madurez&lt;/a> — el router aparece a partir del nivel 3.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/">Siete fases de despliegue&lt;/a> — el router es lo que cierra F6.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a> — el router puede escalar con KEDA sobre sus propias métricas; convive con el autoscaling de los motores.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a> — el token-aware LB consume &lt;code>vllm:num_requests_running&lt;/code> y &lt;code>vllm:gpu_cache_usage_perc&lt;/code> para decidir réplica.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> — qué cachea el prefix-aware routing y por qué multiplica el hit rate.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving prefill/decode&lt;/a> — los routers production-grade (Dynamo) son aware de la disaggregation y rutean prefill y decode a pools distintos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — el router emite los spans padre &lt;code>gen_ai.*&lt;/code> y propaga &lt;code>traceparent&lt;/code> a los motores.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails y safety en LLMs&lt;/a> — los guardrails ligeros inline se ejecutan típicamente en el router.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel para inferencia LLM&lt;/a> — el router por capability cobra todo su sentido cuando hay backends heterogéneos (NVIDIA para LLM grande, Intel para embeddings/reranker, NUC para edge); el catálogo se extiende con &lt;code>backend&lt;/code> y &lt;code>region&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&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>