Configuración: Web UI, config.json y variables de entorno

Por: Artiko
bifrostconfiguracionconfig-jsonenvsecretos

Configuración: Web UI, config.json y variables de entorno

En el capítulo anterior levantaste Bifrost y enviaste tu primer request a través del gateway. Para llegar ahí probablemente pegaste una API key directamente o la configuraste a las apuradas desde el dashboard. Eso está bien para un primer contacto, pero un AI gateway que va a vivir en producción necesita una configuración ordenada, reproducible y, sobre todo, que no tenga secretos hardcodeados en ningún archivo versionado.

En este capítulo vas a entender las tres formas en que Bifrost se configura, cuándo conviene cada una, cómo está estructurado el archivo config.json, cómo referenciar variables de entorno para que tus claves nunca queden en texto plano, dónde guarda Bifrost su estado, y el modelo de fuente de verdad que decide quién gana cuando el archivo y la base de datos no coinciden. Es un capítulo conceptual y denso, pero es el que te va a evitar dolores de cabeza en cada capítulo siguiente.

Las tres formas de configurar Bifrost

Bifrost no te obliga a elegir una única manera de configurarlo. Expone tres superficies de configuración que conviven, y la gracia está en usar la adecuada para cada situación.

flowchart TD
    subgraph Fuentes["Tres superficies de configuracion"]
        UI["Web UI / Dashboard<br/>visual, exploratorio"]
        API["API HTTP<br/>runtime dinamico, automatizable"]
        FILE["config.json<br/>declarativo, versionable"]
    end
    UI --> STORE[("config_store<br/>SQLite por defecto")]
    API --> STORE
    FILE -. "seed + drift detection<br/>en el arranque" .-> STORE
    STORE --> RUNTIME["Estado en runtime<br/>de Bifrost"]

1. Web UI (configuración visual)

El dashboard de Bifrost es una interfaz web pensada para explorar y configurar a mano. Desde ahí agregas proveedores, pegas claves, defines virtual keys, activas plugins y revisas logs. Es la forma ideal cuando:

Los cambios hechos por la UI se persisten en la base de datos de configuración (el config_store, que veremos más abajo) y toman efecto inmediatamente, sin reinicio.

2. API HTTP (runtime dinámico)

Todo lo que hace la UI lo hace por debajo contra la API HTTP del gateway. Eso significa que puedes automatizar la configuración: scripts de provisioning, pipelines de CI/CD que registran una virtual key nueva, o un sistema interno que ajusta presupuestos de forma programática. Es la forma ideal cuando:

Al igual que la UI, los cambios por API se escriben en el config_store y aplican al instante.

3. Archivo config.json (declarativo)

El config.json es la configuración declarativa y versionable. Lo defines en disco, lo subes a tu repositorio (sin secretos, claro) y describe el estado deseado del gateway: proveedores, cliente, governance, stores, plugins. Es la forma ideal cuando:

A diferencia de UI y API, el config.json se lee una sola vez al arrancar. Cualquier cambio en el archivo requiere reiniciar Bifrost para que lo tome.

Regla práctica: usa config.json para definir la base reproducible de cada entorno, la API para automatizar cambios en caliente, y la UI para explorar y para ajustes manuales puntuales. No son excluyentes; están pensadas para convivir, y la sección de fuente de verdad explica exactamente cómo.

Estructura de config.json

El config.json está organizado en secciones de primer nivel. Cada una gobierna un aspecto del gateway. Estas son las secciones del esquema, según la referencia oficial:

SeccionPara que sirve
$schemaURL del esquema para validación y autocompletado en tu IDE
versionModo de compatibilidad para la semántica de los arrays
source_of_truthEstrategia de reconciliación con la base de datos
encryption_keyClave AES-256 para cifrar secretos en el store
clientPool de workers, logging, CORS, auth, timeouts de MCP
providersClaves de los proveedores LLM y ajustes de red
governanceAuth de admin, presupuestos, rate limits, routing
config_storeBackend de base de datos para la configuración
logs_storeBackend para los logs de request/response
vector_storeBase vectorial para el semantic cache
pluginsMódulos opcionales de funcionalidad
frameworkCatálogo de precios de modelos
mcpConfiguración de herramientas MCP
websocketAjuste fino de la Realtime API

Algunas secciones son exclusivas de Enterprise y no están disponibles en el open source: guardrails_config (moderación de contenido), access_profiles (plantillas RBAC) y cluster_config (clustering multi-nodo). Las menciono para que las reconozcas en la referencia, pero no las usaremos en este tutorial salvo en el capítulo final.

Un config.json realista y comentado

Aquí tienes un config.json completo que toca las secciones principales. Fíjate en que ningún secreto está escrito en texto plano: todos usan la referencia env.* que veremos en la siguiente sección.

