Plugins y extensibilidad
Plugins y extensibilidad
En el capítulo 11 vimos cómo Bifrost te entrega observabilidad nativa: logs en tiempo real, métricas Prometheus y trazas OpenTelemetry. Quizá no lo notaste, pero ese sistema de logging es en realidad un plugin más. Y aquí está la idea clave de este capítulo: casi todo lo “mágico” que hace Bifrost —governance, semantic caching, telemetría— no está cableado en el núcleo, sino implementado como plugins que se enganchan alrededor de cada llamada al proveedor.
Eso es enorme. Significa que el mismo mecanismo que usa Maxim AI para construir las features oficiales está disponible para ti: puedes interceptar cada request antes de que salga al proveedor, inspeccionar o transformar cada respuesta antes de que vuelva al cliente, e incluso cortocircuitar la llamada por completo (devolver una respuesta sintética sin tocar al proveedor). En este capítulo desmenuzamos esa arquitectura de middleware, recorremos los plugins integrados, simulamos respuestas con el plugin mocker para tests, escribimos nuestro primer plugin en Go y, finalmente, compilamos un binario dinámico de Bifrost capaz de cargarlo.
Qué es un plugin en Bifrost
Un plugin es middleware: código que se ejecuta antes y después de la llamada al proveedor, con la posibilidad de modificar, registrar, validar o enriquecer los datos que fluyen por el sistema. Es el mismo patrón conceptual que un middleware de Express o un interceptor de Axios, pero aplicado al pipeline de inferencia de un AI gateway.
Bifrost implementa los plugins con el sistema de plugins nativo de Go: se compilan como archivos de objeto compartido (.so) y se cargan en tiempo de ejecución. Esto trae cuatro ventajas frente a un modelo basado en procesos externos:
- Integración nativa con Go — acceso completo al sistema de tipos de Bifrost (sus
schemas), sin serialización intermedia. - Carga dinámica — los plugins se cargan sin recompilar el binario base.
- Type safety — el compilador de Go verifica que las firmas de tus hooks coincidan.
- Rendimiento — al correr en el mismo proceso, no hay overhead de comunicación entre procesos (IPC). La documentación cita un overhead de 1-10 µs por plugin para operaciones simples.
WASM está deprecado. Versiones tempranas de Bifrost exploraron plugins en WebAssembly (WASM) para portabilidad. Ese enfoque quedó deprecado en favor de los plugins en Go, que ofrecen acceso de tipos nativo y mejor rendimiento. Si encuentras tutoriales viejos sobre plugins WASM, ignóralos: el camino soportado hoy es Go.
El modelo simétrico pre/post
El corazón de la arquitectura es un modelo de hooks pre/post simétrico alrededor de la llamada al proveedor. Los hooks “pre” se ejecutan hacia delante en el orden de registro; los hooks “post” se ejecutan en orden inverso (patrón LIFO, last-in first-out), como el desenrollado de una pila. Esto garantiza que cada plugin “cierre” lo que abrió en el orden correcto, igual que los defer anidados de Go.
flowchart LR
Cliente([Cliente]) --> PA[Plugin A · PreLLMHook]
PA --> PB[Plugin B · PreLLMHook]
PB --> PC[Plugin C · PreLLMHook]
PC --> Prov{{Llamada al proveedor LLM}}
Prov --> QC[Plugin C · PostLLMHook]
QC --> QB[Plugin B · PostLLMHook]
QB --> QA[Plugin A · PostLLMHook]
QA --> Respuesta([Respuesta al cliente])
PB -.->|short-circuit| QB
classDef pre fill:#1e3a5f,stroke:#4a90d9,color:#fff
classDef post fill:#2d4a22,stroke:#6abf4b,color:#fff
classDef prov fill:#5a3a1e,stroke:#d9904a,color:#fff
class PA,PB,PC pre
class QA,QB,QC post
class Prov prov
Fíjate en la flecha punteada del diagrama: el Plugin B puede cortocircuitar la cadena. Si devuelve un short-circuit, la llamada al proveedor y los plugins posteriores en la fase “pre” se saltan, pero —y esto es importante— todos los PreHooks que ya se ejecutaron reciben igualmente su PostHook correspondiente. La simetría se respeta siempre.
Las fases del ciclo de vida
La documentación describe cinco estados del ciclo de vida de un plugin:
- Discovery — Bifrost encuentra y cataloga los plugins (vía CLI, entorno, config o escaneo de directorio).
- Loading — verificación de firma del binario y validación de checksum.
- Initialization — se llama a
Init(config)con un timeout acotado. - Runtime — procesamiento activo de requests con monitoreo de salud.
- Cleanup — liberación de recursos al apagar el gateway, vía
Cleanup().
Los hooks: dónde se engancha tu plugin
A partir de la versión 1.5.x, Bifrost expone varios puntos de enganche. No necesitas implementarlos todos: cada hook es opcional, y omitir uno simplemente desactiva ese punto de intercepción. Estos son los principales:
| Hook | Cuándo se ejecuta | Puede cortocircuitar |
|---|---|---|
PreRequestHook | Una sola vez por request, en la fase de enrutamiento (elige proveedor/modelo y fallbacks) | No (decisiones de routing) |
PreLLMHook | Por cada intento de proveedor, justo antes de la llamada | Sí (LLMPluginShortCircuit) |
PostLLMHook | Por cada intento de proveedor, justo después (orden inverso) | Puede recuperar errores |
HTTPTransportPreHook | A nivel de transporte HTTP, antes de procesar | Sí |
HTTPTransportPostHook | A nivel de transporte HTTP, después (orden inverso) | — |
HTTPTransportStreamChunkHook | Por cada chunk de una respuesta en streaming | — |
PreMCPHook / PostMCPHook | Alrededor de las llamadas al MCP Gateway | Sí |
Una distinción crucial: PreRequestHook corre exactamente una vez por request, mientras que PreLLMHook y PostLLMHook corren una vez por cada intento de proveedor. Esto importa cuando hay fallbacks: si el proveedor primario falla y Bifrost reintenta con el secundario, los hooks PreLLMHook/PostLLMHook se vuelven a disparar, pero PreRequestHook no.
El orden de ejecución completo
Poniendo todo junto, el flujo de un request a través de la maquinaria de hooks queda así:
sequenceDiagram
participant C as Cliente
participant HTTP as HTTPTransportPreHook
participant Route as PreRequestHook
participant LLM as PreLLMHook
participant P as Proveedor
participant LLMp as PostLLMHook
participant HTTPp as HTTPTransportPostHook
C->>HTTP: request entrante
HTTP->>Route: (1 vez) decisiones de routing
Route->>LLM: por cada intento de proveedor
LLM->>P: llamada al LLM (o short-circuit)
P-->>LLMp: respuesta o error
LLMp-->>HTTPp: orden inverso (LIFO)
HTTPp-->>C: respuesta final
Recuperación de errores y short-circuit
Dos mecanismos hacen que los plugins sean potentes de verdad, no solo “observadores pasivos”:
El short-circuit. Un PreLLMHook puede devolver un *schemas.LLMPluginShortCircuit que contiene una respuesta sintética. Cuando lo hace, Bifrost se salta la llamada al proveedor y usa esa respuesta. Esto es exactamente lo que hace el plugin de semantic caching: si encuentra un hit en caché, cortocircuita y devuelve la respuesta cacheada sin gastar un solo token. El short-circuit tiene dos variantes según el campo AllowFallbacks:
AllowFallbacks=true(onil): ante un error, Bifrost intentará con el proveedor de fallback.AllowFallbacks=false: se salta los fallbacks y devuelve directo al cliente.
La recuperación de errores. Un PostLLMHook recibe tanto la respuesta como el *schemas.BifrostError. Puede convertir un error en una respuesta exitosa: por ejemplo, transformar un fallo del proveedor en una respuesta de fallback construida por tu plugin. Esto te da un punto centralizado para degradar con elegancia.
Por seguridad, los fallos de un plugin en la fase PreRequestHook son no bloqueantes: se registran como warnings y el pipeline continúa. Además, Bifrost aplica recuperación de panics y aislamiento de ejecución para que un plugin defectuoso no tumbe el gateway.
Los plugins integrados
Bifrost se construye sobre su propia maquinaria de plugins. Conocerlos te ayuda a entender qué hace el gateway por debajo y a decidir cuándo necesitas escribir uno propio:
| Plugin | Para qué sirve |
|---|---|
| governance | Aplica las reglas de governance: virtual keys, presupuestos, rate limits, reglas de routing CEL y load balancing ponderado. Corre en la fase de routing. |
| semanticcache | Implementa el semantic caching: busca respuestas similares por significado y, si hay hit, cortocircuita la llamada al proveedor. |
| telemetry | Expone métricas de Prometheus y alimenta la observabilidad (latencias, tokens, conteos por proveedor/modelo). |
| logging | Registra cada request y respuesta para el monitoreo en tiempo real de la Web UI. |
| mocker | Simula respuestas de proveedor para testing y desarrollo, sin tocar APIs reales (lo vemos en detalle abajo). |
| jsonparser | Procesa y valida payloads JSON dentro del pipeline. |
| maxim | Integra con la plataforma de Maxim AI para tracing y evaluación de calidad de las respuestas. |
Estos plugins viven en el grupo builtin del sistema de secuenciamiento, que veremos más adelante. Tus plugins se colocan antes (pre_builtin) o después (post_builtin) de ese grupo.
El plugin mocker: simular proveedores para tests
El plugin mocker es el favorito de cualquiera que escriba tests. Su trabajo es mockear respuestas de proveedores de IA para testing, desarrollo y simulación: intercepta los requests y devuelve respuestas simuladas sin hacer llamadas reales (ni gastar tokens, ni depender de la red). Es ideal para tests de integración deterministas, demos offline y simulación de errores.
Configuración base
El plugin usa un objeto MockerConfig. Su esqueleto mínimo:
{
"Enabled": true,
"DefaultBehavior": "passthrough",
"GlobalLatency": null,
"Rules": []
}
El campo DefaultBehavior admite tres valores: "passthrough" (deja pasar el request al proveedor real si ninguna regla coincide), "success" o "error".
Definir reglas de mock
Cada entrada del array Rules describe una regla con condiciones de coincidencia y respuestas:
{
"Name": "rule-identifier",
"Enabled": true,
"Priority": 0,
"Probability": 1.0,
"Conditions": {},
"Responses": [],
"Latency": null
}
Las condiciones filtran a qué requests aplica la regla. Puedes matchear por proveedor, modelo, una regex sobre el mensaje o el tamaño del request:
{
"Conditions": {
"Providers": ["openai", "anthropic"],
"Models": ["gpt-4", "claude-3"],
"MessageRegex": "(?i).*hello.*",
"RequestSize": {
"Min": 100,
"Max": 1000
}
}
}
Las respuestas (Responses) son objetos ponderados por Weight. Una respuesta exitosa permite contenido estático o un template con variables:
{
"Type": "success",
"Weight": 0.8,
"Content": {
"Message": "Static response text",
"MessageTemplate": "Hello from {{provider}} using {{model}}",
"Model": "gpt-4",
"Usage": {
"PromptTokens": 15,
"CompletionTokens": 25,
"TotalTokens": 40
},
"FinishReason": "stop",
"CustomFields": {}
}
}
Y una respuesta de error simula un fallo del proveedor —perfecto para probar tus fallbacks y retries:
{
"Type": "error",
"Weight": 0.2,
"Error": {
"Message": "Rate limit exceeded",
"Type": "rate_limit",
"Code": "429",
"StatusCode": 429
},
"AllowFallbacks": false
}
Simular latencia
Para que tus tests reflejen condiciones realistas, puedes inyectar retardos. Latencia fija:
{
"Latency": {
"Type": "fixed",
"Min": "250ms"
}
}
O latencia variable (uniforme entre dos cotas):
{
"Latency": {
"Type": "uniform",
"Min": "100ms",
"Max": "500ms"
}
}
Variables de template y bypass
El contenido dinámico admite las variables {{provider}}, {{model}} y {{faker.*}} para generar datos sintéticos (nombres, emails, UUIDs, etc.). Y si necesitas que un request concreto no sea mockeado, basta con poner en el contexto la clave schemas.BifrostContextKey("skip-mocker") en true: ese request pasará de largo al proveedor real.
Combina
DefaultBehavior: "success"con una regla deType: "error"yProbabilitybaja para inyectar fallos aleatorios y validar la resiliencia de tu pipeline bajo condiciones adversas.
Escribir un plugin en Go
Llegó la parte divertida: tu propio plugin. Todo plugin debe exportar tres funciones obligatorias —Init, GetName y Cleanup— y cualquier subconjunto de los hooks opcionales. Las firmas exactas (a partir de v1.5.x) son:
func Init(config any) error
func GetName() string
func Cleanup() error
Los hooks de LLM tienen estas firmas:
func PreLLMHook(ctx *schemas.BifrostContext,
req *schemas.BifrostRequest) (
*schemas.BifrostRequest,
*schemas.LLMPluginShortCircuit,
error)
func PostLLMHook(ctx *schemas.BifrostContext,
resp *schemas.BifrostResponse,
bifrostErr *schemas.BifrostError) (
*schemas.BifrostResponse,
*schemas.BifrostError,
error)
Ejemplo mínimo completo
Este es un plugin “Hello World” funcional, tomado de la documentación oficial. Registra un mensaje en cada hook y deja pasar request y respuesta sin modificarlos:
package main
import (
"fmt"
"github.com/maximhq/bifrost/core/schemas"
)
func Init(config any) error {
fmt.Println("Init called")
return nil
}
func GetName() string {
return "Hello World Plugin"
}
func PreLLMHook(ctx *schemas.BifrostContext,
req *schemas.BifrostRequest) (
*schemas.BifrostRequest,
*schemas.LLMPluginShortCircuit,
error) {
ctx.Log(schemas.LogLevelInfo, "PreLLMHook called")
return req, nil, nil
}
func PostLLMHook(ctx *schemas.BifrostContext,
resp *schemas.BifrostResponse,
bifrostErr *schemas.BifrostError) (
*schemas.BifrostResponse,
*schemas.BifrostError,
error) {
ctx.Log(schemas.LogLevelInfo, "PostLLMHook called")
return resp, bifrostErr, nil
}
func Cleanup() error {
fmt.Println("Cleanup called")
return nil
}
Anatomía de lo que devuelve cada hook:
PreLLMHookretorna(request, shortCircuit, error). Si devuelves elrequest(posiblemente modificado) ynilen el short-circuit, la llamada continúa al proveedor. Si devuelves un*LLMPluginShortCircuitno nulo, Bifrost se salta la llamada y usa esa respuesta sintética.PostLLMHookretorna(response, bifrostErr, error). Aquí puedes transformar la respuesta o recuperar un error convirtiéndolo en una respuesta válida.
El parámetro ctx *schemas.BifrostContext es tu navaja suiza: contexto con alcance de request para loggear (ctx.Log) y para almacenar/leer valores que viajan entre hooks.
Los tipos clave de schemas
Conviene tener claro qué representa cada tipo que aparece en las firmas:
| Tipo | Qué representa |
|---|---|
BifrostContext | Contexto con alcance de request: logging y almacenamiento de valores. |
BifrostRequest | El request LLM entrante (modelo, proveedor, mensajes, parámetros). |
BifrostResponse | La respuesta del proveedor (completions, conteos de tokens). |
BifrostError | Error estructurado con status code, tipo y mensaje. |
LLMPluginShortCircuit | Contiene la respuesta sintética para saltarse la llamada al proveedor. |
BifrostStreamChunk | Wrapper tipado de un chunk de respuesta en streaming. |
Compilar el plugin
Una vez escrito, lo compilas como objeto compartido con el build mode de plugin de Go:
go build -buildmode=plugin -o my-plugin.so main.go
Y lo registras en config.json dentro del array plugins:
{
"plugins": [
{
"enabled": true,
"name": "my-plugin",
"path": "/path/to/my-plugin.so",
"config": { "api_key": "value" }
}
]
}
El campo config se pasa tal cual al Init(config any) de tu plugin, así que ahí defines tus parámetros (claves, umbrales, lo que necesites).
Reglas de compatibilidad (importantes)
El sistema de plugins de Go es potente pero estricto. Si no respetas estas reglas, el plugin no cargará:
- Solo Linux y macOS. El plugin mode de Go no está soportado en Windows.
- Sin cross-compilation. Debes compilar el plugin en la misma plataforma de destino.
- Misma arquitectura (amd64 o arm64) entre plugin y Bifrost.
- Misma versión de Go para ambos. La documentación referencia Go 1.26.1.
- Misma libc. Un binario compilado con
musl(Alpine) no correrá en un sistemaglibc(Debian) y viceversa.
Tu go.mod debe declarar la versión de Go y la dependencia del core de Bifrost:
module github.com/example/my-plugin
go 1.26.1
require github.com/maximhq/bifrost/core v1.2.38
Plugin sequencing: el orden importa
Cuando tienes varios plugins, el orden de ejecución deja de ser un detalle y pasa a ser parte del diseño. Bifrost organiza los plugins en tres grupos de colocación (placement groups) que se ejecutan en secuencia:
flowchart LR
subgraph Pre["pre_builtin (1º request / último response)"]
direction TB
A[auth-validator · order 0]
B[request-enricher · order 1]
end
subgraph Builtin["builtin (governance, cache, telemetry...)"]
BB[plugins integrados]
end
subgraph Post["post_builtin (último request / 1º response)"]
direction TB
C[response-logger · order 0]
D[analytics · order 1]
end
Pre --> Builtin --> Post
classDef pre fill:#1e3a5f,stroke:#4a90d9,color:#fff
classDef bi fill:#5a3a1e,stroke:#d9904a,color:#fff
classDef post fill:#2d4a22,stroke:#6abf4b,color:#fff
class A,B pre
class BB bi
class C,D post
Las reglas son:
- En la fase de request (PreHooks), el orden es
pre_builtin → builtin → post_builtin. - En la fase de response (PostHooks), el orden se invierte (LIFO):
post_builtin → builtin → pre_builtin. Como dice la documentación: “un pluginpre_builtinejecuta suPreLLMHookprimero, pero suPostLLMHookúltimo.” - Dentro de cada grupo, los plugins se ordenan por el campo
order(menor = antes). Si dos compartenorder, se preserva el orden de registro.
Esta tabla muestra una secuencia concreta de ejecución, donde se ve la simetría LIFO entre pre-hooks y post-hooks:
| Plugin | Placement | Order | Pre-hook | Post-hook |
|---|---|---|---|---|
| auth-validator | pre_builtin | 0 | 1º | 5º |
| request-enricher | pre_builtin | 1 | 2º | 4º |
| (integrados) | builtin | — | 3º | 3º |
| response-logger | post_builtin | 0 | 4º | 2º |
| analytics | post_builtin | 1 | 5º | 1º |
Configurar el orden
En config.json defines placement y order por plugin:
{
"plugins": [
{
"name": "auth-validator",
"enabled": true,
"path": "/plugins/auth-validator.so",
"placement": "pre_builtin",
"order": 0
},
{
"name": "request-enricher",
"enabled": true,
"path": "/plugins/request-enricher.so",
"placement": "pre_builtin",
"order": 1
},
{
"name": "response-logger",
"enabled": true,
"path": "/plugins/response-logger.so",
"placement": "post_builtin",
"order": 0
}
]
}
También puedes reordenar en caliente vía la API, con un PUT:
curl -X PUT http://localhost:8080/api/plugins/my-plugin \
-H "Content-Type: application/json" \
-d '{
"placement": "pre_builtin",
"order": 0
}'
¿Qué placement elegir?
- Usa
pre_builtincuando necesites validar/autenticar el request antes de cualquier procesamiento, enriquecerlo con metadata que los plugins integrados deban ver, o cortocircuitar antes incluso de los chequeos de governance. - Usa
post_builtin(el valor por defecto) para transformar la respuesta tras el procesamiento integrado, loggear/hacer analytics sobre el estado final o añadir headers personalizados.
“Cuando tengas dudas, usa el placement por defecto
post_builtin.” — documentación oficial.
Building dynamic binary: compilar Bifrost con tus plugins
Hay un detalle que sorprende a muchos: el binario estándar de Bifrost no puede cargar plugins. El sistema de plugins de Go exige enlazado dinámico para cargar archivos .so en tiempo de ejecución, pero los builds que distribuye Bifrost suelen ser estáticos. Por eso, para usar plugins custom necesitas compilar un binario dinámico de Bifrost.
Compilar el binario dinámico
Con un go build directo, la clave es omitir los flags de enlazado estático (-extldflags '-static' y -tags "sqlite_static"):
go build \
-ldflags="-w -s -X main.Version=v1.3.30" \
-o bifrost-http
Si el repo trae Makefile, es más cómodo el flag DYNAMIC=1, que quita automáticamente los flags estáticos manteniendo CGO_ENABLED=1:
make build DYNAMIC=1
make build DYNAMIC=1 VERSION=1.3.30
make build DYNAMIC=1 GOOS=linux GOARCH=amd64
make build DYNAMIC=1 GOOS=linux GOARCH=arm64
Verificar que el binario es dinámico
Antes de cargar nada, confirma que el binario realmente está enlazado dinámicamente con ldd:
ldd tmp/bifrost-http
La salida debe mostrar dependencias sobre libc.musl-x86_64.so.1 (Alpine) o libc.so.6 (Debian). Si ldd dice “not a dynamic executable”, construiste un estático y el plugin no cargará.
Compilar el plugin con la libc correcta
Recuerda la regla de la libc: el plugin y Bifrost deben compartir versión de libc. Lo más seguro es compilar el plugin dentro del mismo entorno que usarás para correr Bifrost. Para Alpine/musl:
docker run --rm \
-v "$PWD:/work" \
-w /work \
golang:1.26.1-alpine3.23 \
sh -c "apk add --no-cache gcc musl-dev && \
go build -buildmode=plugin -o myplugin.so main.go"
Para Debian/glibc:
docker run --rm \
-v "$PWD:/work" \
-w /work \
golang:1.26.1-bookworm \
sh -c "apt-get update && apt-get install -y gcc && \
go build -buildmode=plugin -o myplugin.so main.go"
Las variables de entorno relevantes en estos builds son CGO_ENABLED=1 (requerido para SQLite y para el soporte de plugins), GOOS=linux y GOARCH=amd64 o arm64.
Apuntar al plugin compilado
Finalmente, en config.json indicas la ruta del .so ya compilado:
{
"plugins": [
{
"path": "/app/data/plugins/myplugin.so",
"config": {}
}
]
}
Arranca el binario dinámico de Bifrost apuntando a esa configuración y, en el log de inicio, deberías ver el mensaje de tu Init (“Init called” en nuestro ejemplo) confirmando que el plugin se cargó.
flowchart TD
G[Compilar Bifrost dinamico<br/>make build DYNAMIC=1] --> V{ldd confirma<br/>dynamic executable?}
V -- No --> G
V -- Si --> P[Compilar plugin<br/>go build -buildmode=plugin]
P --> M[Misma libc, arch y<br/>version de Go?]
M -- No --> P
M -- Si --> R[Registrar .so en config.json<br/>plugins path]
R --> S[Arrancar Bifrost<br/> ver Init called en el log]
classDef step fill:#1e3a5f,stroke:#4a90d9,color:#fff
classDef check fill:#5a3a1e,stroke:#d9904a,color:#fff
class G,P,R,S step
class V,M check
Resumen
Los plugins son el mecanismo de extensibilidad de Bifrost y la base sobre la que están construidas sus propias features. Lo esencial que te llevas:
- Un plugin es middleware con un modelo pre/post simétrico alrededor de la llamada al proveedor: los PreHooks corren hacia delante y los PostHooks en orden inverso (LIFO). Se compilan como
.soy se cargan en tiempo de ejecución con el sistema de plugins nativo de Go. WASM está deprecado en favor de Go. - Los hooks principales son
PreRequestHook(1 vez por request, routing),PreLLMHook/PostLLMHook(1 vez por intento de proveedor) y los hooks de transporte HTTP y MCP. UnPreLLMHookpuede cortocircuitar la llamada con unLLMPluginShortCircuit; unPostLLMHookpuede recuperar errores. - Bifrost trae plugins integrados: governance, semanticcache, telemetry, logging, mocker, jsonparser y maxim. El mocker simula respuestas de proveedor (success, error y latencia) para tests deterministas sin tocar APIs reales.
- Escribir un plugin en Go requiere exportar
Init,GetName,Cleanupy los hooks que necesites; compilarlo congo build -buildmode=pluginy registrarlo en el arraypluginsdeconfig.json. - El secuenciamiento se controla con
placement(pre_builtin,builtin,post_builtin) yorder. Para cargar plugins custom debes compilar un binario dinámico de Bifrost (make build DYNAMIC=1), respetando misma arquitectura, versión de Go y libc entre plugin y gateway.
Hasta aquí hemos usado Bifrost como un servicio HTTP independiente. En el próximo capítulo damos un giro: lo embebemos dentro de tu propia aplicación Go como una librería, usando el mismo sistema de tipos (schemas) y middleware que acabas de conocer.
Siguiente: Go SDK: Bifrost embebido en tu aplicación