<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gobierno on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/gobierno/</link><description>Recent content in Gobierno on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Tue, 08 Sep 2026 07:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/gobierno/index.xml" rel="self" type="application/rss+xml"/><item><title>Claves virtuales, presupuestos y límites en LiteLLM: la capa que decide quién consume la GPU, y las cuatro cosas que la documentación cuenta mal</title><link>https://blog.lo0.es/posts/litellm-limites-presupuestos-claves-virtuales/</link><pubDate>Tue, 08 Sep 2026 07:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/litellm-limites-presupuestos-claves-virtuales/</guid><description>&lt;blockquote>
&lt;p>Tercer artículo del track operativo de la capa de control. El &lt;a href="https://blog.lo0.es/posts/litellm-langfuse-par-operativo/">par con Langfuse&lt;/a> trató la observabilidad, y &lt;a href="https://blog.lo0.es/posts/litellm-proxy-dia-2-alta-disponibilidad/">el día 2 del proxy&lt;/a> la disponibilidad. Aquí toca el gobierno: quién puede llamar, a qué modelo, cuánto y con qué presupuesto. El modelo económico que hay detrás de esas cifras está en &lt;a href="https://blog.lo0.es/posts/finops-multi-tenancy-gpu-litellm/">FinOps y multi-tenencia con LiteLLM&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>En una plataforma sobre GPUs propias no hay factura de proveedor que ordene el consumo. La capacidad es fija, conocida y compartida, así que el reparto lo tiene que imponer alguien, y ese alguien es el gateway.&lt;/p>
&lt;p>&lt;strong>La jerarquía tiene seis niveles&lt;/strong>: organización, equipo, miembro de equipo, usuario interno, clave y cliente final, más los ámbitos de proyecto y etiqueta. Los presupuestos de cada nivel se evalúan &lt;strong>de forma independiente y todos tienen que pasar&lt;/strong>. No se suman ni se anulan entre sí, con una excepción.&lt;/p>
&lt;p>&lt;strong>La excepción está en el código y es la trampa de gobierno más importante del sistema.&lt;/strong> Una clave que pertenece a un equipo suprime la comprobación del presupuesto personal de su dueño, salvo que se active &lt;code>apply_user_budget_to_team_keys&lt;/code>. Un usuario con tope de cien euros que trabaje con la clave de su equipo gasta lo que el equipo le permita.&lt;/p>
&lt;p>&lt;strong>Rotar una clave conserva su gasto.&lt;/strong> &lt;code>/key/regenerate&lt;/code> hace un UPDATE sobre la misma fila cambiando el hash, de modo que &lt;code>spend&lt;/code>, &lt;code>max_budget&lt;/code> y la ventana de presupuesto siguen intactos. Admite periodo de gracia, guardado en una tabla propia, y si el formato del periodo es incorrecto se ignora sin bloquear nada.&lt;/p>
&lt;p>&lt;strong>El limitador v3 ya es el que viene por defecto&lt;/strong> en la 1.100.0, y se vuelve al anterior con una variable de entorno. Su ventana es deslizante, de sesenta segundos, anclada a la primera petición, y los contadores se incrementan con scripts Lua dentro de Redis usando la marca de tiempo del propio Redis para cerrar la carrera entre réplicas.&lt;/p>
&lt;p>Y cuatro puntos donde la documentación oficial no coincide con el código, verificados sobre la etiqueta v1.100.0: &lt;strong>el presupuesto agotado devuelve 429&lt;/strong>, no 400; &lt;strong>las acciones auditadas son seis&lt;/strong>, no tres; &lt;code>/key/info&lt;/code> y &lt;code>/key/list&lt;/code> son &lt;strong>GET&lt;/strong>, no POST; y el limitador v3 arrastra un docstring que dice que no está listo para producción mientras es el que se carga por defecto.&lt;/p>
&lt;h2 id="estás-aquí-el-gateway-como-frontera-administrativa">Estás aquí: el gateway como frontera administrativa&lt;/h2>
&lt;p>Sin esta capa, una plataforma de inferencia propia tiene un único modo de operación: quien conoce la URL, consume. Funciona mientras hay un equipo. Deja de funcionar el día que hay tres, o que alguien conecta un agente que dispara veinte llamadas por interacción y se lleva por delante el tiempo hasta el primer token de todos los demás.&lt;/p>
&lt;p>Las dos preguntas que esta capa responde son distintas y hay que separarlas. La de &lt;strong>capacidad&lt;/strong> es instantánea: cuántas peticiones y cuántos tokens por minuto puede meter cada consumidor, para que la flota no se sature y la latencia se mantenga dentro del acuerdo de servicio. La de &lt;strong>coste&lt;/strong> es acumulada: cuánto lleva gastado cada consumidor este mes contra su presupuesto. Se configuran en el mismo sitio, se aplican en momentos distintos y fallan de formas distintas.&lt;/p>
&lt;h2 id="la-analogía-el-llavero-del-edificio">La analogía: el llavero del edificio&lt;/h2>
&lt;p>Un edificio de oficinas reparte llaves en tres niveles. La maestra abre todo y la tiene el administrador. Las de planta abren una planta entera y las tiene cada empresa arrendataria. Y las de despacho abren un despacho.&lt;/p>
&lt;p>Tres propiedades de ese sistema son las que importan aquí. Una llave se sustituye sin cambiar la cerradura de todo el edificio, y el inquilino conserva su plaza de garaje y su contador de luz: eso es la rotación que conserva el gasto. Una llave perdida se anula desde el cuadro central sin recoger nada físico: eso es el bloqueo desde la interfaz. Y el registro de quién retiró cada llave y cuándo vive en un libro aparte, que es lo que se le enseña a un inspector: eso es la tabla de auditoría.&lt;/p>
&lt;p>La analogía se rompe en un punto, y es el punto donde está el fallo de gobierno de este sistema. En el edificio, tener llave de planta no anula el límite de consumo del despacho. En LiteLLM, sí.&lt;/p>
&lt;h2 id="la-jerarquía-y-la-línea-que-la-rompe">La jerarquía, y la línea que la rompe&lt;/h2>
&lt;p>Los niveles y sus endpoints de gestión:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Nivel&lt;/th>
&lt;th>Endpoints&lt;/th>
&lt;th>Presupuesto&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Organización&lt;/td>
&lt;td>&lt;code>/organization/new&lt;/code>, &lt;code>/organization/member_add&lt;/code>, …&lt;/td>
&lt;td>&lt;code>max_budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Equipo&lt;/td>
&lt;td>&lt;code>/team/new&lt;/code>, &lt;code>/team/member_add&lt;/code>, &lt;code>/team/block&lt;/code>, …&lt;/td>
&lt;td>&lt;code>max_budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Miembro de equipo&lt;/td>
&lt;td>&lt;code>/team/member_add&lt;/code> con &lt;code>max_budget_in_team&lt;/code>&lt;/td>
&lt;td>Fila propia en la tabla de presupuestos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Usuario interno&lt;/td>
&lt;td>&lt;code>/user/new&lt;/code>, &lt;code>/user/update&lt;/code>, …&lt;/td>
&lt;td>&lt;code>max_budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Clave&lt;/td>
&lt;td>&lt;code>/key/generate&lt;/code>, &lt;code>/key/update&lt;/code>, …&lt;/td>
&lt;td>&lt;code>max_budget&lt;/code>, &lt;code>soft_budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cliente final&lt;/td>
&lt;td>&lt;code>/customer/new&lt;/code>, y el juego paralelo &lt;code>/end_user/*&lt;/code>&lt;/td>
&lt;td>&lt;code>max_budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El comportamiento por defecto es el correcto para una plataforma multi-inquilino: cada ámbito lleva su propio contador y la comprobación de cada uno es independiente de las demás. Una petición pasa si pasan todas.&lt;/p>
&lt;p>Y luego está esta condición, que vive en las comprobaciones de autorización:&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="n">is_team_key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">team_object&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">team_object&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">team_id&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">is_team_key&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">general_settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;apply_user_budget_to_team_keys&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="kc">True&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Traducido: cuando la clave pertenece a un equipo, la comprobación del presupuesto del usuario dueño de esa clave &lt;strong>retorna sin evaluar nada&lt;/strong>. El tope personal existe en la base de datos, se ve en la interfaz, y no se aplica.&lt;/p>
&lt;p>Para un despliegue donde los presupuestos personales son decorativos, da igual. Para uno donde el tope por persona es el control que se le enseñó a alguien como garantía de que un usuario no puede desbordar la plataforma, es un agujero. La línea que lo cierra es una:&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">general_settings&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">apply_user_budget_to_team_keys&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="claves-virtuales-el-ciclo-de-vida-completo">Claves virtuales: el ciclo de vida completo&lt;/h2>
&lt;p>Los endpoints existen todos y algunos no están donde se espera:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Ruta&lt;/th>
&lt;th>Método&lt;/th>
&lt;th>Para qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/key/generate&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Crear&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/update&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Modificar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/info&lt;/code>&lt;/td>
&lt;td>&lt;strong>GET&lt;/strong>&lt;/td>
&lt;td>Consultar una&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/list&lt;/code>&lt;/td>
&lt;td>&lt;strong>GET&lt;/strong>&lt;/td>
&lt;td>Listar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/delete&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Borrar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/regenerate&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Rotar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/block&lt;/code>, &lt;code>/key/unblock&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Suspender y reactivar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/key/health&lt;/code>&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>Comprobar los callbacks de esa clave&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los dos marcados en negrita son GET, y varios ejemplos de la documentación dan a entender lo contrario. &lt;code>/key/health&lt;/code> tampoco es lo que su nombre sugiere: no valida la clave ni su presupuesto, comprueba los callbacks de registro asociados a ella.&lt;/p>
&lt;p>Los parámetros de creación son muchos, y los que gobiernan son estos:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s1">&amp;#39;https://gateway.interno/key/generate&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Authorization: Bearer sk-...&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;key_alias&amp;#34;: &amp;#34;equipo-datos-notebooks&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_id&amp;#34;: &amp;#34;t-datos&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;models&amp;#34;: [&amp;#34;llama-70b&amp;#34;, &amp;#34;qwen-30b&amp;#34;],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_budget&amp;#34;: 250,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;budget_duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;tpm_limit&amp;#34;: 400000,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;rpm_limit&amp;#34;: 600,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_parallel_requests&amp;#34;: 8,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;duration&amp;#34;: &amp;#34;90d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;tags&amp;#34;: [&amp;#34;produccion&amp;#34;, &amp;#34;notebooks&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Junto a esos hay controles que rara vez se usan y resuelven problemas concretos: &lt;code>model_max_budget&lt;/code> para poner tope por modelo dentro de la misma clave, &lt;code>model_tpm_limit&lt;/code> y &lt;code>model_rpm_limit&lt;/code> para lo mismo con capacidad, &lt;code>enforced_params&lt;/code> para exigir que ciertas claves lleguen siempre con determinados campos, &lt;code>allowed_routes&lt;/code> para que una clave de aplicación no pueda tocar los endpoints de gestión, y &lt;code>blocked&lt;/code> para nacer suspendida.&lt;/p>
&lt;h3 id="cómo-se-guardan">Cómo se guardan&lt;/h3>
&lt;p>El hash de una clave es un SHA-256 hexadecimal de una sola pasada, sin sal y sin función de derivación:&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="n">hashed_token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">hashlib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">encode&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hexdigest&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La fila vive en &lt;code>LiteLLM_VerificationToken&lt;/code>, con el hash como clave primaria, y contiene el gasto acumulado, los límites, la caducidad, los modelos permitidos y el resto de la configuración.&lt;/p>
&lt;p>Dos consecuencias que hay que tener claras antes de una auditoría. La primera es que este hash no está pensado para resistir un ataque de diccionario sobre la base de datos, y el propio código lo reconoce en un comentario sobre la derivación de claves de cifrado, donde admite que un SHA-256 sin sal de una pasada no es una función de derivación y que pasar a HKDF sería más defendible en una auditoría. La mitigación real es que las claves virtuales se generan con suficiente entropía, no la fortaleza del hash. La segunda es que &lt;code>LITELLM_SALT_KEY&lt;/code> &lt;strong>no interviene aquí&lt;/strong>: esa variable cifra las credenciales de proveedor almacenadas, no las claves virtuales.&lt;/p>
&lt;p>Sobre esa variable hay una trampa que tumba un despliegue entero. Si no está definida, &lt;strong>cae por defecto en la clave maestra&lt;/strong>. Rotar &lt;code>LITELLM_MASTER_KEY&lt;/code> sin haber fijado antes una &lt;code>LITELLM_SALT_KEY&lt;/code> propia deja ilegibles todas las credenciales de proveedor guardadas, y el fallo no bloquea: registra un error y devuelve nulo, con lo que el síntoma aparece como modelos que dejan de autenticarse sin explicación. Fijar &lt;code>LITELLM_SALT_KEY&lt;/code> el día uno, y no tocarla nunca, es de las cosas que más disgustos ahorra.&lt;/p>
&lt;h3 id="rotación">Rotación&lt;/h3>
&lt;p>&lt;code>/key/regenerate&lt;/code> no crea una clave nueva y borra la vieja. Actualiza la misma fila cambiando el hash, de modo que &lt;strong>el gasto acumulado, el presupuesto máximo y la ventana de reinicio se conservan&lt;/strong>. Para un chargeback mensual esto es exactamente lo que se quiere: rotar a mitad de mes no pone el contador a cero.&lt;/p>
&lt;p>Además admite periodo de gracia, para que la clave antigua siga siendo válida mientras los consumidores se actualizan:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s1">&amp;#39;https://gateway.interno/key/sk-vieja/regenerate&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Authorization: Bearer sk-...&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;grace_period&amp;#34;: &amp;#34;24h&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El hash antiguo se guarda en una tabla aparte con su fecha de revocación. Sin ese parámetro, la revocación es inmediata, y el valor por defecto se puede fijar con &lt;code>LITELLM_KEY_ROTATION_GRACE_PERIOD&lt;/code>.&lt;/p>
&lt;p>Un detalle con dientes: si el formato del periodo es incorrecto, el sistema registra un aviso y &lt;strong>continúa sin periodo de gracia&lt;/strong>. La rotación se completa, la clave vieja muere en el acto, y lo único que queda es una línea de log. En un procedimiento de rotación automatizado, esa combinación de &amp;ldquo;sigue adelante&amp;rdquo; y &amp;ldquo;avisa en el log&amp;rdquo; es la que produce el corte de servicio a las tres de la mañana.&lt;/p>
&lt;h2 id="qué-pasa-exactamente-cuando-se-agota-un-presupuesto">Qué pasa exactamente cuando se agota un presupuesto&lt;/h2>
&lt;p>El código lanza &lt;code>BudgetExceededError&lt;/code> con &lt;strong>status 429&lt;/strong>. La documentación en algún punto dice 400, y en eso manda el código. Que sea 429 tiene una implicación práctica: los clientes de OpenAI reintentan automáticamente ante un 429, así que un presupuesto agotado se convierte en una tormenta de reintentos contra un gateway que va a seguir rechazando. Los clientes internos deberían distinguir el motivo, que viaja en el mensaje.&lt;/p>
&lt;p>Los mensajes son plantillas y dicen el ámbito, el gasto y el tope:&lt;/p>
&lt;pre tabindex="0">&lt;code>Budget has been exceeded! Key=&amp;lt;clave&amp;gt; Current cost: &amp;lt;gasto&amp;gt;, Max budget: &amp;lt;tope&amp;gt;
Budget has been exceeded! Team=&amp;lt;equipo&amp;gt; Current cost: &amp;lt;gasto&amp;gt;, Max budget: &amp;lt;tope&amp;gt;
ExceededBudget: User=&amp;lt;usuario&amp;gt; over budget. Spend=&amp;lt;gasto&amp;gt;, Budget=&amp;lt;tope&amp;gt;
ExceededBudget: End User=&amp;lt;cliente&amp;gt; over budget. Spend=&amp;lt;gasto&amp;gt;, Budget=&amp;lt;tope&amp;gt;
Budget has been exceeded! Organization=&amp;lt;org&amp;gt; ...
Budget has been exceeded! Tag=&amp;lt;etiqueta&amp;gt; ...
&lt;/code>&lt;/pre>&lt;p>Dos precisiones sobre la comparación. Es &lt;code>gasto &amp;gt;= tope&lt;/code>, con el signo igual incluido, así que llegar justo al tope ya bloquea. Y el presupuesto blando, &lt;code>soft_budget&lt;/code>, &lt;strong>no bloquea nada&lt;/strong>: solo escribe una línea de log al cruzarlo. Sirve para avisar, no para contener, y montar sobre él una alerta es la forma correcta de usarlo.&lt;/p>
&lt;p>El resto de rechazos, con sus códigos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Situación&lt;/th>
&lt;th>Código&lt;/th>
&lt;th>Comportamiento&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Presupuesto agotado&lt;/td>
&lt;td>429&lt;/td>
&lt;td>Mensaje con ámbito, gasto y tope&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Clave caducada&lt;/td>
&lt;td>401&lt;/td>
&lt;td>Además borra la entrada de caché, así que la caducidad surte efecto en toda la flota sin esperar al TTL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modelo no permitido&lt;/td>
&lt;td>403&lt;/td>
&lt;td>Tipo de error distinto según el ámbito: clave, equipo, usuario, organización o proyecto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Clave bloqueada&lt;/td>
&lt;td>Excepción genérica&lt;/td>
&lt;td>&lt;code>Key is blocked. Update via /key/unblock if you're an admin.&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La última fila es una inconsistencia del proyecto que merece conocerse: el bloqueo lanza una excepción sin tipo ni código explícito, mientras la caducidad y el acceso a modelos sí los llevan. Cualquier alerta que discrimine por tipo de error no verá los bloqueos igual que ve lo demás.&lt;/p>
&lt;p>Un apunte útil sobre modelos permitidos: la comprobación acepta tanto el alias como el modelo subyacente, de forma que una clave autorizada a &lt;code>llama-70b&lt;/code> puede llamar por su alias o por su nombre real sin configuración adicional.&lt;/p>
&lt;h2 id="límites-de-capacidad-el-limitador-v3">Límites de capacidad: el limitador v3&lt;/h2>
&lt;p>Este es el cambio silencioso más relevante del año en esta capa. En la 1.100.0, el limitador que se carga por defecto es la tercera versión. Para volver a la anterior hay que pedirlo de forma explícita:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LEGACY_MULTI_INSTANCE_RATE_LIMITING&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No hay bandera para activarlo, solo para desactivarlo. Y arrastra una contradicción que hay que conocer antes de fiarse: el propio fichero sigue abriendo con un comentario que dice que está en desarrollo y no listo para producción, mientras es el que corre. El diseño está declaradamente inspirado en el de Envoy.&lt;/p>
&lt;p>&lt;strong>La ventana es deslizante y dura sesenta segundos&lt;/strong>, configurable con &lt;code>LITELLM_RATE_LIMIT_WINDOW_SIZE&lt;/code>. No es un registro rodante exacto ni una ventana alineada con el reloj: se ancla a la primera petición y se reinicia cuando ha pasado el tamaño de ventana desde ese momento. Para límites de un minuto, la diferencia con una ventana de calendario aparece en los bordes y rara vez importa.&lt;/p>
&lt;p>Lo que sí importa es Redis. Los contadores se incrementan con scripts Lua ejecutados dentro de Redis, usando la marca de tiempo del propio Redis y no la del pod, precisamente para que los reinicios de ventana sean deterministas entre réplicas y para cerrar la carrera entre comprobar y contar. Sin Redis, &lt;strong>cada pod cuenta por su cuenta y el límite efectivo se multiplica por el número de pods&lt;/strong>. Un límite de 600 peticiones por minuto con cinco réplicas es un límite de 3.000.&lt;/p>
&lt;p>Hay una imprecisión inherente en los límites de tokens que no se resuelve con Redis. El número de tokens de salida no se conoce hasta que la respuesta termina, así que el limitador cuenta con una estimación antes de la llamada y reconcilia después. El exceso posible es, por cada petición en vuelo, la diferencia entre lo estimado y lo real. Con concurrencia alta y respuestas largas, eso permite sobrepasar el tope dentro de una ventana. Para proteger la flota, el límite de peticiones concurrentes (&lt;code>max_parallel_requests&lt;/code>) es un instrumento más directo que el de tokens por minuto.&lt;/p>
&lt;h3 id="lo-que-ve-el-cliente">Lo que ve el cliente&lt;/h3>
&lt;p>Al ser limitado, un 429 con el mensaje del ámbito, el límite actual, lo que queda y cuándo se reinicia. Y tres cabeceras: &lt;code>retry-after&lt;/code> con el tamaño de ventana, es decir &lt;code>60&lt;/code> por defecto, más &lt;code>rate_limit_type&lt;/code> y &lt;code>reset_at&lt;/code>.&lt;/p>
&lt;p>En el camino de éxito, las cabeceras informativas usan un formato anidado propio:&lt;/p>
&lt;pre tabindex="0">&lt;code>x-ratelimit-api_key-remaining-requests
x-ratelimit-api_key-limit-tokens
x-ratelimit-team-remaining-requests
&lt;/code>&lt;/pre>&lt;p>&lt;strong>No son los nombres estándar&lt;/strong> &lt;code>x-ratelimit-limit-requests&lt;/code> que emiten OpenAI y compatibles, así que un cliente que espere el formato habitual no encontrará nada. Hay además una incidencia abierta sobre estas cabeceras perdiéndose en respuestas en streaming, que es la mayoría del tráfico de un asistente.&lt;/p>
&lt;h2 id="identidad-más-allá-de-la-clave-estática">Identidad: más allá de la clave estática&lt;/h2>
&lt;p>Repartir claves a mano funciona con tres equipos y deja de funcionar con treinta. Las dos vías para conectar el gateway a la identidad corporativa:&lt;/p>
&lt;p>&lt;strong>JWT.&lt;/strong> Se activa con &lt;code>enable_jwt_auth&lt;/code> y se configura con el bloque &lt;code>litellm_jwtauth&lt;/code>, que mapea campos del token a entidades de LiteLLM: el equipo sale de &lt;code>team_id_jwt_field&lt;/code>, el usuario de &lt;code>user_id_jwt_field&lt;/code>, la organización de &lt;code>org_id_jwt_field&lt;/code>. Admite notación de punto para claims anidados, creación automática de usuarios y equipos con &lt;code>user_id_upsert&lt;/code> y &lt;code>team_id_upsert&lt;/code>, control de acceso basado en roles con &lt;code>enforce_rbac&lt;/code>, restricción de modelos por equipo con &lt;code>enforce_team_based_model_access&lt;/code>, y varios emisores a la vez. La clave pública del emisor se cachea con un TTL de 600 segundos por defecto.&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">general_settings&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">enable_jwt_auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="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">litellm_jwtauth&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">team_id_jwt_field&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;groups&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">user_id_jwt_field&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sub&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">user_email_jwt_field&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;email&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">enforce_rbac&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="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">enforce_team_based_model_access&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="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">team_id_upsert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>OIDC para la interfaz de administración.&lt;/strong> Con Keycloak se usa el proveedor genérico, y las variables son &lt;code>PROXY_BASE_URL&lt;/code>, &lt;code>GENERIC_CLIENT_ID&lt;/code>, &lt;code>GENERIC_CLIENT_SECRET&lt;/code>, &lt;code>GENERIC_AUTHORIZATION_ENDPOINT&lt;/code>, &lt;code>GENERIC_TOKEN_ENDPOINT&lt;/code>, &lt;code>GENERIC_USERINFO_ENDPOINT&lt;/code>, más un juego de atributos para mapear el identificador, el correo, el nombre y el rol, y &lt;code>GENERIC_ROLE_MAPPINGS_*&lt;/code> para traducir grupos de Keycloak a roles de LiteLLM. El inicio de sesión está limitado en frecuencia y las sesiones tienen TTL propio.&lt;/p>
&lt;p>Para el aprovisionamiento automático hay SCIM en la versión enterprise, con base &lt;code>/scim/v2&lt;/code>, y la baja de un usuario bloquea sus claves y las saca de la caché de autenticación. Esa parte no está en el árbol abierto, así que no se puede verificar contra el código.&lt;/p>
&lt;p>Esto encaja con el trabajo de identidad ya tratado en &lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">hardening y secretos del stack soberano&lt;/a> y en el artículo sobre &lt;a href="https://blog.lo0.es/posts/mcp-crece-autenticacion-keycloak/">autenticación de MCP con Keycloak&lt;/a>, que usa el mismo emisor.&lt;/p>
&lt;h2 id="auditoría-qué-queda-registrado">Auditoría: qué queda registrado&lt;/h2>
&lt;p>La tabla es &lt;code>LiteLLM_AuditLog&lt;/code> y sus columnas son las que se esperan de un registro de cambios:&lt;/p>
&lt;pre tabindex="0">&lt;code>id, updated_at, changed_by, changed_by_api_key,
action, table_name, object_id, before_value, updated_values
&lt;/code>&lt;/pre>&lt;p>Guarda el valor anterior y el nuevo, y atribuye el cambio a un actor y a la clave con la que se hizo.&lt;/p>
&lt;p>Tres precisiones que la documentación no recoge bien.&lt;/p>
&lt;p>&lt;strong>Las acciones son seis, en pasado&lt;/strong>: &lt;code>created&lt;/code>, &lt;code>updated&lt;/code>, &lt;code>deleted&lt;/code>, &lt;code>blocked&lt;/code>, &lt;code>unblocked&lt;/code> y &lt;code>rotated&lt;/code>. La documentación menciona tres. Que el bloqueo, el desbloqueo y la rotación sean acciones propias es justo lo que hace falta para reconstruir el ciclo de vida de una credencial ante un auditor.&lt;/p>
&lt;p>&lt;strong>El alcance es mayor de lo anunciado.&lt;/strong> Además de equipos, usuarios, claves y modelos, se auditan los cambios de configuración del proxy y de la configuración de SSO. Un cambio de emisor de identidad queda registrado.&lt;/p>
&lt;p>&lt;strong>El valor por defecto depende de la licencia.&lt;/strong> El orden de resolución es &lt;code>litellm_settings.store_audit_logs&lt;/code>, luego la variable &lt;code>LITELLM_STORE_AUDIT_LOGS&lt;/code>, y si ninguna está puesta, queda &lt;strong>activado en enterprise y desactivado en la versión abierta&lt;/strong>. Un despliegue OSS que dé por hecho que hay auditoría no la tiene:&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">litellm_settings&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">store_audit_logs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dos piezas más para una cadena de evidencia seria. La atribución del actor se puede pasar con la cabecera &lt;code>LiteLLM-Changed-By&lt;/code>, y su uso está restringido por la configuración de la clave o del equipo, de modo que un llamante cualquiera no puede falsificarla. Y los eventos se pueden enviar fuera con &lt;code>audit_log_callbacks&lt;/code> y sus parámetros de S3, que es lo que permite depositarlos en un almacén inmutable con bloqueo de objeto.&lt;/p>
&lt;p>Esa última parte es la que convierte el registro en evidencia. La tabla en Postgres es mutable por quien tenga acceso a la base de datos, y a septiembre de 2026 no tiene índices, así que consultarla por rango de fechas sobre un histórico largo tampoco será cómodo.&lt;/p>
&lt;h2 id="qué-demuestra-esto-ante-un-auditor-y-qué-no">Qué demuestra esto ante un auditor, y qué no&lt;/h2>
&lt;p>Esta es la parte que decide si el montaje sirve para un cliente regulado. Con todo lo anterior configurado, la plataforma sostiene sin dificultad:&lt;/p>
&lt;p>&lt;strong>Control de acceso por identidad corporativa&lt;/strong>, con altas y bajas en el proveedor de identidad y no en el gateway, y con el rol determinando a qué modelos se llega. &lt;strong>Segregación entre inquilinos&lt;/strong>, con contadores independientes por organización, equipo y persona. &lt;strong>Trazabilidad de los cambios administrativos&lt;/strong>, con actor, momento, valor anterior y valor nuevo, exportable a un almacén inmutable. Y &lt;strong>límites de consumo demostrables&lt;/strong>, con la evidencia del rechazo en el log de gasto.&lt;/p>
&lt;p>Lo que no cubre, y hay que resolver fuera:&lt;/p>
&lt;p>&lt;strong>El registro de las interacciones&lt;/strong> no es esto. La tabla de auditoría registra cambios de configuración, no llamadas al modelo. Las llamadas están en el log de gasto y en las trazas, y las trazas son best-effort, que es la discusión del &lt;a href="https://blog.lo0.es/posts/litellm-langfuse-par-operativo/">primer artículo de la serie&lt;/a>.&lt;/p>
&lt;p>&lt;strong>La integridad del registro&lt;/strong> no la aporta Postgres. Sin exportación a almacenamiento inmutable, la evidencia es tan fuerte como los permisos de la base de datos.&lt;/p>
&lt;p>&lt;strong>El contenido de los prompts&lt;/strong> es un problema aparte, con su propio tratamiento de datos personales y su decisión sobre si se guarda, se enmascara o no se registra.&lt;/p>
&lt;p>Y una advertencia sobre el control que suele enseñarse primero: mientras &lt;code>apply_user_budget_to_team_keys&lt;/code> no esté activado, el tope por persona no se está aplicando a las claves de equipo. Enseñar ese campo en la interfaz como prueba de un control que no se ejecuta es el tipo de hallazgo que un auditor competente encuentra. El mapeo completo de controles está en &lt;a href="https://blog.lo0.es/posts/controles-tecnicos-ens-42001-eu-ai-act/">ENS, ISO 42001 y EU AI Act&lt;/a>.&lt;/p>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;ol>
&lt;li>&lt;code>apply_user_budget_to_team_keys: true&lt;/code> si los presupuestos personales tienen que aplicarse.&lt;/li>
&lt;li>&lt;code>LITELLM_SALT_KEY&lt;/code> fijada el día uno, distinta de la clave maestra y nunca rotada a la ligera.&lt;/li>
&lt;li>&lt;code>store_audit_logs: true&lt;/code> de forma explícita, sin confiar en el valor por defecto.&lt;/li>
&lt;li>&lt;code>audit_log_callbacks&lt;/code> hacia un almacén inmutable si hay que sostener una auditoría.&lt;/li>
&lt;li>Redis obligatorio si hay más de una réplica, o los límites se multiplican por el número de pods.&lt;/li>
&lt;li>&lt;code>max_parallel_requests&lt;/code> por clave, además de los límites por minuto, para proteger la flota de un agente desbocado.&lt;/li>
&lt;li>&lt;code>allowed_routes&lt;/code> en las claves de aplicación, para que no alcancen los endpoints de gestión.&lt;/li>
&lt;li>&lt;code>soft_budget&lt;/code> con una alerta encima, entendiendo que no bloquea.&lt;/li>
&lt;li>Rotación con &lt;code>grace_period&lt;/code> verificado, porque un formato inválido revoca en el acto.&lt;/li>
&lt;li>JWT contra Keycloak en cuanto haya más de un puñado de equipos, con &lt;code>enforce_rbac&lt;/code> y &lt;code>enforce_team_based_model_access&lt;/code>.&lt;/li>
&lt;/ol>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>El tope de un usuario no se aplica.&lt;/strong> Está usando una clave de equipo y falta &lt;code>apply_user_budget_to_team_keys&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Los clientes reintentan en bucle contra un presupuesto agotado.&lt;/strong> El código es 429 y las bibliotecas de OpenAI reintentan los 429 automáticamente.&lt;/p>
&lt;p>&lt;strong>El presupuesto blando no ha detenido nada.&lt;/strong> No bloquea, solo registra.&lt;/p>
&lt;p>&lt;strong>Los límites por minuto se cumplen al doble o al triple.&lt;/strong> No hay Redis, y cada réplica lleva su cuenta.&lt;/p>
&lt;p>&lt;strong>El cliente no ve las cabeceras de límite.&lt;/strong> Los nombres son anidados y propios, y en streaming hay una incidencia abierta por la que se pierden.&lt;/p>
&lt;p>&lt;strong>No hay registro de auditoría.&lt;/strong> Es un despliegue OSS y el valor por defecto ahí es desactivado.&lt;/p>
&lt;p>&lt;strong>Los modelos dejan de autenticarse tras rotar la clave maestra.&lt;/strong> No había &lt;code>LITELLM_SALT_KEY&lt;/code> fija, y las credenciales de proveedor cifradas ya no se pueden descifrar.&lt;/p>
&lt;p>&lt;strong>La rotación cortó el servicio en el acto.&lt;/strong> El &lt;code>grace_period&lt;/code> llevaba un formato que el sistema no entendió y siguió adelante sin él.&lt;/p>
&lt;p>&lt;strong>El bloqueo de una clave no dispara la alerta.&lt;/strong> Se lanza como excepción genérica, sin el tipo que sí llevan la caducidad y el acceso a modelos.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>La capa de claves y presupuestos es la que convierte una URL compartida en una plataforma con inquilinos. Está bien resuelta en LiteLLM y tiene más profundidad de la que la documentación sugiere, con presupuestos por modelo dentro de una clave, ventanas de presupuesto múltiples, límites por etiqueta y rotación que preserva el histórico.&lt;/p>
&lt;p>De todo el artículo, dos cosas se llevan la atención. La línea de &lt;code>apply_user_budget_to_team_keys&lt;/code>, porque es la diferencia entre tener un control y creer que se tiene. Y la fijación de &lt;code>LITELLM_SALT_KEY&lt;/code>, porque su valor por defecto convierte una rotación rutinaria de la clave maestra en una caída difícil de diagnosticar. Las dos son una línea de configuración y las dos se descubren tarde.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/litellm-langfuse-par-operativo/">LiteLLM y Langfuse: el par operativo&lt;/a> — dónde acaban las trazas de todo esto, y por qué el registro de auditoría no puede apoyarse en ellas.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/litellm-proxy-dia-2-alta-disponibilidad/">LiteLLM en día 2: alta disponibilidad&lt;/a> — el Redis que aquí se da por montado, y la aritmética de Postgres que sostiene la tabla de gasto.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/finops-multi-tenancy-gpu-litellm/">FinOps y multi-tenencia GPU con LiteLLM&lt;/a> — de dónde salen las cifras que se ponen en &lt;code>max_budget&lt;/code>, y la diferencia entre showback y chargeback.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/coste-por-token-y-por-request/">Del GPU-hora al coste por token y por petición&lt;/a> — la conversión que da sentido a un presupuesto en euros sobre una GPU propia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/controles-tecnicos-ens-42001-eu-ai-act/">Controles técnicos: ENS, ISO 42001 y EU AI Act&lt;/a> — el mapeo completo del que este artículo cubre la parte de control de acceso y trazabilidad administrativa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">Hardening y secretos del stack soberano&lt;/a> — la gestión de &lt;code>LITELLM_SALT_KEY&lt;/code> y del resto de secretos del despliegue.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mcp-crece-autenticacion-keycloak/">Cuando MCP crece: autenticación con Keycloak&lt;/a> — el mismo emisor de identidad, aplicado a la otra puerta de entrada de la plataforma.&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>LiteLLM, &lt;em>Virtual Keys&lt;/em> (parámetros de creación, ciclo de vida, rotación): &lt;a href="https://docs.litellm.ai/docs/proxy/virtual_keys">https://docs.litellm.ai/docs/proxy/virtual_keys&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;em>Budgets, Rate Limits&lt;/em> (jerarquía, presupuestos por nivel, clientes finales): &lt;a href="https://docs.litellm.ai/docs/proxy/users">https://docs.litellm.ai/docs/proxy/users&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;em>JWT Token Auth&lt;/em> (bloque &lt;code>litellm_jwtauth&lt;/code> y todos sus campos): &lt;a href="https://docs.litellm.ai/docs/proxy/token_auth">https://docs.litellm.ai/docs/proxy/token_auth&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;em>Audit Logs&lt;/em> y administración múltiple (la cabecera &lt;code>LiteLLM-Changed-By&lt;/code>): &lt;a href="https://docs.litellm.ai/docs/proxy/multiple_admins">https://docs.litellm.ai/docs/proxy/multiple_admins&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;em>SCIM&lt;/em> (aprovisionamiento automático, versión enterprise): &lt;a href="https://docs.litellm.ai/docs/tutorials/scim_litellm">https://docs.litellm.ai/docs/tutorials/scim_litellm&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>litellm/proxy/auth/auth_checks.py&lt;/code> en la etiqueta v1.100.0 (la condición de &lt;code>apply_user_budget_to_team_keys&lt;/code> y los mensajes de presupuesto agotado): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/auth/auth_checks.py">https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/auth/auth_checks.py&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>litellm/proxy/management_endpoints/key_management_endpoints.py&lt;/code> en v1.100.0 (rutas, rotación sobre la misma fila, periodo de gracia): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/management_endpoints/key_management_endpoints.py">https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/management_endpoints/key_management_endpoints.py&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>litellm/proxy/hooks/parallel_request_limiter_v3.py&lt;/code> y &lt;code>litellm/proxy/hooks/__init__.py&lt;/code> en v1.100.0 (el limitador v3 como valor por defecto, la ventana, los scripts Lua): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/hooks/parallel_request_limiter_v3.py">https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/hooks/parallel_request_limiter_v3.py&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>litellm/exceptions.py&lt;/code> en v1.100.0 (&lt;code>BudgetExceededError&lt;/code> con código 429): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/exceptions.py">https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/exceptions.py&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>litellm/proxy/common_utils/encrypt_decrypt_utils.py&lt;/code> en v1.100.0 (&lt;code>LITELLM_SALT_KEY&lt;/code> cayendo en la clave maestra, y el comentario sobre la derivación de claves): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/common_utils/encrypt_decrypt_utils.py">https://github.com/BerriAI/litellm/blob/v1.100.0/litellm/proxy/common_utils/encrypt_decrypt_utils.py&lt;/a>.&lt;/li>
&lt;li>LiteLLM, &lt;code>schema.prisma&lt;/code> en v1.100.0 (&lt;code>LiteLLM_VerificationToken&lt;/code>, &lt;code>LiteLLM_AuditLog&lt;/code>, la tabla de claves deprecadas): &lt;a href="https://github.com/BerriAI/litellm/blob/v1.100.0/schema.prisma">https://github.com/BerriAI/litellm/blob/v1.100.0/schema.prisma&lt;/a>.&lt;/li>
&lt;li>LiteLLM, issue #27748 — cabeceras &lt;code>x-ratelimit-*&lt;/code> perdidas en respuestas en streaming con el limitador v3: &lt;a href="https://github.com/BerriAI/litellm/issues/27748">https://github.com/BerriAI/litellm/issues/27748&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>