{
  "$schema": "https://www.getbifrost.ai/schema",
  "version": 2,
  "source_of_truth": "split",
  "encryption_key": "env.BIFROST_ENCRYPTION_KEY",

  "client": {
    "initial_pool_size": 500,
    "drop_excess_requests": false,
    "enable_logging": true,
    "disable_content_logging": false,
    "log_retention_days": 365,
    "logging_headers": ["user-id", "organization"],
    "enforce_auth_on_inference": true,
    "allowed_origins": ["https://app.example.com"],
    "allow_direct_keys": false,
    "max_request_body_size_mb": 100,
    "mcp_agent_depth": 10,
    "mcp_tool_execution_timeout": 30,
    "async_job_result_ttl": 3600
  },

  "providers": {
    "openai": {
      "keys": [
        {
          "name": "primary",
          "value": "env.OPENAI_API_KEY",
          "models": ["gpt-4o", "gpt-4-turbo"],
          "weight": 1.0
        }
      ],
      "network_config": {
        "default_request_timeout_in_seconds": 60,
        "max_retries": 3
      },
      "concurrency_and_buffer_size": {
        "concurrency": 100,
        "buffer_size": 1000
      }
    },
    "anthropic": {
      "keys": [
        {
          "name": "main",
          "value": "env.ANTHROPIC_API_KEY",
          "models": ["claude-3-opus", "claude-3-sonnet"],
          "weight": 0.8
        }
      ]
    }
  },

  "governance": {
    "auth_config": [
      { "username": "admin", "password": "env.ADMIN_PASSWORD" }
    ]
  },

  "config_store": {
    "enabled": true,
    "type": "sqlite",
    "config": { "path": "./config.db" }
  },

  "logs_store": {
    "enabled": true,
    "type": "sqlite",
    "config": { "path": "./logs.db" }
  },

  "plugins": [
    { "name": "semantic_cache", "enabled": true }
  ]
}

No te abrumes con cada campo: los detalles de providers se cubren en Proveedores, los de governance en Governance, y los de plugins en Plugins y extensibilidad. En este capítulo nos centramos en client, los stores y source_of_truth.

El campo $schema apunta a la URL del esquema oficial; agregarlo te da validación y autocompletado en editores como VS Code. El campo version controla la semántica de compatibilidad de los arrays entre versiones de Bifrost.

Variables de entorno: nunca hardcodees secretos

La prioridad número uno de cualquier configuración es no exponer secretos. Bifrost resuelve esto con un patrón simple: en cualquier campo que acepte un valor sensible puedes poner el string env.NOMBRE_DE_LA_VARIABLE en lugar del valor literal, y Bifrost lo sustituye en el arranque por el contenido de esa variable de entorno.

{
  "providers": {
    "openai": {
      "keys": [
        { "name": "primary", "value": "env.OPENAI_API_KEY", "models": ["gpt-4o"], "weight": 1.0 }
      ]
    }
  }
}

En este ejemplo, value no es la clave en sí: es la referencia env.OPENAI_API_KEY. Cuando Bifrost arranca, busca la variable de entorno OPENAI_API_KEY y la inyecta. Así tu config.json queda limpio para versionar en git: contiene la forma de la configuración, pero no los secretos.

Defines las variables como cualquier variable de entorno del sistema:

export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export BIFROST_ENCRYPTION_KEY="una-clave-larga-y-aleatoria"
export ADMIN_PASSWORD="un-password-fuerte"

O, si despliegas con Docker, pasándolas al contenedor:

docker run -p 8080:8080 \
  -e OPENAI_API_KEY="sk-..." \
  -e ANTHROPIC_API_KEY="sk-ant-..." \
  -e BIFROST_ENCRYPTION_KEY="una-clave-larga-y-aleatoria" \
  -v "$(pwd):/app/data" \
  maximhq/bifrost

El patrón env.* funciona en múltiples campos: claves de proveedores, credenciales de base de datos, password de admin, clave de cifrado, secretos de virtual keys y demás. Un detalle útil: la encryption_key admite tanto el prefijo env.VAR como leerse directamente desde la variable BIFROST_ENCRYPTION_KEY. Esa clave cifra los secretos que quedan persistidos en el config_store con AES-256, de modo que ni siquiera la base de datos los guarda en claro.

Configuración del cliente (client config)

La sección client agrupa el comportamiento global del gateway: cuánta concurrencia maneja, cómo hace logging, qué políticas de CORS y auth aplica, y los timeouts de las herramientas MCP. Estos son los campos más relevantes con sus valores por defecto:

Pool de conexiones

CampoTipoDefaultSignificado
initial_pool_sizeintegerconfigurable (p. ej. 500)Goroutines worker preasignadas por cola de proveedor
drop_excess_requestsbooleanfalseDescarta requests cuando la cola está llena, en vez de esperar

