Governance: virtual keys, presupuestos y límites
Governance: virtual keys, presupuestos y límites
En el capítulo 8 aprendiste a reducir costo y latencia con el caché semántico. Esa optimización es valiosa, pero plantea una pregunta incómoda: ¿quién está gastando ese presupuesto y cómo evitas que un equipo, un cliente o un script descontrolado se coma toda tu cuota de OpenAI en una tarde? Aquí es donde entra el módulo de governance (gobierno) de Bifrost.
Hasta ahora repartiste tus claves reales (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) entre proveedores como vimos en el capítulo 4. El problema es evidente: si entregas esas claves a tus equipos, pierdes todo control. No sabes quién consume qué, no puedes ponerle techo a nadie y revocar una clave significa rotarla para todo el mundo. Governance resuelve esto introduciendo una capa de identidad y política entre quien consume y tus claves reales: las virtual keys.
En este capítulo construirás, paso a paso, un sistema de gobierno completo: virtual keys que aíslan a tus consumidores, una jerarquía de presupuestos que va desde la clave individual hasta el cliente, límites de rate por tokens y por requests, restricciones de qué modelos puede usar cada quien, headers obligatorios para atribución y, finalmente, enrutamiento inteligente que manda las consultas simples a modelos baratos y las complejas a modelos potentes.
Qué es una virtual key
Una virtual key (VK) es la entidad central de governance en Bifrost. Es una clave lógica (un string con prefijo sk-bf-*) que entregas a un equipo, una aplicación o un cliente sin exponer jamás tus API keys reales. La VK no es solo una credencial: es el punto donde se enganchan las políticas de acceso, presupuesto y límites.
Cada virtual key te da control sobre cuatro dimensiones:
- Control de acceso: filtra por modelo y por proveedor. Una VK puede tener permitido
gpt-4o-minide OpenAI y nada más. - Gestión de costo: presupuesto propio que se verifica junto al del equipo y el del cliente.
- Rate limiting: throttling por tokens y por requests, aplicado únicamente a nivel de VK.
- Restricción de claves: limita el uso a claves de proveedor específicas mediante
key_ids.
Una propiedad clave es que la VK pertenece de forma mutuamente exclusiva a un equipo (team_id) O a un cliente (customer_id) O a ninguno de los dos. Nunca a ambos. Además tiene un estado is_active que te permite habilitarla o deshabilitarla al instante sin tocar tus claves reales.
flowchart LR
Dev["Equipo / App / Cliente"] -->|"x-bf-vk: sk-bf-..."| VK["Virtual Key<br/>(política + presupuesto)"]
VK -->|valida acceso| Acceso{"Modelo permitido?<br/>Presupuesto OK?<br/>Rate limit OK?"}
Acceso -->|si| Real["Claves reales<br/>OPENAI_API_KEY<br/>ANTHROPIC_API_KEY"]
Acceso -->|no| Rechazo["402 / 403 / 429"]
Real --> Proveedor["Proveedor de IA"]
El header de autenticación
Para usar una virtual key en una petición de inferencia, la pasas en una cabecera HTTP. Bifrost acepta varias formas según el estilo de SDK que uses:
x-bf-vk— cabecera específica de Bifrost (la más recomendada).Authorization: Bearer sk-bf-*— estilo OpenAI.x-api-key— estilo Anthropic.x-goog-api-key— estilo Google Gemini.
Nota de compatibilidad: las virtual keys antiguas (sin el prefijo
sk-bf-*) solo se soportan con la cabecerax-bf-vk.
Una petición de inferencia con virtual key se ve así:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-bf-vk: sk-bf-*" \
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hola!"}]}'
El consumidor nunca ve tu clave real de OpenAI: solo conoce su sk-bf-*. Si mañana ese equipo deja la empresa, deshabilitas la VK y listo, sin rotar nada más.
Autenticación vs. virtual keys
Conviene separar dos capas que son independientes:
- Autenticación: valida la identidad del usuario que administra Bifrost (credenciales Basic/Bearer).
- Virtual keys: enrutamiento de la petición y gobierno.
Cuando la autenticación está habilitada, usa x-bf-vk para la virtual key, porque la cabecera Authorization la consume la autenticación. Si configuras disable_auth_on_inference: true, la VK sí puede viajar en Authorization. Y si quieres forzar que toda petición de inferencia lleve una VK, activa enforce_auth_on_inference: true en tu configuración.
Crear virtual keys
Tienes tres maneras de crear virtual keys: la Web UI, la API de governance y el config.json. Veremoslas en orden de menor a mayor automatización.
Desde la Web UI
En la interfaz que conociste en el capítulo 3, navega a Virtual Keys → Add Virtual Key y configura:
- Budget: límite máximo en dólares y duración de reset (
1m/1h/1d/1w/1M/1Y). - Calendar alignment: opción para alinear los resets de presupuesto a límites de calendario.
- Rate limits: límites de tokens y de requests con sus duraciones de reset.
- Team o Customer: asignación (mutuamente exclusiva).
Desde la API de governance
Para crear una VK adjunta a un equipo, haces un POST al endpoint de governance:
curl -X POST http://localhost:8080/api/governance/virtual-keys \
-H "Content-Type: application/json" \
-d '{
"name": "Engineering Team API",
"description": "Main API key for engineering team",
"provider_configs": [
{
"provider": "openai",
"weight": 0.5,
"allowed_models": ["gpt-4o-mini"]
}
],
"team_id": "team-eng-001",
"budget": {
"max_limit": 100.00,
"reset_duration": "1M"
},
"rate_limit": {
"token_max_limit": 10000,
"token_reset_duration": "1h",
"request_max_limit": 100,
"request_reset_duration": "1m"
},
"key_ids": ["8c52039e-38c6-48b2-8016-0bd884b7befb"],
"is_active": true
}'
Para una VK adjunta directamente a un cliente, cambias team_id por customer_id:
curl -X POST http://localhost:8080/api/governance/virtual-keys \
-H "Content-Type: application/json" \
-d '{
"name": "Executive API Key",
"description": "Direct customer-level API access",
"provider_configs": [
{
"provider": "openai",
"weight": 0.5,
"allowed_models": ["gpt-4o"]
}
],
"customer_id": "customer-acme-corp",
"budget": {
"max_limit": 500.00,
"reset_duration": "1M"
},
"is_active": true
}'
El resto de operaciones CRUD sobre virtual keys son:
| Operación | Método y endpoint |
|---|---|
| Listar todas | GET /api/governance/virtual-keys |
| Obtener una | GET /api/governance/virtual-keys/{vk_id} |
| Actualizar | PUT /api/governance/virtual-keys/{vk_id} |
| Eliminar | DELETE /api/governance/virtual-keys/{vk_id} |
Desde config.json
Si prefieres declarar tu gobierno como código, el bloque governance en config.json te permite definir virtual keys, presupuestos y rate limits de forma estática:
{
"governance": {
"virtual_keys": [
{
"id": "vk-001",
"name": "Engineering Team API",
"value": "sk-bf-*",
"description": "Main API key for engineering team",
"is_active": true,
"provider_configs": [
{
"provider": "openai",
"weight": 0.5,
"allowed_models": ["gpt-4o-mini"],
"key_ids": ["openai-primary"]
}
],
"team_id": "team-eng-001",
"rate_limit_id": "rate-limit-eng-vk"
}
],
"budgets": [
{
"id": "budget-eng-vk",
"virtual_key_id": "vk-001",
"max_limit": 100.00,
"reset_duration": "1M",
"current_usage": 0.0,
"last_reset": "2025-01-01T00:00:00Z"
}
],
"rate_limits": [
{
"id": "rate-limit-eng-vk",
"token_max_limit": 10000,
"token_reset_duration": "1h",
"token_current_usage": 0,
"request_max_limit": 100,
"request_reset_duration": "1m",
"request_current_usage": 0
}
]
}
}
Los campos clave de la virtual key en config.json son:
value: el string real de la virtual key.provider_configs: array de proveedores, cada uno conweightyallowed_models.key_ids: restringe a claves de proveedor específicas; usa["*"]para permitir todas.team_id/customer_id: mutuamente exclusivos.rate_limit_id: enlaza con la configuración de rate limiting.
La jerarquía de presupuestos
Aquí está el corazón del control de costo. Bifrost organiza los presupuestos en una jerarquía que va de lo más específico a lo más general. La idea es que cada petición debe pasar todos los presupuestos que apliquen, no solo uno.
flowchart TD
Cust["Customer<br/>budget propio"] --> Team["Team<br/>budget de departamento"]
Team --> VK["Virtual Key<br/>budget individual"]
VK --> PC["Provider Config<br/>budget por proveedor"]
classDef nivel fill:#1e293b,stroke:#38bdf8,color:#e2e8f0;
class Cust,Team,VK,PC nivel;
La estructura conceptual es Customer → Team → Virtual Key → Provider Config. Una virtual key puede colgar directamente de un Customer, de un Team, o funcionar de forma autónoma sin ninguno de los dos.
Cómo cascadean los presupuestos
La regla de oro la marca la propia documentación: “cada presupuesto debe tener saldo restante suficiente para que la petición proceda”. El sistema verifica los presupuestos secuencialmente. Si falla el del Provider Config, el de la VK, el del Team o el del Customer, la petición se bloquea por completo, no hay deducciones parciales.
Cuando la petición sí procede, “el costo se descuenta de todos los presupuestos aplicables: el mismo costo se aplica a cada nivel”. Es decir, una petición de $0.02 resta $0.02 al presupuesto de la VK, $0.02 al del Team y $0.02 al del Customer simultáneamente. Así cada nivel mantiene su propia visión del gasto acumulado.
Importante: el rate limiting aplica solo a nivel de virtual key. Los Teams y los Customers tienen presupuestos independientes pero no tienen rate limits propios.
Campos de un budget
Los campos del array budgets en config.json son:
id(string): identificador único.max_limit(float): tope de gasto en USD.reset_duration(string): periodo, p. ej."1d","1M","1Y".calendar_aligned(boolean, opcional): si estrue, resetea en los límites del periodo en UTC.virtual_key_id/provider_config_id(string): referencia al padre.
Un ejemplo de presupuesto a nivel de provider config:
{
"id": "budget-pc-openai",
"provider_config_id": 1,
"max_limit": 1000.00,
"reset_duration": "1M",
"calendar_aligned": true
}
Duraciones de reset
Los sufijos soportados para reset_duration son:
| Sufijo | Significado |
|---|---|
m | minuto |
h | hora |
d | día |
w | semana |
M | mes |
Y | año |
La alineación de calendario (calendar_aligned: true) requiere periodos de nivel día o mayor. Las duraciones sub-horarias no pueden usar calendar_aligned.
Rate limits: tokens y requests
Mientras los presupuestos controlan el dinero, los rate limits controlan la frecuencia. Bifrost soporta dos comprobaciones paralelas que deben pasar ambas:
token_max_limit/token_reset_duration: techo de tokens consumidos por periodo.request_max_limit/request_reset_duration: tope de cantidad de peticiones por periodo.
Un rate limit aislado en config.json se ve así:
{
"id": "rl-gpt4o",
"request_max_limit": 500,
"request_reset_duration": "1h",
"token_max_limit": 500000,
"token_reset_duration": "1h"
}
Esto se traduce en: máximo 500 peticiones por hora y máximo 500 000 tokens por hora. En cuanto se cruza cualquiera de los dos límites, el proveedor queda excluido del enrutamiento y el cliente recibe un 429.
{
"error": {
"type": "rate_limited",
"message": "Rate limits exceeded: [token limit exceeded (1500/1000, resets every 1h)]"
}
}
Recuerda que las duraciones admiten formatos como "1h" (por hora), "1d" (por día) y "1M" (por mes), igual que los presupuestos.
Model limits: qué modelos puede usar cada VK
Hay dos mecanismos complementarios para restringir modelos.
allowed_models en provider_configs
La forma directa de limitar modelos es la lista allowed_models dentro de cada provider_config de la virtual key. Las reglas de validación son explícitas:
allowed_models: ["*"]— permite todos los modelos soportados por el proveedor (valida contra el Model Catalog).allowed_modelsvacío ([]u omitido) — deny-all: niega todos los modelos.- Lista explícita — solo se permiten los modelos listados.
El sistema funciona con deny-by-default: una virtual key sin provider_configs bloquea todos los proveedores hasta que los agregas explícitamente. Esto es seguridad por diseño: nadie tiene acceso a algo que no le concediste.
Un detalle crítico de enrutamiento: el cruce entre proveedores no ocurre automáticamente. Una petición de gpt-4o no se enruta a Anthropic a menos que agregues explícitamente "gpt-4o" al allowed_models de Anthropic.
Model limits con scope
El segundo mecanismo son los model limits, que aplican topes de gasto y de rate a modelos concretos, con un alcance configurable. Sus campos son:
model_name: modelo específico, o"*"para todos.provider: filtro por proveedor (opcional).scope:"global"o"virtual_key".scope_id: requerido cuandoscopees"virtual_key".budget_ids: array de referencias a budgets.rate_limit_id: referencia a una entrada de rate limits.
Esto te permite, por ejemplo, ponerle un presupuesto global a gpt-4o independientemente de quién lo consuma, o ligar un rate limit específico al uso de un modelo caro dentro de una VK concreta. Todos los límites que coincidan se verifican de forma independiente durante el procesamiento de la petición, y “todos los presupuestos deben pasar para que una petición sea permitida”.
Códigos de error de governance
Cuando una política rechaza una petición, Bifrost devuelve un código HTTP que te dice exactamente qué falló:
| Código | Significado |
|---|---|
400 | Falta la virtual key (o falta un header requerido) |
402 | Presupuesto excedido |
403 | VK inactiva, o modelo/proveedor bloqueado |
429 | Rate limit o límite de tokens excedido |
Required headers: forzar atribución
A veces no basta con saber qué VK hizo la petición: necesitas metadata adicional para auditoría, aislamiento de tenants o ruteo. Para eso están los required headers (headers requeridos), que obligan a que ciertas cabeceras HTTP estén presentes en cada petición.
Sus casos de uso típicos:
- Aislamiento de tenant: identificar al tenant que llama mediante headers como
X-Tenant-ID. - Trazas de auditoría: rastrear peticiones entre servicios con
X-Correlation-ID. - Ruteo personalizado: soportar metadata dependiente de la infraestructura.
Se configuran con el campo required_headers dentro de la sección client del config.json:
{
"client": {
"required_headers": ["X-Tenant-ID", "X-Correlation-ID"]
}
}
El campo es de tipo string[], opcional, y el matcheo es case-insensitive. Si llega una petición sin alguno de esos headers, Bifrost responde con un 400 Bad Request antes de que la petición toque al proveedor:
{
"error": {
"message": "missing required headers: x-tenant-id, x-correlation-id",
"type": "missing_required_headers"
}
}
Un prerequisito importante: la validación de required headers requiere que governance esté habilitado. La comprobación aplica tanto a peticiones de inferencia LLM como a la ejecución de herramientas MCP que verás en el capítulo 10.
Governance routing: enrutamiento por petición
El enrutamiento por gobierno opera a través de la configuración de la virtual key y te da control granular sobre cómo se dirigen las peticiones. Ya viste los campos centrales (provider_configs, allowed_models, weight, key_ids), pero hay dos comportamientos que conviene entender bien: el balanceo por peso y los fallbacks automáticos.
Balanceo de carga por peso
Cuando varios proveedores tienen pesos numéricos, las peticiones se distribuyen proporcionalmente. Los pesos se normalizan automáticamente a una suma de 1.0 en base a los pesos de todos los proveedores disponibles.
{
"governance": {
"virtual_keys": [
{
"id": "vk-prod-main",
"provider_configs": [
{
"provider": "openai",
"allowed_models": ["gpt-4o", "gpt-4o-mini"],
"weight": 0.2
}
],
"key_ids": ["key-prod-001"]
}
]
}
}
Por ejemplo, si una petición de gpt-4o puede ir a Azure (peso 80%) o a OpenAI (peso 20%), y ambos soportan el modelo, el 80% va a Azure y el 20% a OpenAI. Si el modelo solo lo soporta un proveedor, el 100% va a ese. Un weight: null excluye al proveedor de la selección ponderada.
Fallbacks automáticos
Si tu petición no trae un array fallbacks propio en el cuerpo, governance los genera automáticamente: ordena los proveedores por peso (de mayor a menor) y los agrega como fallbacks. Esto te da resiliencia sin intervención manual, complementando lo que viste en el capítulo 7. Si especificas fallbacks manualmente, esos tienen prioridad sobre la generación automática.
El complexity router: enrutar por complejidad
Llegamos a la pieza más inteligente del gobierno. El complexity router clasifica automáticamente cada petición entrante en cuatro niveles de complejidad y expone el resultado como la variable complexity_tier, que puedes usar en reglas de enrutamiento. La meta es directa: mandar las consultas simples a modelos baratos y las complejas a modelos potentes, optimizando costo sin tocar el código de tu aplicación.
flowchart TD
Req["Peticion entrante"] --> Score["Scoring 0.0 - 1.0<br/>(5 dimensiones ponderadas)"]
Score --> Tier{"complexity_tier"}
Tier -->|< 0.15| S["SIMPLE → modelo barato<br/>(groq llama-3.1-8b)"]
Tier -->|0.15 - 0.35| M["MEDIUM → modelo intermedio<br/>(gpt-4o-mini)"]
Tier -->|0.35 - 0.60| C["COMPLEX → modelo capaz<br/>(claude-sonnet)"]
Tier -->|>= 0.60 o override| R["REASONING → modelo frontera<br/>(claude-opus)"]
El algoritmo de scoring
El sistema genera un puntaje de 0.0 a 1.0 a partir de cinco dimensiones ponderadas:
| Dimensión | Peso | Propósito |
|---|---|---|
| Presencia de código | 30% | Artefactos de programación, debugging |
| Marcadores de razonamiento | 25% | Lenguaje analítico de múltiples pasos |
| Términos técnicos | 25% | Jerga de infraestructura y operaciones |
| Conteo de tokens | 10% | Longitud del prompt (más largo = mayor) |
| Indicadores simples | −5% | Atenuador para saludos y consultas triviales |
Hay matices que vale conocer: el system prompt aporta un 25% de peso a las dimensiones puntuadas (contexto léxico suave, sin forzar el nivel); la mezcla por defecto de conversación es 60% del último mensaje + 40% del historial, que se ajusta a 35%/65% para follow-ups referenciales cortos como “hazlo” o “reintenta”; y existe un override de razonamiento: dos o más keywords de razonamiento (o una fuerte combinada con señales de código/técnicas) fuerzan el nivel Reasoning sin importar el puntaje numérico.
Configuración en config.json
La estructura completa va bajo governance.complexity_analyzer_config:
{
"governance": {
"complexity_analyzer_config": {
"tier_boundaries": {
"simple_medium": 0.15,
"medium_complex": 0.35,
"complex_reasoning": 0.60
},
"keywords": {
"code_keywords": ["function", "class", "api", "debug", "deploy"],
"reasoning_keywords": ["step by step", "explain why", "tradeoffs", "root cause analysis"],
"technical_keywords": ["architecture", "kubernetes", "latency", "authentication"],
"simple_keywords": ["hello", "hi", "thanks", "what is", "define"]
}
}
}
}
Especificaciones de los campos:
tier_boundaries.simple_medium,medium_complex,complex_reasoning: números entre 0 y 1, estrictamente crecientes.keywords.*: arrays de strings; cada lista requiere al menos una entrada. Los keywords se normalizan a minúsculas y se deduplican al guardar.- Hot-reload: los cambios aplican de inmediato, sin reinicio.
Cuando complexity_analyzer_config está presente en config.json, el modo split por defecto preserva las ediciones de UI y API entre reinicios mientras la sección del archivo no cambie. Si quieres que el archivo sea la fuente de verdad absoluta, agrega source_of_truth: "config.json".
Reglas de enrutamiento con CEL
La variable complexity_tier se usa en expresiones CEL con operadores estándar:
complexity_tier == "REASONING"
complexity_tier in ["COMPLEX", "REASONING"]
complexity_tier != "SIMPLE"
!(complexity_tier in ["SIMPLE", "MEDIUM"])
Y se combina con otras condiciones, como un header de tier premium o el nombre de un equipo:
headers["x-tier"] == "premium" && complexity_tier == "REASONING"
team_name == "ml-research" && complexity_tier == "REASONING"
Una escalera completa de cuatro niveles, que es el patrón clásico de optimización de costo, se define así:
[
{"cel_expression": "complexity_tier == \"SIMPLE\"", "targets": [{"provider": "groq", "model": "llama-3.1-8b-instant", "weight": 1}], "priority": 0},
{"cel_expression": "complexity_tier == \"MEDIUM\"", "targets": [{"provider": "openai", "model": "gpt-4o-mini", "weight": 1}], "priority": 1},
{"cel_expression": "complexity_tier == \"COMPLEX\"", "targets": [{"provider": "anthropic", "model": "claude-sonnet-4-5", "weight": 1}], "priority": 2},
{"cel_expression": "complexity_tier == \"REASONING\"", "targets": [{"provider": "anthropic", "model": "claude-opus-4-5", "weight": 1}], "priority": 3}
]
Para un despliegue conservador, puedes empezar con una sola regla que solo desvíe lo más complejo al modelo frontera y deje todo lo demás en su ruta normal:
{
"id": "complexity-reasoning",
"name": "Reasoning → Frontier model",
"cel_expression": "complexity_tier == \"REASONING\"",
"targets": [{"provider": "anthropic", "model": "claude-opus-4-5", "weight": 1}],
"priority": 0
}
Rendimiento y limitaciones
El impacto del router es mínimo: menos de 1 ms, porque el matcheo de keywords es pre-compilado, en proceso y sin llamadas externas. En los logs de enrutamiento verás líneas como Complexity: tier=REASONING score=0.38 words=25, y puedes depurar las decisiones en la Web UI bajo Routing Decision Logs.
Eso sí, el análisis de complejidad requiere texto de usuario analizable. No corre sobre generación de imágenes, embeddings, audio/video, peticiones de count-tokens, chat con multimedia mixto (texto + imágenes) ni peticiones que solo traen system/developer prompts. Cuando el análisis no está disponible, complexity_tier queda en "unknown" y las reglas CEL simplemente degradan en silencio (no matchean), sin romper el flujo.
Resumen
En este capítulo construiste el sistema de gobierno completo de Bifrost:
- Las virtual keys son la entidad central: claves lógicas
sk-bf-*que entregas a equipos y clientes sin exponer tus API keys reales, con control de acceso, costo, rate y restricción de claves. Se pasan en el headerx-bf-vk(oAuthorization,x-api-key,x-goog-api-key) y pertenecen a unteam_idO uncustomer_idde forma mutuamente exclusiva. - La jerarquía de presupuestos Customer → Team → Virtual Key → Provider Config exige que cada petición pase todos los budgets aplicables; el costo se descuenta en cada nivel y un solo fallo bloquea la petición entera.
- Los rate limits (
token_max_limit,request_max_limitcon susreset_duration) aplican solo a nivel de VK; los model limits yallowed_modelsrestringen qué modelos puede usar cada quien, con deny-by-default. - Los required headers (
client.required_headers) fuerzan cabeceras de atribución y devuelven400si faltan. - El governance routing balancea por peso y genera fallbacks automáticos, y el complexity router clasifica cada consulta en SIMPLE/MEDIUM/COMPLEX/REASONING para mandar lo simple a modelos baratos y lo complejo a modelos potentes mediante reglas CEL.
Con governance dominado, tienes control total sobre quién consume qué, cuánto gasta y a qué modelo llega. El siguiente paso es extender Bifrost más allá de la inferencia: darle herramientas a tus agentes.
Siguiente: MCP Gateway: herramientas para tus agentes