<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Litellm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/litellm/</link><description>Recent content in Litellm 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/litellm/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><item><title>LLM Guard: el traductor jurado con cuaderno de equivalencias — anatomía, scanners y su integración con Langfuse, vLLM y LiteLLM</title><link>https://blog.lo0.es/posts/llm-guard-fundamentos/</link><pubDate>Mon, 01 Jun 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/llm-guard-fundamentos/</guid><description>&lt;blockquote>
&lt;p>Este post es &lt;strong>deep-dive de una sola pieza&lt;/strong> dentro de la capa cubierta en el &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post sobre guardrails y safety LLM&lt;/a>. Aquel mapea las cuatro líneas de defensa (input, retrieval, tool, output) y el catálogo OSS 2026 a vista de pájaro; éste baja al ras de &lt;strong>LLM Guard&lt;/strong> porque su patrón Anonymize/Deanonymize, su modelo de scanners composables y sus cuatro modos de despliegue merecen tratamiento propio. Las analogías que se construyeron arriba (cocina HACCP, cuatro CCP) siguen valiendo: este post amplía el zoom sobre la herramienta que ocupa el cinturón de PII y de scanners individuales dentro de esa arquitectura.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>LLM Guard es la herramienta OSS (MIT, Protect AI) que materializa la capa de guardrails LLM con un modelo radicalmente distinto al de NeMo Guardrails y al de Guardrails AI: en lugar de un DSL declarativo (Colang) o de un framework de validators con LLM-as-judge externos, ofrece un &lt;strong>catálogo de detectores compactos especializados&lt;/strong> —15 input scanners, 21 output scanners— componibles como pipeline Python, con un mecanismo único distintivo: el patrón &lt;strong>Anonymize → LLM → Deanonymize con Vault&lt;/strong>. El Vault es un almacén centralizado del mapping entre entidades reales (&lt;code>John Doe&lt;/code>, &lt;code>12345678X&lt;/code>) y placeholders (&lt;code>[REDACTED_PERSON_1]&lt;/code>, &lt;code>[REDACTED_DNI_1]&lt;/code>); en input, las entidades se redactan y el mapping se guarda; el LLM nunca ve datos personales reales; en output, el Deanonymize scanner restituye los originales antes de devolver la respuesta al usuario. Este post desmonta: la anatomía interna (Vault + scanners + orquestador con &lt;code>fail_fast&lt;/code> y caché TTL), los cuatro patrones de despliegue con sus matemáticas (librería in-process, API FastAPI, sidecar OTel sobre vLLM, plugin de AI Gateway — LiteLLM, Envoy AI Gateway, Kong AI Gateway), los diagramas de integración con Langfuse (vía OTel HTTP exporter de LLM Guard + &lt;code>langfuse.score()&lt;/code> desde el AI Gateway), las matemáticas con benchmarks del proyecto (Anonymize en 177 ms CPU → 128 ms ONNX-CPU → 125 ms GPU FP16 → 38 ms GPU+ONNX, escalado x4.6 cuando combinas ONNX + GPU), el patrón ONNX como aceleración por defecto sin GPU dedicada, la comparativa con NeMo Guardrails (DSL Colang declarativo orientado a flujo conversacional) y Guardrails AI (validators tipo contrato JSON con judges externos), la aplicación a hardware on-premise (qué scanners aguantan CPU, cuáles necesitan GPU compartida) y las siete trampas operativas específicas de la herramienta.&lt;/p>
&lt;h2 id="la-analogía-el-traductor-jurado-con-cuaderno-de-equivalencias">La analogía: el traductor jurado con cuaderno de equivalencias&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="LLM Guard como traductor jurado con cuaderno de equivalencias">
&lt;style>
.t-user{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.t-trad{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:8}
.t-model{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.t-vault{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:8}
.tl{font:600 13px sans-serif;fill:#222}
.ts{font:400 11px sans-serif;fill:#555}
.tn{font:italic 11px sans-serif;fill:#555}
.tar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mt1)}
.tcb{stroke:#7a5;stroke-width:1.4;fill:none;stroke-dasharray:5 3;marker-end:url(#mt2)}
&lt;/style>
&lt;defs>
&lt;marker id="mt1" 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;marker id="mt2" 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="#7a5"/>&lt;/marker>
&lt;/defs>
&lt;rect x="20" y="40" width="120" height="60" class="t-user"/>
&lt;text x="80" y="64" text-anchor="middle" class="tl">Cliente&lt;/text>
&lt;text x="80" y="82" text-anchor="middle" class="ts">"Mi DNI es 12345678X,&lt;/text>
&lt;text x="80" y="96" text-anchor="middle" class="ts">¿paga IVA?"&lt;/text>
&lt;rect x="180" y="40" width="160" height="60" class="t-trad"/>
&lt;text x="260" y="64" text-anchor="middle" class="tl">Traductor (Anonymize)&lt;/text>
&lt;text x="260" y="82" text-anchor="middle" class="ts">redacta entidades sensibles&lt;/text>
&lt;text x="260" y="96" text-anchor="middle" class="ts">+ inscribe en cuaderno&lt;/text>
&lt;rect x="380" y="40" width="160" height="60" class="t-model"/>
&lt;text x="460" y="64" text-anchor="middle" class="tl">LLM&lt;/text>
&lt;text x="460" y="82" text-anchor="middle" class="ts">recibe texto saneado:&lt;/text>
&lt;text x="460" y="96" text-anchor="middle" class="ts">"Mi DNI es [DNI_1], ¿paga IVA?"&lt;/text>
&lt;rect x="580" y="40" width="160" height="60" class="t-trad"/>
&lt;text x="660" y="64" text-anchor="middle" class="tl">Traductor (Deanonymize)&lt;/text>
&lt;text x="660" y="82" text-anchor="middle" class="ts">restituye originales&lt;/text>
&lt;text x="660" y="96" text-anchor="middle" class="ts">desde el cuaderno&lt;/text>
&lt;rect x="680" y="40" width="120" height="60" class="t-user" transform="translate(-20 130)"/>
&lt;text x="720" y="194" text-anchor="middle" class="tl" transform="translate(-20 0)">Cliente recibe&lt;/text>
&lt;text x="720" y="212" text-anchor="middle" class="ts" transform="translate(-20 0)">respuesta con&lt;/text>
&lt;text x="720" y="226" text-anchor="middle" class="ts" transform="translate(-20 0)">"12345678X" restituido&lt;/text>
&lt;path class="tar" d="M140,70 L180,70"/>
&lt;path class="tar" d="M340,70 L380,70"/>
&lt;path class="tar" d="M540,70 L580,70"/>
&lt;path class="tar" d="M660,100 Q660,150 700,170"/>
&lt;rect x="280" y="220" width="220" height="80" class="t-vault"/>
&lt;text x="390" y="244" text-anchor="middle" class="tl">Vault (cuaderno compartido)&lt;/text>
&lt;text x="390" y="262" text-anchor="middle" class="ts">[PERSON_1] = "Marta García"&lt;/text>
&lt;text x="390" y="276" text-anchor="middle" class="ts">[DNI_1] = "12345678X"&lt;/text>
&lt;text x="390" y="290" text-anchor="middle" class="ts">[IBAN_1] = "ES91 2100 0418..."&lt;/text>
&lt;path class="tcb" d="M260,100 L320,218"/>
&lt;path class="tcb" d="M460,218 L660,100"/>
&lt;text x="200" y="160" class="tn">guarda mapping&lt;/text>
&lt;text x="500" y="160" class="tn">consulta para restituir&lt;/text>
&lt;text x="410" y="340" text-anchor="middle" class="tn">El LLM nunca ve la PII original. El cuaderno (Vault) es el único punto que conoce la equivalencia.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Un traductor jurado serio que trabaja con documentos sensibles —un contrato laboral, una historia clínica, una declaración fiscal— no envía el texto crudo al traductor automático que tiene en la nube. Lleva un &lt;strong>cuaderno de equivalencias&lt;/strong> abierto sobre la mesa. Cuando recibe el documento original, abre el cuaderno y va apuntando: &amp;ldquo;Marta García&amp;rdquo; → &lt;code>[PERSONA-1]&lt;/code>, &amp;ldquo;12345678X&amp;rdquo; → &lt;code>[DNI-1]&lt;/code>, &amp;ldquo;ES91 2100 0418&amp;hellip;&amp;rdquo; → &lt;code>[IBAN-1]&lt;/code>. Sustituye cada aparición en el texto por su etiqueta y pasa el texto &lt;strong>anonimizado&lt;/strong> al servicio de traducción. El servicio devuelve una traducción que sigue conteniendo las etiquetas. El traductor abre de nuevo el cuaderno, restituye cada etiqueta por su valor original, y entrega al cliente la traducción final con la PII intacta. Para el servicio de traducción, esos datos personales &lt;strong>nunca existieron&lt;/strong>: sólo vio placeholders.&lt;/p>
&lt;p>Esta es la operación exacta que define el carácter de LLM Guard frente al resto del ecosistema. NeMo Guardrails resuelve safety con un &lt;strong>grafo declarativo&lt;/strong> de reglas en Colang; Guardrails AI con &lt;strong>validators&lt;/strong> que invocan a un LLM-as-judge para verificar contratos; LLM Guard con un &lt;strong>catálogo de detectores compactos especializados&lt;/strong> + el patrón Vault. Los tres son válidos en distintos escenarios. La elección no es de gusto: es estructural según cómo se construye el sistema y dónde está el cuello.&lt;/p>
&lt;p>El traductor también revisa, claro, que el texto no contenga otros problemas además de la PII: insultos, instrucciones para reprogramarse, links a páginas hostiles, código que no debería estar ahí. Para eso tiene el resto del catálogo de scanners. Pero la firma de la casa, lo que la distingue, es ese cuaderno.&lt;/p>
&lt;h2 id="anatomía-interna-de-llm-guard">Anatomía interna de LLM Guard&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Anatomía interna de LLM Guard">
&lt;style>
.a-orch{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.a-in{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:6}
.a-out{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:6}
.a-vault{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:6}
.a-obs{fill:#f8a8d8;stroke:#444;stroke-width:1.4;rx:6}
.al{font:600 12px sans-serif;fill:#222}
.as{font:400 10px sans-serif;fill:#444}
.an{font:italic 10px sans-serif;fill:#555}
.aar{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#ma1)}
&lt;/style>
&lt;defs>&lt;marker id="ma1" 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;rect x="20" y="20" width="780" height="40" class="a-orch"/>
&lt;text x="410" y="42" text-anchor="middle" class="al">Orquestador: scan_prompt() / scan_output() · fail_fast · caché TTL · timeout · OTel spans&lt;/text>
&lt;text x="410" y="56" text-anchor="middle" class="as">Itera scanners en orden, agrega is_valid y risk_score, emite trace y métricas Prometheus&lt;/text>
&lt;text x="50" y="90" class="al">Input scanners (15)&lt;/text>
&lt;rect x="30" y="100" width="120" height="22" class="a-in"/>&lt;text x="90" y="115" text-anchor="middle" class="as">Anonymize ⓥ&lt;/text>
&lt;rect x="30" y="125" width="120" height="22" class="a-in"/>&lt;text x="90" y="140" text-anchor="middle" class="as">PromptInjection&lt;/text>
&lt;rect x="30" y="150" width="120" height="22" class="a-in"/>&lt;text x="90" y="165" text-anchor="middle" class="as">Toxicity&lt;/text>
&lt;rect x="30" y="175" width="120" height="22" class="a-in"/>&lt;text x="90" y="190" text-anchor="middle" class="as">Secrets&lt;/text>
&lt;rect x="30" y="200" width="120" height="22" class="a-in"/>&lt;text x="90" y="215" text-anchor="middle" class="as">TokenLimit&lt;/text>
&lt;rect x="30" y="225" width="120" height="22" class="a-in"/>&lt;text x="90" y="240" text-anchor="middle" class="as">BanTopics&lt;/text>
&lt;rect x="30" y="250" width="120" height="22" class="a-in"/>&lt;text x="90" y="265" text-anchor="middle" class="as">BanCompetitors&lt;/text>
&lt;rect x="30" y="275" width="120" height="22" class="a-in"/>&lt;text x="90" y="290" text-anchor="middle" class="as">BanCode / Code&lt;/text>
&lt;rect x="30" y="300" width="120" height="22" class="a-in"/>&lt;text x="90" y="315" text-anchor="middle" class="as">Sentiment&lt;/text>
&lt;rect x="30" y="325" width="120" height="22" class="a-in"/>&lt;text x="90" y="340" text-anchor="middle" class="as">Gibberish&lt;/text>
&lt;rect x="30" y="350" width="120" height="22" class="a-in"/>&lt;text x="90" y="365" text-anchor="middle" class="as">Language&lt;/text>
&lt;rect x="30" y="375" width="120" height="22" class="a-in"/>&lt;text x="90" y="390" text-anchor="middle" class="as">InvisibleText&lt;/text>
&lt;rect x="30" y="400" width="120" height="22" class="a-in"/>&lt;text x="90" y="415" text-anchor="middle" class="as">Regex · BanSubstrings&lt;/text>
&lt;rect x="180" y="120" width="160" height="170" class="a-vault"/>
&lt;text x="260" y="142" text-anchor="middle" class="al">Vault&lt;/text>
&lt;text x="260" y="160" text-anchor="middle" class="as">Diccionario in-memory&lt;/text>
&lt;text x="260" y="174" text-anchor="middle" class="as">por sesión / request&lt;/text>
&lt;text x="260" y="200" text-anchor="middle" class="as">[PERSON_1]→"Marta García"&lt;/text>
&lt;text x="260" y="214" text-anchor="middle" class="as">[DNI_1]→"12345678X"&lt;/text>
&lt;text x="260" y="228" text-anchor="middle" class="as">[IBAN_1]→"ES91..."&lt;/text>
&lt;text x="260" y="252" text-anchor="middle" class="as">.placeholder() / .get()&lt;/text>
&lt;text x="260" y="266" text-anchor="middle" class="as">opcional: persistencia&lt;/text>
&lt;text x="260" y="280" text-anchor="middle" class="as">Redis / cliente sticky&lt;/text>
&lt;path class="aar" d="M150,110 L186,140"/>
&lt;text x="360" y="90" class="al">Output scanners (21)&lt;/text>
&lt;rect x="350" y="100" width="120" height="22" class="a-out"/>&lt;text x="410" y="115" text-anchor="middle" class="as">Deanonymize ⓥ&lt;/text>
&lt;rect x="350" y="125" width="120" height="22" class="a-out"/>&lt;text x="410" y="140" text-anchor="middle" class="as">Sensitive (PII out)&lt;/text>
&lt;rect x="350" y="150" width="120" height="22" class="a-out"/>&lt;text x="410" y="165" text-anchor="middle" class="as">Toxicity · Bias&lt;/text>
&lt;rect x="350" y="175" width="120" height="22" class="a-out"/>&lt;text x="410" y="190" text-anchor="middle" class="as">NoRefusal&lt;/text>
&lt;rect x="350" y="200" width="120" height="22" class="a-out"/>&lt;text x="410" y="215" text-anchor="middle" class="as">Relevance&lt;/text>
&lt;rect x="350" y="225" width="120" height="22" class="a-out"/>&lt;text x="410" y="240" text-anchor="middle" class="as">FactualConsistency&lt;/text>
&lt;rect x="350" y="250" width="120" height="22" class="a-out"/>&lt;text x="410" y="265" text-anchor="middle" class="as">JSON validator&lt;/text>
&lt;rect x="350" y="275" width="120" height="22" class="a-out"/>&lt;text x="410" y="290" text-anchor="middle" class="as">MaliciousURLs&lt;/text>
&lt;rect x="350" y="300" width="120" height="22" class="a-out"/>&lt;text x="410" y="315" text-anchor="middle" class="as">URLReachability&lt;/text>
&lt;rect x="350" y="325" width="120" height="22" class="a-out"/>&lt;text x="410" y="340" text-anchor="middle" class="as">LanguageSame&lt;/text>
&lt;rect x="350" y="350" width="120" height="22" class="a-out"/>&lt;text x="410" y="365" text-anchor="middle" class="as">ReadingTime&lt;/text>
&lt;rect x="350" y="375" width="120" height="22" class="a-out"/>&lt;text x="410" y="390" text-anchor="middle" class="as">BanCompetitors&lt;/text>
&lt;rect x="350" y="400" width="120" height="22" class="a-out"/>&lt;text x="410" y="415" text-anchor="middle" class="as">Regex · BanSubstrings&lt;/text>
&lt;path class="aar" d="M340,140 L350,140"/>
&lt;text x="540" y="90" class="al">Modelos backend&lt;/text>
&lt;rect x="510" y="100" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="120" text-anchor="middle" class="al">ONNX runtime&lt;/text>
&lt;text x="590" y="136" text-anchor="middle" class="as">modelos cuantizados&lt;/text>
&lt;text x="590" y="152" text-anchor="middle" class="as">CPU + GPU compatibles&lt;/text>
&lt;rect x="510" y="170" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="190" text-anchor="middle" class="al">Transformers (HF)&lt;/text>
&lt;text x="590" y="206" text-anchor="middle" class="as">BERT NER, distilbert&lt;/text>
&lt;text x="590" y="222" text-anchor="middle" class="as">deberta, bge, etc.&lt;/text>
&lt;rect x="510" y="240" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="260" text-anchor="middle" class="al">Presidio Analyzer&lt;/text>
&lt;text x="590" y="276" text-anchor="middle" class="as">spaCy / flair / regex&lt;/text>
&lt;text x="590" y="292" text-anchor="middle" class="as">~50 entidades base&lt;/text>
&lt;rect x="510" y="310" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="330" text-anchor="middle" class="al">Validators puros&lt;/text>
&lt;text x="590" y="346" text-anchor="middle" class="as">regex, JSON schema,&lt;/text>
&lt;text x="590" y="362" text-anchor="middle" class="as">stdlib URL parsing&lt;/text>
&lt;text x="700" y="90" class="al">Telemetría&lt;/text>
&lt;rect x="690" y="100" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="120" text-anchor="middle" class="al">OTel exporter&lt;/text>
&lt;text x="747" y="136" text-anchor="middle" class="as">traces (HTTP)&lt;/text>
&lt;text x="747" y="152" text-anchor="middle" class="as">metrics (HTTP)&lt;/text>
&lt;rect x="690" y="170" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="190" text-anchor="middle" class="al">Prometheus&lt;/text>
&lt;text x="747" y="206" text-anchor="middle" class="as">/metrics endpoint&lt;/text>
&lt;text x="747" y="222" text-anchor="middle" class="as">counters + histograms&lt;/text>
&lt;rect x="690" y="240" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="260" text-anchor="middle" class="al">structured logs&lt;/text>
&lt;text x="747" y="276" text-anchor="middle" class="as">stdout JSON,&lt;/text>
&lt;text x="747" y="292" text-anchor="middle" class="as">parseable Loki/ELK&lt;/text>
&lt;rect x="690" y="310" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="330" text-anchor="middle" class="al">FastAPI&lt;/text>
&lt;text x="747" y="346" text-anchor="middle" class="as">/analyze/prompt&lt;/text>
&lt;text x="747" y="362" text-anchor="middle" class="as">/analyze/output&lt;/text>
&lt;text x="410" y="445" text-anchor="middle" class="an">El Vault es la pieza única: lo comparten Anonymize (input) y Deanonymize (output) en la misma request o sesión. Sin él, la PII se filtraría al LLM.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las tres piezas estructurales son:&lt;/p>
&lt;p>&lt;strong>1. El orquestador&lt;/strong> (&lt;code>scan_prompt&lt;/code>, &lt;code>scan_output&lt;/code>). Recibe una lista de scanners en orden y los ejecuta secuencialmente sobre el texto. Devuelve la terna &lt;code>(sanitized_text, results_valid, results_score)&lt;/code> donde:&lt;/p>
&lt;ul>
&lt;li>&lt;code>sanitized_text&lt;/code> es el texto transformado por los scanners que mutan (Anonymize, BanSubstrings con redaction).&lt;/li>
&lt;li>&lt;code>results_valid&lt;/code> es un dict &lt;code>{scanner_name: bool}&lt;/code> indicando qué scanners pasaron.&lt;/li>
&lt;li>&lt;code>results_score&lt;/code> es un dict &lt;code>{scanner_name: float}&lt;/code> con el risk score reportado (0 limpio, 1 violación máxima).&lt;/li>
&lt;/ul>
&lt;p>Soporta &lt;code>fail_fast=True&lt;/code> para cortar tras el primer fail. Soporta &lt;code>timeout&lt;/code> por scanner para no bloquearse en un detector lento. Cuando se expone como API FastAPI, soporta caché TTL para evitar reescanear prompts repetidos (caso de bots con preguntas idénticas).&lt;/p>
&lt;p>&lt;strong>2. El catálogo de scanners.&lt;/strong> Quince input scanners y veintiún output scanners, cada uno con su propio modelo backend y su umbral configurable:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Familia&lt;/th>
&lt;th>Input&lt;/th>
&lt;th>Output&lt;/th>
&lt;th>Backend dominante&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>PII&lt;/strong>&lt;/td>
&lt;td>Anonymize&lt;/td>
&lt;td>Deanonymize, Sensitive&lt;/td>
&lt;td>Presidio + BERT-NER&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Inyección y jailbreak&lt;/strong>&lt;/td>
&lt;td>PromptInjection&lt;/td>
&lt;td>—&lt;/td>
&lt;td>DeBERTa fine-tuned (Protect AI propio)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Toxicidad y bias&lt;/strong>&lt;/td>
&lt;td>Toxicity, Sentiment&lt;/td>
&lt;td>Toxicity, Bias, Sentiment&lt;/td>
&lt;td>RoBERTa / BERT fine-tuned&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tópicos prohibidos&lt;/strong>&lt;/td>
&lt;td>BanTopics, BanCompetitors&lt;/td>
&lt;td>BanTopics, BanCompetitors&lt;/td>
&lt;td>Zero-shot classifier BART-MNLI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Substrings y regex&lt;/strong>&lt;/td>
&lt;td>BanSubstrings, Regex&lt;/td>
&lt;td>BanSubstrings, Regex&lt;/td>
&lt;td>string matching + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secrets&lt;/strong>&lt;/td>
&lt;td>Secrets&lt;/td>
&lt;td>—&lt;/td>
&lt;td>detect-secrets (Yelp) + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Estructura&lt;/strong>&lt;/td>
&lt;td>TokenLimit, Language, InvisibleText, Gibberish&lt;/td>
&lt;td>JSON, Language, LanguageSame, Gibberish, ReadingTime&lt;/td>
&lt;td>tokenizer, lang-detect, JSON schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Código&lt;/strong>&lt;/td>
&lt;td>BanCode, Code&lt;/td>
&lt;td>BanCode, Code&lt;/td>
&lt;td>classifier de lenguaje + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>URLs&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>MaliciousURLs, URLReachability&lt;/td>
&lt;td>block-list + DNS lookup&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Calidad de respuesta&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>NoRefusal, Relevance, FactualConsistency&lt;/td>
&lt;td>NLI-cross-encoder + cosine similarity&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cada scanner se importa y se instancia individualmente, con su umbral propio:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.input_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Anonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Secrets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.vault&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">vault&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Vault&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">scanners&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Anonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.5&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.85&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.7&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Secrets&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. El Vault.&lt;/strong> Pieza única no encontrada en NeMo Guardrails ni en Guardrails AI con el mismo modelo. Es un diccionario in-memory por sesión o request que guarda el mapping &lt;code>placeholder → valor_original&lt;/code>. Lo escribe el scanner &lt;code>Anonymize&lt;/code> en input y lo lee el scanner &lt;code>Deanonymize&lt;/code> en output. Si el Vault es compartido entre múltiples requests del mismo usuario, el mapping persiste (útil para conversaciones multi-turno). Si es por request, se descarta tras la respuesta.&lt;/p>
&lt;p>El Vault básico es &lt;code>dict&lt;/code> Python; para entornos distribuidos con múltiples pods, se sustituye por un Redis sticky (mismo usuario → mismo pod) o por un Vault custom que lea/escriba a un Redis externo, descartado tras un TTL. Esto es operacional, no de la librería core.&lt;/p>
&lt;h2 id="el-flujo-anonymize--llm--deanonymize-en-detalle">El flujo Anonymize → LLM → Deanonymize en detalle&lt;/h2>
&lt;p>El patrón canónico de uso de LLM Guard se descompone en seis pasos exactos:&lt;/p>
&lt;pre tabindex="0">&lt;code>1. Recibir prompt del usuario:
&amp;#34;Mi nombre es Marta García y mi IBAN es ES9121000418450200051332,
¿podéis revisar el cargo del 14 de marzo?&amp;#34;
2. scan_prompt() con [Anonymize(vault), PromptInjection(), Toxicity()]
→ Anonymize redacta entidades y las guarda en vault:
vault[&amp;#34;[REDACTED_PERSON_1]&amp;#34;] = &amp;#34;Marta García&amp;#34;
vault[&amp;#34;[REDACTED_IBAN_1]&amp;#34;] = &amp;#34;ES9121000418450200051332&amp;#34;
→ PromptInjection comprueba que no haya jailbreak (no lo hay)
→ Toxicity comprueba que no haya insultos (no los hay)
→ results_valid = {Anonymize: True, PromptInjection: True, Toxicity: True}
→ sanitized_prompt:
&amp;#34;Mi nombre es [REDACTED_PERSON_1] y mi IBAN es [REDACTED_IBAN_1],
¿podéis revisar el cargo del 14 de marzo?&amp;#34;
3. Llamar al LLM con sanitized_prompt:
→ vLLM recibe el prompt sin PII real
→ genera respuesta:
&amp;#34;Sí, [REDACTED_PERSON_1], voy a revisar el cargo en la cuenta
[REDACTED_IBAN_1]. ¿Puedes confirmar el importe?&amp;#34;
4. scan_output() con [Deanonymize(vault), Toxicity(), Relevance(), Sensitive()]
→ Deanonymize sustituye placeholders por valores del vault:
[REDACTED_PERSON_1] → &amp;#34;Marta García&amp;#34;
[REDACTED_IBAN_1] → &amp;#34;ES9121000418450200051332&amp;#34;
→ Toxicity comprueba que la respuesta no sea ofensiva
→ Relevance comprueba que responde al prompt
→ Sensitive comprueba que no aparezca PII no autorizada
(en este caso, la PII restituida está autorizada porque la trajo
el propio usuario y la firma el Vault → la regla aplica solo a
PII nueva inventada por el LLM)
→ sanitized_response:
&amp;#34;Sí, Marta García, voy a revisar el cargo en la cuenta
ES9121000418450200051332. ¿Puedes confirmar el importe?&amp;#34;
5. Devolver al usuario sanitized_response.
6. Si la sesión sigue, el vault persiste y los próximos turnos reutilizan
los mismos placeholders. Cuando termina la sesión, el vault se descarta.
&lt;/code>&lt;/pre>&lt;p>Tres detalles que importan operativamente:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Las entidades persistentes&lt;/strong> (&lt;code>[REDACTED_PERSON_1]&lt;/code> para &amp;ldquo;Marta García&amp;rdquo;) se mantienen constantes durante la sesión. Si el usuario menciona otra persona (&amp;ldquo;hablé con Juan Pérez&amp;rdquo;), Anonymize asignará &lt;code>[REDACTED_PERSON_2]&lt;/code>. La coherencia inter-turno la asegura el Vault.&lt;/li>
&lt;li>&lt;strong>El LLM nunca ve los datos originales&lt;/strong> durante la sesión. Esto es la propiedad clave para casos donde el LLM se sirve desde un modelo en cloud o cuando se loguea el prompt (Langfuse, OTel) sin acceso confidencial.&lt;/li>
&lt;li>&lt;strong>El logging de LLM Guard registra los placeholders&lt;/strong>, no los valores originales. Para auditoría con valores originales hace falta una capa adicional (acceso al Vault con permisos privilegiados) — esto es por diseño, no por defecto.&lt;/li>
&lt;/ul>
&lt;h2 id="cuatro-modos-de-despliegue">Cuatro modos de despliegue&lt;/h2>
&lt;h3 id="modo-1--librería-python-in-process">Modo 1 — Librería Python in-process&lt;/h3>
&lt;p>El más simple: &lt;code>pip install llm-guard&lt;/code>, importar los scanners en el código de la aplicación, llamar a &lt;code>scan_prompt&lt;/code>/&lt;code>scan_output&lt;/code> directamente. Los modelos se cargan en el proceso. La ventaja es latencia mínima; la desventaja es que cada réplica de la aplicación carga sus propios modelos en memoria.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># en el servidor de la app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">scan_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">scan_output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.input_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Anonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.output_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Deanonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">OutToxicity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Relevance&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.vault&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">vault&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Vault&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">input_scanners&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">Anonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">()]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output_scanners&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">Deanonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">OutToxicity&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">Relevance&lt;/span>&lt;span class="p">()]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># en el handler de la request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sanitized_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">valid_in&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">score_in&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scan_prompt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">input_scanners&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">user_prompt&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="nb">all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">valid_in&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">()):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">error_response&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score_in&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">vllm_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">complete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sanitized_prompt&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sanitized_resp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">valid_out&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">score_out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scan_output&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output_scanners&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sanitized_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">response&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="nb">all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">valid_out&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">()):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">error_response&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score_out&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">return&lt;/span> &lt;span class="n">sanitized_resp&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Encaja con el &lt;strong>patrón A (sidecar)&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post de guardrails&lt;/a> cuando la app y el sidecar comparten proceso. Y con el &lt;strong>patrón C (in-process)&lt;/strong> si la app es directamente la capa de inferencia.&lt;/p>
&lt;h3 id="modo-2--api-fastapi-propia">Modo 2 — API FastAPI propia&lt;/h3>
&lt;p>El proyecto incluye un servidor FastAPI listo (&lt;code>llm-guard-api&lt;/code>) que expone los scanners detrás de dos endpoints REST:&lt;/p>
&lt;pre tabindex="0">&lt;code>POST /analyze/prompt
body: {&amp;#34;prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;scanners&amp;#34;: [...] (opcional)}
response: {&amp;#34;sanitized_prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;is_valid&amp;#34;: bool, &amp;#34;scanners&amp;#34;: {scanner: {is_valid, risk_score}}}
POST /analyze/output
body: {&amp;#34;prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;scanners&amp;#34;: [...]}
response: análoga
&lt;/code>&lt;/pre>&lt;p>Configuración por &lt;code>config/scanners.yml&lt;/code> con variables de entorno (&lt;code>SCAN_FAIL_FAST&lt;/code>, &lt;code>CACHE_MAX_SIZE&lt;/code>, &lt;code>CACHE_TTL&lt;/code>, &lt;code>SCAN_PROMPT_TIMEOUT&lt;/code>&amp;hellip;). Lleva métricas Prometheus en &lt;code>/metrics&lt;/code> y traces OTel HTTP exporter por defecto.&lt;/p>
&lt;p>Encaja con el &lt;strong>patrón B (servicio centralizado tras AI Gateway)&lt;/strong> del post de guardrails.&lt;/p>
&lt;h3 id="modo-3--sidecar-otel-sobre-el-pod-del-motor-de-inferencia">Modo 3 — Sidecar OTel sobre el pod del motor de inferencia&lt;/h3>
&lt;p>Para deployments de vLLM en Kubernetes, una variante del modo 2 es desplegar la API de LLM Guard como &lt;strong>sidecar container&lt;/strong> en el mismo pod del vLLM, hablando por localhost. El AI Gateway delante invoca al sidecar antes y después de la inferencia. El OTel collector del nodo agrega los spans de vLLM con los spans &lt;code>gen_ai.guardrail.*&lt;/code> de LLM Guard automáticamente porque comparten &lt;code>trace_id&lt;/code> propagado por baggage HTTP.&lt;/p>
&lt;p>Esto encaja con el &lt;strong>patrón A (sidecar)&lt;/strong> del post de guardrails, pero con la disciplina de la API REST para no acoplar lenguaje (el AI Gateway puede ser Envoy en C++, LLM Guard en Python).&lt;/p>
&lt;h3 id="modo-4--plugin-dentro-de-un-ai-gateway">Modo 4 — Plugin dentro de un AI Gateway&lt;/h3>
&lt;p>Tres AI Gateways soportan LLM Guard como plugin nativo en 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LiteLLM Proxy&lt;/strong> (MIT, BerriAI) — plugin &lt;code>llm_guard&lt;/code> activable en config con &lt;code>guardrails: [&amp;quot;llm_guard&amp;quot;]&lt;/code>. Llama internamente a la API.&lt;/li>
&lt;li>&lt;strong>Envoy AI Gateway&lt;/strong> (CNCF, Apache 2.0) — filtro &lt;code>ai-guardrails&lt;/code> con backend pluggable apuntando al servicio LLM Guard.&lt;/li>
&lt;li>&lt;strong>Kong AI Gateway&lt;/strong> (Apache 2.0) — plugin &lt;code>ai-proxy&lt;/code> con post-procesador que invoca LLM Guard.&lt;/li>
&lt;/ul>
&lt;p>En los tres casos, el AI Gateway es el punto único de entrada de la app cliente al LLM; el gateway llama a LLM Guard antes/después de pasar al motor de inferencia. Ventaja: lock-in cero en el código de la aplicación; cambiar de LLM Guard a NeMo Guardrails es cambiar el plugin del gateway, no reescribir la app. Desventaja: el hop adicional añade latencia (típicamente 5-15 ms intra-cluster).&lt;/p>
&lt;h2 id="integración-gráfica-con-langfuse-vllm-y-el-stack-otel">Integración gráfica con Langfuse, vLLM y el stack OTel&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Integración de LLM Guard con Langfuse, vLLM y el stack OTel">
&lt;style>
.b-app{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.b-gw{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:8}
.b-lg{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:8}
.b-vllm{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.b-otel{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:8}
.b-langfuse{fill:#f8a8d8;stroke:#444;stroke-width:1.4;rx:8}
.b-storage{fill:#f0e8c0;stroke:#444;stroke-width:1.4;rx:8}
.bl{font:600 13px sans-serif;fill:#222}
.bs{font:400 11px sans-serif;fill:#444}
.bn{font:italic 10px sans-serif;fill:#555}
.bar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mb1)}
.bart{stroke:#5a5;stroke-width:1.4;fill:none;stroke-dasharray:5 3;marker-end:url(#mb2)}
&lt;/style>
&lt;defs>
&lt;marker id="mb1" 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;marker id="mb2" 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="#5a5"/>&lt;/marker>
&lt;/defs>
&lt;rect x="20" y="40" width="140" height="50" class="b-app"/>
&lt;text x="90" y="60" text-anchor="middle" class="bl">App cliente&lt;/text>
&lt;text x="90" y="78" text-anchor="middle" class="bs">chatbot · backend · agente&lt;/text>
&lt;rect x="200" y="40" width="180" height="50" class="b-gw"/>
&lt;text x="290" y="60" text-anchor="middle" class="bl">AI Gateway&lt;/text>
&lt;text x="290" y="78" text-anchor="middle" class="bs">LiteLLM · Envoy AI · Kong AI&lt;/text>
&lt;rect x="430" y="20" width="160" height="40" class="b-lg"/>
&lt;text x="510" y="38" text-anchor="middle" class="bl">LLM Guard API&lt;/text>
&lt;text x="510" y="54" text-anchor="middle" class="bs">scan_prompt + scan_output&lt;/text>
&lt;rect x="430" y="70" width="160" height="40" class="b-vllm"/>
&lt;text x="510" y="88" text-anchor="middle" class="bl">vLLM&lt;/text>
&lt;text x="510" y="104" text-anchor="middle" class="bs">motor inferencia + adapter&lt;/text>
&lt;rect x="640" y="40" width="160" height="50" class="b-storage"/>
&lt;text x="720" y="60" text-anchor="middle" class="bl">Vault Redis&lt;/text>
&lt;text x="720" y="78" text-anchor="middle" class="bs">mapping placeholder→PII&lt;/text>
&lt;path class="bar" d="M160,65 L200,65"/>
&lt;path class="bar" d="M380,55 L430,40"/>
&lt;path class="bar" d="M380,75 L430,90"/>
&lt;path class="bar" d="M510,60 L640,65"/>
&lt;text x="170" y="55" class="bn">1: prompt&lt;/text>
&lt;text x="390" y="35" class="bn">2: pre-scan&lt;/text>
&lt;text x="390" y="105" class="bn">3: inferencia&lt;/text>
&lt;text x="555" y="55" class="bn">vault R/W&lt;/text>
&lt;rect x="20" y="180" width="240" height="80" class="b-otel"/>
&lt;text x="140" y="202" text-anchor="middle" class="bl">OTel Collector (DaemonSet)&lt;/text>
&lt;text x="140" y="220" text-anchor="middle" class="bs">recibe spans gen_ai.* y&lt;/text>
&lt;text x="140" y="234" text-anchor="middle" class="bs">gen_ai.guardrail.* desde:&lt;/text>
&lt;text x="140" y="250" text-anchor="middle" class="bs">vLLM, LLM Guard, AI Gateway&lt;/text>
&lt;path class="bart" d="M510,110 Q260,140 140,178"/>
&lt;path class="bart" d="M510,60 Q330,140 200,178"/>
&lt;path class="bart" d="M290,90 Q230,140 140,178"/>
&lt;text x="320" y="135" class="bn">spans OTel HTTP&lt;/text>
&lt;rect x="300" y="180" width="200" height="80" class="b-langfuse"/>
&lt;text x="400" y="202" text-anchor="middle" class="bl">Langfuse&lt;/text>
&lt;text x="400" y="220" text-anchor="middle" class="bs">/api/public/otel ingestion&lt;/text>
&lt;text x="400" y="236" text-anchor="middle" class="bs">+ /api/public/scores&lt;/text>
&lt;text x="400" y="252" text-anchor="middle" class="bs">+ datasets + sessions&lt;/text>
&lt;path class="bar" d="M260,220 L300,220"/>
&lt;text x="270" y="215" class="bn">OTLP&lt;/text>
&lt;rect x="540" y="180" width="120" height="40" class="b-otel"/>
&lt;text x="600" y="200" text-anchor="middle" class="bl">Tempo / Jaeger&lt;/text>
&lt;text x="600" y="216" text-anchor="middle" class="bs">trace storage&lt;/text>
&lt;rect x="540" y="225" width="120" height="40" class="b-otel"/>
&lt;text x="600" y="245" text-anchor="middle" class="bl">VictoriaMetrics&lt;/text>
&lt;text x="600" y="261" text-anchor="middle" class="bs">métricas Prom&lt;/text>
&lt;path class="bar" d="M260,210 L540,200"/>
&lt;path class="bar" d="M260,235 L540,240"/>
&lt;rect x="700" y="180" width="100" height="80" class="b-storage"/>
&lt;text x="750" y="202" text-anchor="middle" class="bl">Grafana&lt;/text>
&lt;text x="750" y="220" text-anchor="middle" class="bs">datasource&lt;/text>
&lt;text x="750" y="234" text-anchor="middle" class="bs">Tempo + VM&lt;/text>
&lt;text x="750" y="252" text-anchor="middle" class="bs">+ Langfuse&lt;/text>
&lt;path class="bar" d="M660,220 L700,220"/>
&lt;rect x="20" y="320" width="780" height="50" class="b-gw"/>
&lt;text x="410" y="340" text-anchor="middle" class="bl">Plano scoring de Langfuse: el AI Gateway postea langfuse.score(trace_id, name="guardrail.PromptInjection", value=risk_score)&lt;/text>
&lt;text x="410" y="356" text-anchor="middle" class="bs">por cada scanner ejecutado; eso permite a Langfuse construir dashboards de "% bloqueos por categoría" y series temporales&lt;/text>
&lt;path class="bar" d="M290,90 Q290,290 400,320"/>
&lt;text x="305" y="200" class="bn">scores HTTP&lt;/text>
&lt;text x="410" y="400" text-anchor="middle" class="bn">Tres planos de telemetría se mezclan: traces (OTel → Tempo + Langfuse), métricas (Prometheus → VictoriaMetrics), scores (Langfuse SDK).&lt;/text>
&lt;text x="410" y="418" text-anchor="middle" class="bn">Grafana los une por trace_id; Langfuse los une por session_id + trace_id propagado.&lt;/text>
&lt;text x="410" y="438" text-anchor="middle" class="bn">El Vault Redis tiene su propio plano de datos y NO se exporta a observabilidad — la PII original nunca sale de él.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las &lt;strong>tres rutas de integración con Langfuse&lt;/strong> que importan operativamente:&lt;/p>
&lt;p>&lt;strong>Ruta A — OTel HTTP exporter de LLM Guard.&lt;/strong> LLM Guard tiene exporter OTel HTTP nativo. Configurando &lt;code>OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://langfuse.cluster/api/public/otel&lt;/code>, los spans &lt;code>gen_ai.guardrail.*&lt;/code> que emite cada scanner llegan directamente a Langfuse y aparecen como spans hijos del span LLM principal (siempre que el &lt;code>trace_id&lt;/code> se propague vía baggage HTTP desde el AI Gateway). Esta es la ruta canónica en 2026.&lt;/p>
&lt;p>&lt;strong>Ruta B — Langfuse scoring API desde el AI Gateway.&lt;/strong> El AI Gateway (LiteLLM, Envoy AI, Kong AI), al recibir la respuesta de LLM Guard con los &lt;code>risk_score&lt;/code> por scanner, emite una llamada &lt;code>langfuse.score(trace_id, name=&amp;quot;guardrail.PromptInjection&amp;quot;, value=0.87, comment=&amp;quot;blocked&amp;quot;)&lt;/code> por cada scanner. En Langfuse aparece como scores enganchados al mismo trace que la inferencia. Permite dashboards &amp;ldquo;bloqueos por categoría&amp;rdquo; y series temporales por scanner. Es &lt;strong>complementaria&lt;/strong> a la ruta A: la A trae los spans, la B trae el score numérico fácil de agregar en SQL.&lt;/p>
&lt;p>&lt;strong>Ruta C — Sessions de Langfuse + Vault metadata.&lt;/strong> En modo conversacional, el AI Gateway propaga &lt;code>langfuse_session_id&lt;/code> al Vault como su clave. Cuando un usuario tiene una sesión multi-turno, Langfuse muestra la traza completa de la sesión, con los placeholders que se reutilizan turno a turno. La PII original sigue sin viajar a Langfuse — sólo los placeholders y sus categorías.&lt;/p>
&lt;p>El &lt;strong>OTel Collector&lt;/strong> del nodo es el pegamento: recibe spans de vLLM (por OpenLLMetry o instrumentación nativa), de LLM Guard (por su exporter OTel) y del AI Gateway (instrumentación HTTP estándar), los &lt;strong>une por trace_id&lt;/strong>, y los envía paralelamente a Langfuse (vía OTLP HTTP) y a Tempo/Jaeger. Las métricas Prometheus de LLM Guard van a VictoriaMetrics por scraping normal. Grafana ofrece la vista unificada para investigación cross-trace; Langfuse ofrece la vista LLM-céntrica con sessions y scores. El &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">post sobre tracing OTel GenAI&lt;/a> detalla la mecánica completa del Collector.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan">Las matemáticas que importan&lt;/h2>
&lt;h3 id="latencia-por-scanner--los-números-reales">Latencia por scanner — los números reales&lt;/h3>
&lt;p>El proyecto publica benchmarks reproducibles. Para el scanner Anonymize (input length 317 chars, batch 5), los datos de referencia son:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Plataforma&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Latencia avg&lt;/th>
&lt;th>p99&lt;/th>
&lt;th>QPS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>AWS m5.xlarge (CPU)&lt;/td>
&lt;td>Transformers&lt;/td>
&lt;td>177 ms&lt;/td>
&lt;td>326 ms&lt;/td>
&lt;td>1.789&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS m5.xlarge (CPU)&lt;/td>
&lt;td>&lt;strong>ONNX runtime&lt;/strong>&lt;/td>
&lt;td>&lt;strong>128 ms&lt;/strong>&lt;/td>
&lt;td>180 ms&lt;/td>
&lt;td>2.464&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS r6a.xlarge (AMD CPU)&lt;/td>
&lt;td>Transformers&lt;/td>
&lt;td>244 ms&lt;/td>
&lt;td>284 ms&lt;/td>
&lt;td>1.298&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS g5.xlarge (NVIDIA A10G)&lt;/td>
&lt;td>Transformers FP16&lt;/td>
&lt;td>125 ms&lt;/td>
&lt;td>498 ms&lt;/td>
&lt;td>2.532&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS g5.xlarge (A10G)&lt;/td>
&lt;td>&lt;strong>ONNX + GPU&lt;/strong>&lt;/td>
&lt;td>&lt;strong>38 ms&lt;/strong>&lt;/td>
&lt;td>99 ms&lt;/td>
&lt;td>8.317&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tres observaciones operativas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ONNX siempre gana.&lt;/strong> Incluso en CPU, ONNX baja el avg de 177 a 128 ms (factor 1,4×). En GPU con ONNX, baja de 177 a 38 ms (factor 4,6×). La regla práctica: &lt;strong>siempre exportar el modelo del scanner a ONNX antes de producción&lt;/strong>. La preview del SaaS oficial lo usa por defecto.&lt;/li>
&lt;li>&lt;strong>GPU sin ONNX no rinde tanto como uno espera.&lt;/strong> Una A10G sin ONNX (125 ms) es comparable a m5.xlarge con ONNX (128 ms). La GPU sola no compensa si el grafo de inferencia no está optimizado. El binomio relevante es ONNX + GPU.&lt;/li>
&lt;li>&lt;strong>La latencia p99 sin ONNX explota.&lt;/strong> En GPU sin ONNX, el p99 de 498 ms triplica el avg de 125 ms — colas y batching producen tail latencies altas. Con ONNX, el ratio p99/avg cae a 2,6× (99/38), mucho más predecible.&lt;/li>
&lt;/ol>
&lt;p>Para una capa de guardrails con cinco scanners ejecutados secuencialmente (Anonymize, PromptInjection, Toxicity, Secrets, BanTopics), la suma del p99 es lo que determina el budget de la línea 1 (input). Cinco scanners a ~100 ms p99 cada uno = 500 ms p99 acumulado — fuera de presupuesto para chat interactivo. Con ONNX bajamos a ~50 ms cada uno = 250 ms p99 — manejable. &lt;strong>Con &lt;code>fail_fast=True&lt;/code>&lt;/strong>, el tiempo esperado es menor (el más probable es que pasen los más baratos y fallen los caros sólo si se ejecutan).&lt;/p>
&lt;p>Para un cálculo más fino, la latencia esperada del pipeline con &lt;code>fail_fast&lt;/code> es:&lt;/p>
&lt;p>[
\mathbb{E}[L] = \sum_{i=1}^{N} L_i \cdot \prod_{j=1}^{i-1} p_j
]&lt;/p>
&lt;p>donde (L_i) es la latencia del scanner (i) y (p_j) la probabilidad de que el scanner (j) devuelva válido. En tráfico bien comportado (la mayoría de prompts pasan todos los scanners), (\prod p_j \approx 1) y la fórmula colapsa a la suma directa. En tráfico adversarial, los scanners más rápidos al principio del pipeline cortan antes y la latencia esperada baja drásticamente.&lt;/p>
&lt;h3 id="coste-computacional-por-scanner">Coste computacional por scanner&lt;/h3>
&lt;p>El tamaño del modelo backend determina el coste y la posibilidad de correr en CPU vs requerir GPU:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Modelo backend típico&lt;/th>
&lt;th>Parámetros&lt;/th>
&lt;th>VRAM FP16 / ONNX-INT8&lt;/th>
&lt;th>CPU viable&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Anonymize (BERT-NER)&lt;/td>
&lt;td>dslim/bert-base-NER&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí (con ONNX)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anonymize (BERT-large)&lt;/td>
&lt;td>dslim/bert-large-NER&lt;/td>
&lt;td>335 M&lt;/td>
&lt;td>670 MB / 170 MB&lt;/td>
&lt;td>Sí pero lento (~500 ms CPU)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PromptInjection&lt;/td>
&lt;td>DeBERTa-v3-base fine-tuned&lt;/td>
&lt;td>184 M&lt;/td>
&lt;td>370 MB / 90 MB&lt;/td>
&lt;td>Sí (con ONNX)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Toxicity&lt;/td>
&lt;td>unitary/toxic-bert&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sentiment&lt;/td>
&lt;td>distilbert-sst2&lt;/td>
&lt;td>67 M&lt;/td>
&lt;td>130 MB / 35 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gibberish&lt;/td>
&lt;td>small distilbert&lt;/td>
&lt;td>67 M&lt;/td>
&lt;td>130 MB / 35 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BanTopics&lt;/td>
&lt;td>BART-MNLI zero-shot&lt;/td>
&lt;td>407 M&lt;/td>
&lt;td>815 MB / 200 MB&lt;/td>
&lt;td>Lento en CPU (~400 ms)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bias (output)&lt;/td>
&lt;td>RoBERTa-bias&lt;/td>
&lt;td>125 M&lt;/td>
&lt;td>250 MB / 65 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FactualConsistency&lt;/td>
&lt;td>cross-encoder/nli-deberta&lt;/td>
&lt;td>184 M&lt;/td>
&lt;td>370 MB / 90 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Relevance&lt;/td>
&lt;td>sentence-transformers&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenLimit, Regex, JSON, BanSubstrings, Secrets&lt;/td>
&lt;td>(sin modelo)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Trivial&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Patrón razonable on-premise&lt;/strong>: scanners sin modelo (TokenLimit, Regex, BanSubstrings, Secrets) corren en CPU sin pestañear. Anonymize, PromptInjection, Toxicity, Sentiment, Relevance corren cómodamente en CPU con ONNX-INT8 con ~50-150 ms p99. BanTopics y los basados en cross-encoder grandes (FactualConsistency) son los candidatos a vivir en una GPU compartida si quieres p99 &amp;lt; 100 ms.&lt;/p>
&lt;h3 id="throughput-de-la-api-en-cluster">Throughput de la API en cluster&lt;/h3>
&lt;p>Una instancia de la API FastAPI con 4 workers Uvicorn sobre un nodo con 8 vCPUs alcanza ~600-1.200 RPS sobre un pipeline típico de 5 scanners en CPU + ONNX. Para escalar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Horizontal&lt;/strong>: replicar pods detrás de un Service ClusterIP — escalado lineal porque los scanners son stateless (excepto el Vault, que es por sesión y se externaliza a Redis si se quiere sticky o compartido).&lt;/li>
&lt;li>&lt;strong>Vertical con GPU&lt;/strong>: 1 H100 sirve ~5.000-10.000 RPS con todos los scanners en ONNX-GPU. Es overkill para la mayoría de deployments excepto en multi-tenant con miles de QPS sostenidos.&lt;/li>
&lt;/ul>
&lt;p>La regla práctica del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post sobre guardrails&lt;/a> (1 GPU guardrails por 4-8 GPUs LLM) se mantiene aquí: con cluster 4×H100 SXM sirviendo Llama 70B en TP=4, una L4 o RTX 4090 dedicada al servicio LLM Guard cubre la carga.&lt;/p>
&lt;h2 id="comparativa-con-nemo-guardrails-y-guardrails-ai">Comparativa con NeMo Guardrails y Guardrails AI&lt;/h2>
&lt;p>Las tres herramientas resuelven el mismo problema desde tres modelos arquitectónicos distintos. La elección entre ellas no es de calidad —las tres están maduras—, es de &lt;strong>encaje con el resto del stack&lt;/strong>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimensión&lt;/th>
&lt;th>LLM Guard&lt;/th>
&lt;th>NeMo Guardrails&lt;/th>
&lt;th>Guardrails AI&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Modelo conceptual&lt;/strong>&lt;/td>
&lt;td>Pipeline de scanners compactos&lt;/td>
&lt;td>Grafo declarativo Colang (flujo conversacional)&lt;/td>
&lt;td>Validators tipo contrato JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Detección dominante&lt;/strong>&lt;/td>
&lt;td>Modelos ML especializados (BERT, DeBERTa) por categoría&lt;/td>
&lt;td>Reglas + LLM-as-judge&lt;/td>
&lt;td>Validators heurísticos + LLM-as-judge externo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PII workflow&lt;/strong>&lt;/td>
&lt;td>Anonymize + Vault + Deanonymize&lt;/td>
&lt;td>Vía Presidio integrado, sin Vault built-in&lt;/td>
&lt;td>Validators de PII, sin restitución automática&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Licencia&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0 (+ Hub paid)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Lenguaje&lt;/strong>&lt;/td>
&lt;td>Python&lt;/td>
&lt;td>Python + Colang DSL&lt;/td>
&lt;td>Python&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Madurez API&lt;/strong>&lt;/td>
&lt;td>API FastAPI built-in, OTel built-in&lt;/td>
&lt;td>Server FastAPI built-in, OTel parcial&lt;/td>
&lt;td>API server externo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Despliegue cluster&lt;/strong>&lt;/td>
&lt;td>Lib + API + sidecar + plugin gateways&lt;/td>
&lt;td>Lib + server&lt;/td>
&lt;td>Lib + server + Hub SaaS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Latencia típica (5 scanners ONNX-GPU)&lt;/strong>&lt;/td>
&lt;td>50-200 ms&lt;/td>
&lt;td>100-500 ms (más si hay LLM judge)&lt;/td>
&lt;td>100-300 ms (depende del validator)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo brilla&lt;/strong>&lt;/td>
&lt;td>Apps con PII fuerte, multi-tenant con sesiones, requisitos GDPR/HIPAA&lt;/td>
&lt;td>Sistemas conversacionales con flujos definidos, agentes con dialog policy&lt;/td>
&lt;td>Apps con contratos JSON estrictos, structured output con validación adicional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo no encaja&lt;/strong>&lt;/td>
&lt;td>Si necesitas dialog policy declarativa&lt;/td>
&lt;td>Si quieres detectores compactos sin LLM judge&lt;/td>
&lt;td>Si quieres Vault y Deanonymize automático&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los tres son &lt;strong>complementarios en deployments grandes&lt;/strong>. Un patrón maduro en 2026 es:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>NeMo Guardrails&lt;/strong> orquesta el flujo de diálogo (qué tools puede invocar el agente, en qué orden, con qué cooldowns).&lt;/li>
&lt;li>&lt;strong>LLM Guard&lt;/strong> ocupa la línea de PII + scanners compactos en input y output, con su Vault haciendo el trabajo sucio de anonimización.&lt;/li>
&lt;li>&lt;strong>Guardrails AI&lt;/strong> valida outputs estructurados (JSON Schema, function calling) con sus validators.&lt;/li>
&lt;/ul>
&lt;p>La separación de responsabilidades evita el solapamiento y permite cambiar piezas sin reescribir todo. Las tres exponen API FastAPI y emiten spans OTel; el AI Gateway las orquesta secuencialmente.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise">Aplicado a hardware on-premise&lt;/h2>
&lt;h3 id="en-la-rtx-4090-24-gb">En la RTX 4090 (24 GB)&lt;/h3>
&lt;p>Una 4090 dedicada al pod del servicio LLM Guard sirve cómodamente el pipeline completo en producción media:&lt;/p>
&lt;ul>
&lt;li>Anonymize (BERT-NER ONNX-INT8): ~50 MB VRAM.&lt;/li>
&lt;li>PromptInjection (DeBERTa ONNX-INT8): ~90 MB.&lt;/li>
&lt;li>Toxicity, Sentiment, Gibberish: ~150 MB total.&lt;/li>
&lt;li>BanTopics (BART-MNLI ONNX-INT8): ~200 MB.&lt;/li>
&lt;li>Bias, Relevance, FactualConsistency (output): ~250 MB total.&lt;/li>
&lt;/ul>
&lt;p>Total ~750 MB. Resto de la VRAM ociosa o aprovechable para batching agresivo. Throughput sostenido a 3.000-6.000 RPS sobre el pipeline completo. Para deployments con &amp;lt; 500 RPS sostenidos, la 4090 está sub-utilizada y se puede compartir con otra carga (embeddings de RAG, reranker BGE).&lt;/p>
&lt;h3 id="en-el-cluster-4h100-sxm-320-gb-total-nvlink">En el cluster 4×H100 SXM (320 GB total, NVLink)&lt;/h3>
&lt;p>Sobra capacidad por orden de magnitud. Patrón razonable:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>3 H100&lt;/strong> sirviendo el LLM principal en TP=3 (Llama 70B FP8).&lt;/li>
&lt;li>&lt;strong>1 H100 dividida en MIG instances&lt;/strong> (1g.10gb o similar) — una porción para LLM Guard (~10 GB MIG es más que suficiente), otra para el reranker, otra para embeddings.&lt;/li>
&lt;/ul>
&lt;p>Throughput agregado para LLM Guard a esa escala: 15.000-30.000 RPS. Sobra para multi-tenant grande con sesiones largas.&lt;/p>
&lt;h2 id="las-trampas-operativas-específicas">Las trampas operativas específicas&lt;/h2>
&lt;p>&lt;strong>Trampa 1 — Vault sin TTL.&lt;/strong> El Vault crece sin freno si no se limpia. En modo lib in-process por request, no hay problema (el objeto se destruye). En modo servicio centralizado con Redis, &lt;strong>falta poner TTL&lt;/strong> y el Redis se llena. Trampa silenciosa que se descubre cuando el pod de Redis OOM-killea en producción a las 6 semanas.&lt;/p>
&lt;p>&lt;strong>Trampa 2 — Vault no compartido entre pods + AI Gateway sin sticky session.&lt;/strong> Si el AI Gateway distribuye round-robin entre múltiples pods de LLM Guard, el Vault local de un pod no sabe del mapping creado por otro. Resultado: en el turno 2 de una sesión, el Deanonymize no encuentra los placeholders del turno 1 y deja &lt;code>[REDACTED_PERSON_1]&lt;/code> literal en la respuesta. Solución: Vault Redis compartido &lt;strong>o&lt;/strong> sticky session por user_id.&lt;/p>
&lt;p>&lt;strong>Trampa 3 — Modelos no exportados a ONNX en producción.&lt;/strong> Se despliega con la config por defecto (Transformers) y la latencia es 3-5× peor que la que reportan los benchmarks. Equipo asume que LLM Guard &amp;ldquo;es lento&amp;rdquo;. La solución es exportar a ONNX (built-in en el proyecto) y configurar &lt;code>recognizer_conf&lt;/code> con la ruta al &lt;code>.onnx&lt;/code> del modelo.&lt;/p>
&lt;p>&lt;strong>Trampa 4 — &lt;code>fail_fast=False&lt;/code> con muchos scanners.&lt;/strong> Sin &lt;code>fail_fast&lt;/code>, todos los scanners corren siempre, incluso si el primero ya bloqueó. Latencia 3-5× peor en tráfico adversarial. Para producción, salvo razón explícita (querer métricas completas por scanner aun bloqueando), &lt;code>fail_fast=True&lt;/code> es el default razonable.&lt;/p>
&lt;p>&lt;strong>Trampa 5 — &lt;code>cache_ttl&lt;/code> infinito + prompts con PII variable.&lt;/strong> Si la caché de la API guarda el &lt;code>sanitized_prompt&lt;/code> indefinidamente, dos sesiones distintas con misma estructura de prompt pero diferentes PII pueden colidir si la clave de caché no incluye el Vault hash. Hay que verificar que la clave de caché incluya o bien el contenido completo (sin PII) o un hash del prompt original.&lt;/p>
&lt;p>&lt;strong>Trampa 6 — Logs estructurados con PII original.&lt;/strong> Los logs stdout JSON de LLM Guard registran por defecto sólo placeholders. Pero si se añaden hooks custom para debug, es fácil filtrar la PII original al log. Auditoría regulatoria (RGPD, ENS) detecta esto y es incumplimiento. Disciplina: nunca añadir hooks que lean del Vault sin permiso explícito.&lt;/p>
&lt;p>&lt;strong>Trampa 7 — &lt;code>scan_output&lt;/code> sin &lt;code>prompt&lt;/code> original.&lt;/strong> El método &lt;code>scan_output&lt;/code> espera (&lt;code>prompt&lt;/code>, &lt;code>output&lt;/code>) para validadores que comparan ambos (Relevance, LanguageSame, FactualConsistency). Si se le pasa sólo el output, esos scanners fallan silenciosamente o devuelven &lt;code>is_valid=True&lt;/code> por defecto. Hay que conservar el &lt;code>sanitized_prompt&lt;/code> en el AI Gateway y pasarlo al scan_output.&lt;/p>
&lt;h2 id="cuándo-elegir-llm-guard-y-cuándo-no">Cuándo elegir LLM Guard (y cuándo no)&lt;/h2>
&lt;p>&lt;strong>Elegir LLM Guard cuando&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>El requisito de &lt;strong>anonimización PII con restitución automática&lt;/strong> está en la lista. Es la razón #1 para usarlo. Banca, salud, asesoría legal, RRHH — cualquier caso con PII fuerte que no debe llegar al LLM aunque éste sea local.&lt;/li>
&lt;li>Quieres un &lt;strong>pipeline pythonic&lt;/strong> sin DSL nuevo. Si el equipo es Python-puro y prefiere componer scanners como objetos antes que aprender Colang.&lt;/li>
&lt;li>El stack ya tiene un &lt;strong>AI Gateway&lt;/strong> (LiteLLM, Envoy AI, Kong AI) y se integra como plugin sin tocar la app.&lt;/li>
&lt;li>Necesitas &lt;strong>OTel y Prometheus built-in&lt;/strong> sin instrumentación adicional.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>No elegir LLM Guard cuando&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>El sistema es un &lt;strong>agente conversacional con flujos de diálogo complejos&lt;/strong> (políticas, fallbacks, escalado a humano). Ahí NeMo Guardrails con Colang es estructuralmente mejor.&lt;/li>
&lt;li>La capa de safety se reduce a &lt;strong>validar outputs estructurados&lt;/strong> (JSON, function calling). Guardrails AI con sus validators es más natural.&lt;/li>
&lt;li>Tu &lt;strong>latencia budget es ultra-agresivo&lt;/strong> (&amp;lt; 30 ms para toda la capa). Habrá que reducir scanners y aceptar cobertura menor; quizás un único PromptGuard 2 + Presidio en sidecar (patrón del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post de guardrails&lt;/a>) sea más simple.&lt;/li>
&lt;li>No quieres cargar con &lt;strong>el peso operativo del Vault distribuido&lt;/strong> (Redis, TTL, sticky session). Para sistemas pequeños sin requerimiento fuerte de PII, sobra-dimensiona.&lt;/li>
&lt;/ul>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Custom scanners&lt;/strong>: cómo escribir tu propio scanner cuando ninguno del catálogo encaja (regex compleja de dominio, classifier fine-tuned propio). El proyecto admite scanners custom heredando de &lt;code>InputScanner&lt;/code> / &lt;code>OutputScanner&lt;/code> con tres métodos.&lt;/li>
&lt;li>&lt;strong>Integración con SLSA / supply chain&lt;/strong>: cómo firmar el contenedor de LLM Guard con cosign, attestations SLSA, y verificación en cluster antes de admitirlo. Tema operativo de &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">seguridad de supply chain&lt;/a> (OWASP LLM03).&lt;/li>
&lt;li>&lt;strong>Red teaming contra LLM Guard&lt;/strong>: técnicas conocidas que evaden detectores (homoglyphs, Unicode confusables, encoding base64 dentro del prompt). El proyecto publica un suite de tests adversariales para hacer benchmarking propio. Cómo se monta como gate continuo en CI.&lt;/li>
&lt;li>&lt;strong>Benchmark comparativo con Bedrock Guardrails y Azure AI Content Safety&lt;/strong>: F1 por categoría sobre tráfico real cruzando tres deployments distintos. El &lt;a href="https://blog.lo0.es/posts/oss-vs-hyperscalers-llmops/">post de OSS vs hyperscalers&lt;/a> tiene la comparativa estratégica; falta el comparativo técnico de detección.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>LLM Guard (Protect AI)&lt;/strong>: &lt;a href="https://llm-guard.com">https://llm-guard.com&lt;/a> — documentación oficial, lista de scanners, benchmarks.&lt;/li>
&lt;li>&lt;strong>Repositorio&lt;/strong>: &lt;a href="https://github.com/protectai/llm-guard">https://github.com/protectai/llm-guard&lt;/a>.&lt;/li>
&lt;li>&lt;strong>LLM Guard API&lt;/strong>: &lt;a href="https://github.com/protectai/llm-guard/tree/main/llm_guard_api">https://github.com/protectai/llm-guard/tree/main/llm_guard_api&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Presidio (Microsoft)&lt;/strong>: &lt;a href="https://microsoft.github.io/presidio/">https://microsoft.github.io/presidio/&lt;/a> — base del scanner Anonymize.&lt;/li>
&lt;li>&lt;strong>detect-secrets (Yelp)&lt;/strong>: &lt;a href="https://github.com/Yelp/detect-secrets">https://github.com/Yelp/detect-secrets&lt;/a> — base del scanner Secrets.&lt;/li>
&lt;li>&lt;strong>Langfuse OTel ingestion&lt;/strong>: &lt;a href="https://langfuse.com/docs/opentelemetry/get-started">https://langfuse.com/docs/opentelemetry/get-started&lt;/a>.&lt;/li>
&lt;li>&lt;strong>LiteLLM guardrails&lt;/strong>: &lt;a href="https://docs.litellm.ai/docs/proxy/guardrails">https://docs.litellm.ai/docs/proxy/guardrails&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Envoy AI Gateway&lt;/strong>: &lt;a href="https://aigateway.envoyproxy.io">https://aigateway.envoyproxy.io&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Kong AI Gateway&lt;/strong>: &lt;a href="https://docs.konghq.com/hub/kong-inc/ai-prompt-guard/">https://docs.konghq.com/hub/kong-inc/ai-prompt-guard/&lt;/a>.&lt;/li>
&lt;li>&lt;strong>OWASP Top 10 for LLM Applications 2025&lt;/strong>: &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">https://owasp.org/www-project-top-10-for-large-language-model-applications/&lt;/a>.&lt;/li>
&lt;li>&lt;strong>ONNX Runtime&lt;/strong>: &lt;a href="https://onnxruntime.ai">https://onnxruntime.ai&lt;/a> — exportación de modelos HF a ONNX para acelerar.&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/guardrails-safety-llm/">Guardrails y safety en LLMs: las cuatro líneas de defensa&lt;/a> — el marco que ubica LLM Guard como una de las herramientas dentro de la capa. Aquel post explica las cuatro líneas (input, retrieval, tool, output), OWASP LLM Top 10 y compara a vista de pájaro NeMo Guardrails, Llama Guard 4, ShieldGemma, Granite Guardian, PromptGuard 2 y LLM Guard.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">El catálogo OSS para LLMOps en seis etapas&lt;/a> — ficha extendida de LLM Guard entre el resto de herramientas OSS por etapa del pipeline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation: el bibliotecario activo&lt;/a> — la prevención en ingest comparte el detector PII de Presidio con LLM Guard; el patrón Vault es la pieza nueva que se añade en runtime.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — el plano OTel sobre el que LLM Guard emite spans &lt;code>gen_ai.guardrail.*&lt;/code> que Langfuse y Tempo consumen.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow&lt;/a> — el &lt;code>prompt_id+version&lt;/code> viaja como atributo de span aunque el contenido del prompt esté anonimizado; complementa el blindaje PII de este post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs: la capa después del tracing&lt;/a> — la pareja offline de LLM Guard. Cuando un scanner reporta tasa alta de FP sobre tráfico real, el ejercicio offline contra golden anotado identifica si afinar threshold o cambiar modelo backend.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle feedback → dataset → adapter&lt;/a> — los incidentes severity HIGH que LLM Guard emite con &lt;code>risk_score &amp;gt; umbral&lt;/code> alimentan el bucle de incident-driven retrain.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/oss-vs-hyperscalers-llmops/">OSS vs hyperscalers en LLMOps&lt;/a> — la columna OSS de la fila &amp;ldquo;Guardrails&amp;rdquo; (NeMo + Presidio + Llama Guard 4 + &lt;strong>LLM Guard&lt;/strong>) frente a Bedrock Guardrails, Azure AI Content Safety, Vertex Model Armor.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">Structured output: function calling y constrained decoding&lt;/a> — el scanner JSON de LLM Guard valida estructura del output como red de seguridad cuando el motor de inferencia ya hizo constrained decoding.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde Guardrails (este post incluido) es la pareja online de la etapa Eval.&lt;/li>
&lt;/ul></description></item></channel></rss>