El initial_pool_size es la palanca principal de rendimiento bruto: define cuántos workers tiene cada cola de proveedor listos de antemano. Es un parámetro configurable (valor de ejemplo: 500). Con drop_excess_requests: true, ante una cola saturada Bifrost prefiere fallar rápido en vez de acumular latencia; útil cuando es preferible un error inmediato a una espera larga.

Logging de request y response

CampoTipoDefaultSignificado
enable_loggingbooleanRegistra todos los requests y responses LLM
disable_content_loggingbooleanfalseElimina el contenido de los mensajes del log (deja solo metadata)
log_retention_daysinteger365Días que se retienen las entradas en el store
logging_headersarray[]Headers HTTP del request a capturar en la metadata del log

disable_content_logging es clave para privacidad: si manejas datos sensibles, lo activas para guardar metadata (modelo, tokens, latencia) sin guardar el contenido de los prompts. Lo veremos a fondo en Observabilidad.

Seguridad y CORS

CampoTipoDefaultSignificado
allowed_originsarray["*"]Orígenes permitidos para CORS (URIs o "*")
enforce_auth_on_inferencebooleanfalseExige auth en las rutas de inferencia /v1/*
max_request_body_size_mbinteger100Tamaño máximo del body del request en MB
whitelisted_routesarray[]Rutas que saltan el middleware de auth
allowed_headersarray[]Headers adicionales permitidos para CORS y WebSocket
allow_direct_keysbooleanfalsePermite usar claves de proveedor crudas saltando el pool registrado

allow_direct_keys merece atención: con false (el default seguro), los clientes solo pueden usar las claves registradas en Bifrost. Si lo pones en true, un cliente podría mandar su propia API key de proveedor en el request y saltarse el pool gestionado. Déjalo en false salvo que tengas un caso muy concreto.

Timeouts de MCP y jobs asíncronos

CampoTipoDefaultSignificado
mcp_agent_depthinteger10Profundidad máxima de recursión de tool-calls en modo agente MCP
mcp_tool_execution_timeoutinteger30Timeout por ejecución de herramienta MCP, en segundos
mcp_tool_sync_intervalinteger10Intervalo de sincronización de tools, en minutos (0 = desactivado)
async_job_result_ttlinteger3600TTL en segundos para los resultados de jobs asíncronos

Estos parámetros gobiernan el MCP Gateway: cuán profundo puede encadenar llamadas a herramientas un agente, y cuánto espera Bifrost por cada ejecución antes de cortar.

Importante sobre los retries: el client no define una política global de reintentos. Los max_retries y los default_request_timeout_in_seconds se configuran por proveedor, dentro de network_config (lo viste en el ejemplo de arriba). La resiliencia avanzada (retries, fallbacks y load balancing) tiene su propio capítulo: Resiliencia.

Storage: dónde Bifrost guarda su estado

Bifrost mantiene dos categorías de datos completamente separadas, cada una con su propio store y su propio backend configurable:

  1. Config (config_store): configuraciones de proveedores, virtual keys, reglas de governance. Es el estado operativo del gateway.
  2. Logs (logs_store): los registros de request/response que ves en la UI.
flowchart LR
    BIFROST["Bifrost gateway"]
    BIFROST --> CS[("config_store<br/>config.db")]
    BIFROST --> LS[("logs_store<br/>logs.db")]
    CS -.-> CSALT["SQLite (default)<br/>PostgreSQL"]
    LS -.-> LSALT["SQLite (default)<br/>PostgreSQL<br/>+ S3/GCS para offload"]

SQLite por defecto

Si omites la sección config_store, Bifrost crea automáticamente un store SQLite en el directorio de la aplicación. Lo mismo aplica al logs_store. La configuración por defecto equivale a:

{
  "config_store": {
    "enabled": true,
    "type": "sqlite",
    "config": { "path": "./config.db" }
  },
  "logs_store": {
    "enabled": true,
    "type": "sqlite",
    "config": { "path": "./logs.db" }
  }
}

Las rutas pueden ser relativas al directorio de la app o absolutas:

{ "config_store": { "type": "sqlite", "config": { "path": "/var/lib/bifrost/config.db" } } }

SQLite es perfecto para empezar, para desarrollo y para deploys de un solo nodo. Cuando montas Bifrost en Docker, monta un volumen para que config.db y logs.db persistan entre reinicios del contenedor.

PostgreSQL para producción

Para alta disponibilidad o múltiples instancias compartiendo estado, cambia el backend a PostgreSQL. Fíjate de nuevo en cómo las credenciales usan env.*:

{
  "config_store": {
    "enabled": true,
    "type": "postgres",
    "config": {
      "host": "env.PG_HOST",
      "port": "5432",
      "user": "env.PG_USER",
      "password": "env.PG_PASSWORD",
      "db_name": "bifrost",
      "ssl_mode": "require",
      "max_idle_conns": 5,
      "max_open_conns": 50
    }
  }
}

Tanto config_store como logs_store soportan SQLite y PostgreSQL. El vector_store (para el semantic cache) usa backends distintos como Weaviate, Redis/Valkey, Qdrant o Pinecone, no SQLite ni Postgres.

Offload de logs a object storage

Los logs crecen rápido. El logs_store admite descargar los registros a almacenamiento de objetos (S3, GCS o MinIO) para no inflar la base de datos:

{
  "object_storage": {
    "type": "s3",
    "bucket": "env.S3_BUCKET",
    "prefix": "bifrost",
    "compress": true,
    "region": "us-east-1",
    "access_key_id": "env.S3_ACCESS_KEY_ID",
    "secret_access_key": "env.S3_SECRET_ACCESS_KEY"
  }
}

Con compress: true los registros se comprimen antes de subirse, y las credenciales pueden venir de env.* o de roles IAM.

Source of truth: quién gana

Llegamos al concepto que más confusión genera y, a la vez, el más importante: si configuro algo en config.json, lo cambio luego desde la UI, y reinicio el gateway… ¿qué valor queda? La respuesta depende del campo source_of_truth.

Recuerda la asimetría fundamental: el config.json se lee una sola vez en el arranque, no se vigila de forma continua. La UI y la API, en cambio, escriben directamente en el config_store, que es la fuente operativa en runtime. La pregunta es cómo se reconcilian ambos en cada arranque.

Modo split (por defecto)

Con source_of_truth: "split" (el comportamiento por defecto), Bifrost trata el config.json como un mecanismo de bootstrap y detección de drift:

Es decir: tus ediciones por UI/API “se pegan” y persisten entre reinicios, salvo que toques esa misma sección en el config.json, en cuyo caso el archivo manda para esa sección.

Modo authoritative (config.json)

Con source_of_truth: "config.json", el archivo gana siempre en el arranque. Bifrost aplica los valores del archivo incluso si el config_hash almacenado coincide, sobreescribiendo explícitamente cualquier cambio previo hecho en la base de datos. Es el modo “infraestructura como código pura”: la verdad vive en el archivo y la UI queda como ventana de solo lectura para esa configuración.

Modo file-only

Si pones config_store.enabled: false, el archivo se carga en memoria al arrancar y se convierte en la única configuración de runtime. No hay base de datos de configuración, así que cualquier cambio requiere editar el archivo y reiniciar. Es el modo más simple y más estricto.

flowchart TD
    START["Arranque de Bifrost"] --> CHECK{"source_of_truth?"}
    CHECK -->|"split (default)"| SPLIT{"¿Cambio la seccion<br/>en config.json?"}
    SPLIT -->|"No"| DB["Gana la base de datos<br/>(ediciones UI/API persisten)"]
    SPLIT -->|"Si"| FILE1["Gana el archivo<br/>para esa seccion"]
    CHECK -->|"config.json"| FILE2["Gana el archivo siempre<br/>(sobreescribe la DB)"]
    CHECK -->|"config_store.enabled: false"| ONLY["File-only:<br/>el archivo es la unica fuente"]

¿Cuál elegir?

Autenticación del gateway: proteger la UI y la API

Por defecto, el dashboard y la API de administración de Bifrost no están protegidos. En cualquier despliegue más allá de tu máquina local debes activar la autenticación, porque quien acceda a la API de admin puede leer y modificar toda la configuración, incluidas las claves.

La autenticación del gateway se configura desde el dashboard, en Workspace → Config → pestaña Security:

Cómo se autentican los clientes

Una vez activada la protección, hay dos formas de autenticar:

Basic Auth (ideal para llamadas a la API):

curl -X POST http://localhost:8080/v1/chat/completions \
  -u "your-username:your-password" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello!"}]}'

Bearer Token (lo usa el dashboard): se emite tras el login, se guarda automáticamente en el localStorage del navegador y la sesión dura 30 días.

Si prefieres declarar las credenciales en el archivo en vez de en la UI, recuerda que la sección governance.auth_config admite definir el username y el password (este último vía env.*), tal como viste en el config.json de ejemplo. La gestión fina de quién puede usar qué modelo y con qué presupuesto pertenece a las virtual keys, que tienen su propio capítulo en Governance.

{
  "governance": {
    "auth_config": [
      { "username": "admin", "password": "env.ADMIN_PASSWORD" }
    ]
  }
}

Buena práctica de seguridad: en producción, activa siempre la protección por password, define allowed_origins con tus dominios reales (no "*"), considera enforce_auth_on_inference: true y deja allow_direct_keys: false. Nunca pongas el dashboard de Bifrost expuesto a internet sin autenticación.

Resumen

Siguiente: Proveedores: claves, aliasing y modelos locales