Go SDK: Bifrost embebido en tu aplicacion

Por: Artiko
bifrostgo-sdkgolangembebidolibreria

Go SDK: Bifrost embebido en tu aplicación

Durante todo el tutorial tratamos a Bifrost como un servicio independiente: un gateway HTTP que levantas con npx, Docker o un binario, al que tus aplicaciones le hablan por la red apuntando su base URL. En el capítulo 12 vimos cómo extender ese gateway con plugins. Pero Bifrost no nació como un servidor: nació como una librería Go. El gateway HTTP que has usado hasta ahora es, en realidad, una capa delgada construida sobre el paquete core. Eso significa que puedes saltarte el servidor por completo y embeber el mismo motor directamente dentro de tu aplicación Go.

En este capítulo aprenderás cuándo conviene esa estrategia, cómo inicializar un cliente Bifrost con tu propia implementación de cuenta, y cómo ejecutar chat completions, streaming, tool calling, context keys y logging sin tocar una sola petición HTTP. Al terminar entenderás que el gateway y el SDK son dos caras de la misma moneda, y sabrás elegir la correcta para cada caso.

¿Cuándo usar el Go SDK en vez del gateway HTTP?

El gateway HTTP es la opción correcta el 90% de las veces: lo despliegas una vez, lo comparten todos tus servicios (sin importar el lenguaje), centraliza governance y observabilidad, y lo actualizas sin recompilar nada. Pero hay escenarios donde embeber el core directamente en un proceso Go gana:

  • Latencia mínima. Sin gateway no hay salto de red local, ni serialización HTTP, ni un proceso intermedio. La petición va de tu código al motor de Bifrost mediante una llamada a función en memoria. Para cargas sensibles a la latencia, eliminar ese hop importa.
  • Embeber en un único servicio Go. Si toda tu inferencia vive dentro de un solo binario Go (un worker, un agente, una herramienta CLI), levantar un gateway aparte es infraestructura extra que no necesitas. El SDK te da el mismo motor sin un proceso adicional que vigilar.
  • Control total del ciclo de vida. Controlas la inicialización, el apagado (Shutdown), el logger, y puedes implementar la interfaz de cuenta para leer claves de tu propio secret manager, base de datos o sistema de configuración, en lugar de un config.json.

La contrapartida: pierdes la Web UI, el lenguaje queda atado a Go, y cada servicio que embeba el SDK gestiona su propia configuración. Es la diferencia entre un router compartido y una librería privada.

flowchart LR
    subgraph gw["Modo Gateway HTTP"]
        A1["App Python"] -->|HTTP| G["Bifrost Gateway"]
        A2["App Node"] -->|HTTP| G
        A3["App Go"] -->|HTTP| G
        G --> P1["Proveedores LLM"]
    end
    subgraph sdk["Modo Go SDK embebido"]
        B["App Go"] -->|llamada en memoria| C["core de Bifrost"]
        C --> P2["Proveedores LLM"]
    end

Una regla práctica: si necesitas que varios lenguajes o equipos compartan la misma capa de governance, usa el gateway (capítulos 09 y 11). Si todo tu camino caliente es Go y persigues latencia, embebe el SDK.

Instalación

El SDK vive en el paquete core. Lo instalas como cualquier dependencia Go:

go get github.com/maximhq/bifrost/core

En tu código importas dos paquetes: el core (que expone Init, el tipo Bifrost y helpers) y schemas, que contiene todos los tipos de request, response, configuración y constantes de proveedor.

import (
    "context"

    "github.com/maximhq/bifrost/core"
    "github.com/maximhq/bifrost/core/schemas"
)

Nota: el paquete core se referencia en el código con el identificador bifrost (es el nombre del paquete Go), por eso las llamadas se escriben bifrost.Init(...) aunque el import sea .../core.

La interfaz Account: configuración en código

En el gateway, los proveedores y sus claves se definen en config.json o por la Web UI (ver capítulo 03). En el SDK los defines implementando una interfaz llamada Account. Esto es deliberado: tu código es la fuente de la configuración, así que puedes leer las claves de variables de entorno, de Vault, de una base de datos o de donde quieras.

La interfaz requiere tres métodos:

type Account interface {
    GetConfiguredProviders() ([]schemas.ModelProvider, error)
    GetKeysForProvider(ctx *context.Context, provider schemas.ModelProvider) ([]schemas.Key, error)
    GetConfigForProvider(provider schemas.ModelProvider) (*schemas.ProviderConfig, error)
}

Cada método tiene una responsabilidad clara:

  • GetConfiguredProviders devuelve la lista de proveedores que tu cuenta soporta (schemas.OpenAI, schemas.Anthropic, schemas.Mistral, schemas.Groq, schemas.Cohere, schemas.Cerebras, etc.).
  • GetKeysForProvider entrega las claves API de cada proveedor. Devuelve un slice de schemas.Key, lo que te permite tener varias claves por proveedor con pesos distintos (la base del load balancing del capítulo 07).
  • GetConfigForProvider aporta la configuración de red y concurrencia: timeouts, reintentos, tamaño de buffer y workers.

Implementación realista

Aquí tienes una implementación completa y compilable conceptualmente. Lee las claves de variables de entorno y aplica las configuraciones por defecto de la librería:

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/maximhq/bifrost/core/schemas"
)

type MyAccount struct{}

func (a *MyAccount) GetConfiguredProviders() ([]schemas.ModelProvider, error) {
    return []schemas.ModelProvider{
        schemas.OpenAI,
        schemas.Anthropic,
        schemas.Mistral,
    }, nil
}

func (a *MyAccount) GetKeysForProvider(ctx *context.Context, provider schemas.ModelProvider) ([]schemas.Key, error) {
    switch provider {
    case schemas.OpenAI:
        return []schemas.Key{{
            Value:  os.Getenv("OPENAI_API_KEY"),
            Models: []string{},
            Weight: 1.0,
        }}, nil
    case schemas.Anthropic:
        return []schemas.Key{{
            Value:  os.Getenv("ANTHROPIC_API_KEY"),
            Models: []string{},
            Weight: 1.0,
        }}, nil
    case schemas.Mistral:
        return []schemas.Key{{
            Value:  os.Getenv("MISTRAL_API_KEY"),
            Models: []string{},
            Weight: 1.0,
        }}, nil
    }
    return nil, fmt.Errorf("provider %s not supported", provider)
}

func (a *MyAccount) GetConfigForProvider(provider schemas.ModelProvider) (*schemas.ProviderConfig, error) {
    return &schemas.ProviderConfig{
        NetworkConfig:            schemas.DefaultNetworkConfig,
        ConcurrencyAndBufferSize: schemas.DefaultConcurrencyAndBufferSize,
    }, nil
}

Sobre los campos de schemas.Key:

  • Value es la clave API (un string). En el ejemplo la leemos con os.Getenv. Usa siempre variables de entorno o un secret manager; nunca incrustes la clave en el código fuente.
  • Models restringe la clave a una lista de modelos. Un slice vacío ([]string{}) significa que la clave sirve para cualquier modelo del proveedor.
  • Weight controla el reparto de tráfico cuando hay varias claves del mismo proveedor (load balancing).

Ajustar red y concurrencia

schemas.DefaultNetworkConfig y schemas.DefaultConcurrencyAndBufferSize cubren la mayoría de los casos, pero puedes construir la configuración a mano cuando necesites afinar timeouts, reintentos o el tamaño del pool de workers. El NetworkConfig expone, entre otros campos: BaseURL (útil para apuntar a un endpoint compatible o un modelo local), MaxRetries, RetryBackoffInitial, RetryBackoffMax y ExtraHeaders. El ConcurrencyAndBufferSize define MaxConcurrency (workers concurrentes por proveedor) y BufferSize (tamaño de la cola interna):

func (a *MyAccount) GetConfigForProvider(provider schemas.ModelProvider) (*schemas.ProviderConfig, error) {
    return &schemas.ProviderConfig{
        NetworkConfig: schemas.NetworkConfig{
            MaxRetries:          3,
            RetryBackoffInitial: 100 * time.Millisecond,
            RetryBackoffMax:     5 * time.Second,
        },
        ConcurrencyAndBufferSize: schemas.ConcurrencyAndBufferSize{
            MaxConcurrency: 10,
            BufferSize:     100,
        },
    }, nil
}

Estos MaxRetries y backoff son la versión embebida de la resiliencia que configurabas en el gateway en el capítulo 07.

Inicializar el cliente Bifrost

Con la cuenta lista, inicializas el motor con bifrost.Init. Recibe un schemas.BifrostConfig con tu cuenta (y opcionalmente un logger, que veremos más adelante) y devuelve un cliente *bifrost.Bifrost. Acuérdate de llamar a Shutdown con defer para liberar los pools de workers de forma ordenada:

func main() {
    client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
        Account: &MyAccount{},
    })
    if initErr != nil {
        log.Fatalf("error inicializando Bifrost: %v", initErr)
    }
    defer client.Shutdown()

    // ... usar client para hacer peticiones
}

A partir de aquí, client es el único objeto que necesitas: todas las operaciones de inferencia son métodos sobre él.

Tu primer ChatCompletion en Go

El método central es ChatCompletionRequest. Recibe un contexto de Bifrost y un *schemas.BifrostChatRequest, y devuelve la respuesta junto a un error tipado de Bifrost.

El contexto se crea con schemas.NewBifrostContext, envolviendo un context.Context estándar. schemas.NoDeadline indica que no quieres imponer un deadline propio (Bifrost gestiona sus timeouts internos):

response, err := client.ChatCompletionRequest(
    schemas.NewBifrostContext(context.Background(), schemas.NoDeadline),
    &schemas.BifrostChatRequest{
        Provider: schemas.OpenAI,
        Model:    "gpt-4o-mini",
        Input: []schemas.ChatMessage{
            {
                Role: schemas.ChatMessageRoleUser,
                Content: &schemas.ChatMessageContent{
                    ContentStr: schemas.Ptr("Explica que es un AI gateway en una frase."),
                },
            },
        },
    },
)
if err != nil {
    log.Printf("la peticion fallo: %v", err)
    return
}

fmt.Println(*response.Choices[0].Message.Content.ContentStr)

Fíjate en varios detalles importantes:

  • Provider y Model son campos del request, no parte de la URL como en el gateway. Cambiar de proveedor es cambiar una constante.
  • Input es un slice de schemas.ChatMessage. Cada mensaje tiene un Role (schemas.ChatMessageRoleUser, ...RoleSystem, ...RoleAssistant) y un Content.
  • Content es un puntero a schemas.ChatMessageContent. Para texto plano usas el campo ContentStr, que es un *string. El helper schemas.Ptr(...) convierte un valor en un puntero de forma concisa (Go no permite tomar la dirección de un literal directamente).
  • La respuesta se lee navegando response.Choices[0].Message.Content.ContentStr, igual estructura que devolvería el gateway en JSON, pero como structs Go tipados.

Manejo de errores: BifrostError

Cuando algo falla (clave inválida, proveedor caído, rate limit, modelo inexistente), Bifrost no devuelve un error genérico opaco: devuelve un error estructurado, BifrostError, que transporta el mensaje, el tipo de error y, cuando aplica, el código de estado del proveedor subyacente. Esto te permite ramificar tu lógica según la causa real del fallo:

response, err := client.ChatCompletionRequest(bfCtx, req)
if err != nil {
    // err es un *schemas.BifrostError con informacion estructurada
    log.Printf("Bifrost error: %v", err)
    // Aqui puedes inspeccionar el tipo de error para decidir si
    // reintentar, hacer fallback manual o devolver al usuario.
    return
}

Como viste en el capítulo 07, Bifrost ya aplica reintentos y fallbacks de forma interna según tu configuración; el BifrostError solo aflora cuando todas esas estrategias se agotaron.

Streaming en Go

Para respuestas token a token usas ChatCompletionStreamRequest. La firma es casi idéntica a la versión no-streaming, pero en lugar de una respuesta única devuelve un canal Go del que recibes chunks a medida que el modelo genera. Consumir un stream es, idiomáticamente, un for chunk := range stream:

stream, err := client.ChatCompletionStreamRequest(
    schemas.NewBifrostContext(context.Background(), schemas.NoDeadline),
    &schemas.BifrostChatRequest{
        Provider: schemas.OpenAI,
        Model:    "gpt-4o-mini",
        Input:    messages,
    },
)
if err != nil {
    log.Printf("la peticion de streaming fallo: %v", err)
    return
}

for chunk := range stream {
    if chunk.BifrostError != nil {
        log.Printf("error en el stream: %v", chunk.BifrostError)
        break
    }

    if chunk.BifrostChatResponse != nil && len(chunk.BifrostChatResponse.Choices) > 0 {
        choice := chunk.BifrostChatResponse.Choices[0]
        if choice.ChatStreamResponseChoice != nil &&
            choice.ChatStreamResponseChoice.Delta != nil &&
            choice.ChatStreamResponseChoice.Delta.Content != nil {
            content := *choice.ChatStreamResponseChoice.Delta.Content
            fmt.Print(content)
        }
    }
}

Tres cosas que conviene entender del modelo de streaming:

  • El error viaja dentro del chunk. A diferencia del request normal, donde el error es el segundo valor de retorno, en streaming cada chunk lleva su propio BifrostError. Comprueba chunk.BifrostError != nil en cada iteración y rompe el bucle si aparece.
  • El contenido llega en el Delta. Cada chunk parcial trae su texto en choice.ChatStreamResponseChoice.Delta.Content (un *string). Acumulas esos fragmentos para reconstruir la respuesta completa.
  • El último chunk es especial. Bifrost estandariza todos los streams para enviar el usage (tokens) y el finish reason solo en el último chunk; el contenido viaja en los chunks anteriores. Si tu código necesita el conteo de tokens, lo encontrarás al final, no repartido.
sequenceDiagram
    participant App as App Go
    participant B as core Bifrost
    participant P as Proveedor LLM
    App->>B: ChatCompletionStreamRequest
    B->>P: stream upstream
    P-->>B: delta "Un"
    B-->>App: chunk (Delta.Content="Un")
    P-->>B: delta " gateway"
    B-->>App: chunk (Delta.Content=" gateway")
    P-->>B: fin + usage
    B-->>App: ultimo chunk (usage + finish_reason)
    Note over App,B: el canal se cierra y el for...range termina

Cuando el modelo termina, Bifrost cierra el canal y el for ... range finaliza solo. No necesitas señal de cierre adicional.

Tool calling en Go

El tool calling embebido funciona igual que en el gateway (capítulo 06), pero declarando las herramientas como structs Go. Una herramienta es un schemas.ChatTool de tipo función, con su nombre, descripción y un esquema de parámetros estilo JSON Schema:

calculatorTool := schemas.ChatTool{
    Type: schemas.ChatToolTypeFunction,
    Function: &schemas.ChatToolFunction{
        Name:        "calculator",
        Description: schemas.Ptr("Una herramienta de calculadora"),
        Parameters: &schemas.ToolFunctionParameters{
            Type: "object",
            Properties: map[string]interface{}{
                "operation": map[string]interface{}{
                    "type":        "string",
                    "description": "La operacion a realizar",
                    "enum":        []string{"add", "subtract", "multiply", "divide"},
                },
                "a": map[string]interface{}{
                    "type":        "number",
                    "description": "El primer numero",
                },
                "b": map[string]interface{}{
                    "type":        "number",
                    "description": "El segundo numero",
                },
            },
            Required: []string{"operation", "a", "b"},
        },
    },
}

Las herramientas se pasan en Params.Tools, y con Params.ToolChoice controlas si el modelo decide por su cuenta ("auto"), si lo fuerzas a una función concreta o si las desactivas ("none"):

response, err := client.ChatCompletionRequest(
    schemas.NewBifrostContext(context.Background(), schemas.NoDeadline),
    &schemas.BifrostChatRequest{
        Provider: schemas.OpenAI,
        Model:    "gpt-4o-mini",
        Input: []schemas.ChatMessage{
            {
                Role: schemas.ChatMessageRoleUser,
                Content: &schemas.ChatMessageContent{
                    ContentStr: schemas.Ptr("¿Cuanto es 2+2? Usa la herramienta calculator."),
                },
            },
        },
        Params: &schemas.ChatParameters{
            Tools: []schemas.ChatTool{calculatorTool},
            ToolChoice: &schemas.ChatToolChoice{
                ChatToolChoiceStr: schemas.Ptr("auto"),
            },
        },
    },
)
if err != nil {
    log.Printf("la peticion fallo: %v", err)
    return
}

Cuando el modelo decide invocar una herramienta, la respuesta no trae texto sino una lista de llamadas en Message.ChatAssistantMessage.ToolCalls. Cada llamada incluye el ID, el nombre de la función y los argumentos como JSON:

assistant := response.Choices[0].Message.ChatAssistantMessage
if assistant != nil && assistant.ToolCalls != nil {
    for _, toolCall := range assistant.ToolCalls {
        fmt.Printf("Llamada a herramienta - %s: %s\n", *toolCall.ID, *toolCall.Function.Name)
        fmt.Printf("Argumentos - %s\n", toolCall.Function.Arguments)
    }
}

El flujo completo de un agente es el clásico de dos pasos: ejecutas la función en tu código con esos argumentos, añades el resultado al historial como un mensaje de rol tool, y vuelves a llamar a ChatCompletionRequest para que el modelo redacte la respuesta final.

Recuerda: si tus herramientas viven en servidores MCP, no necesitas declararlas a mano. El capítulo 10 cubre cómo Bifrost descubre e inyecta herramientas MCP automáticamente; en el SDK las activas con context keys, como verás a continuación.

Context keys: metadata por contexto

Las context keys son el mecanismo del SDK para pasar metadata y configuración por petición sin ensuciar el BifrostChatRequest. Se aprovechan plugins, governance y observabilidad, exactamente lo que en el gateway viajaba en headers HTTP. Las estableces sobre el BifrostContext con SetValue antes de hacer la petición:

bfCtx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
bfCtx.SetValue(schemas.BifrostContextKeyRequestID, "req-001")

response, err := client.ChatCompletionRequest(bfCtx, &schemas.BifrostChatRequest{
    Provider: schemas.OpenAI,
    Model:    "gpt-4o-mini",
    Input:    messages,
})

Las claves están tipadas como constantes del paquete schemas. Estas son algunas de las más útiles, agrupadas por propósito:

Trazabilidad y headers personalizados (alimentan logs y plugins, ver capítulo 11):

bfCtx.SetValue(schemas.BifrostContextKeyRequestID, "req-12345-abc")
bfCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, map[string][]string{
    "x-correlation-id": {"corr-12345"},
    "x-tenant-id":      {"tenant-abc"},
})

Governance: seleccionar una virtual key (presupuestos y límites del capítulo 09):

bfCtx.SetValue(schemas.BifrostContextKeyVirtualKey, "vk-my-team")

Selección explícita de clave API por id o por nombre, útil cuando tienes varias claves del mismo proveedor:

bfCtx.SetValue(schemas.BifrostContextKeyAPIKeyName, "premium-key")

Herramientas MCP que deben incluirse en esta petición concreta:

bfCtx.SetValue(schemas.MCPContextKeyIncludeClients, []string{"github", "filesystem"})
bfCtx.SetValue(schemas.MCPContextKeyIncludeTools, []string{"filesystem-*"})

Algunas claves no se establecen, sino que se leen después de la petición para inspeccionar qué ocurrió internamente: qué clave se eligió, cuántos reintentos hubo o qué índice de fallback se usó. Son la versión programática de la telemetría del gateway:

retries := ctx.Value(schemas.BifrostContextKeyNumberOfRetries).(int)
fallbackIdx := ctx.Value(schemas.BifrostContextKeyFallbackIndex).(int)

También puedes pedir que la respuesta incluya el request y response crudos del proveedor (útil para depurar), activando las flags correspondientes y leyéndolas en ExtraFields:

bfCtx.SetValue(schemas.BifrostContextKeySendBackRawResponse, true)

response, _ := client.ChatCompletionRequest(bfCtx, req)
if response.ChatResponse != nil {
    extra := response.ChatResponse.ExtraFields
    fmt.Printf("Proveedor: %s\n", extra.Provider)
    fmt.Printf("Latencia: %dms\n", extra.Latency)
}

La gran ventaja: las context keys son el puente entre tu código y los plugins del capítulo 12. Un plugin de governance, por ejemplo, lee la virtual key del contexto para aplicar el presupuesto del equipo correcto.

Logging: configurar el logger

Por defecto Bifrost trae un logger integrado, pero puedes ajustar su nivel, su formato de salida y, sobre todo, sustituirlo por el tuyo. La interfaz Logger que Bifrost espera es esta:

type Logger interface {
    Debug(msg string, args ...any)
    Info(msg string, args ...any)
    Warn(msg string, args ...any)
    Error(msg string, args ...any)
    Fatal(msg string, args ...any)
    SetLevel(level schemas.LogLevel)
    SetOutputType(outputType schemas.LoggerOutputType)
}

Para la mayoría de los casos basta con el logger por defecto. Lo creas con NewDefaultLogger, le pasas un nivel inicial, y opcionalmente cambias el formato a JSON (ideal para que tus herramientas de observabilidad lo parseen):

logger := bifrost.NewDefaultLogger(schemas.LogLevelInfo)
logger.SetOutputType(schemas.LoggerOutputTypeJSON)
logger.SetLevel(schemas.LogLevelDebug)

Los niveles disponibles van de más a menos verboso: schemas.LogLevelDebug, schemas.LogLevelInfo, schemas.LogLevelWarn y schemas.LogLevelError.

El logger se inyecta en la inicialización, junto a la cuenta:

client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
    Account: &MyAccount{},
    Logger:  logger,
})

Como Logger es una interfaz, puedes envolver tu logger favorito (por ejemplo Zap o slog) implementando esos métodos, y así unificar los logs de Bifrost con los del resto de tu aplicación. Si quieres silenciar Bifrost por completo, implementa un NoOpLogger cuyos métodos no hagan nada:

type NoOpLogger struct{}

func (l *NoOpLogger) Debug(msg string, args ...any)                  {}
func (l *NoOpLogger) Info(msg string, args ...any)                   {}
func (l *NoOpLogger) Warn(msg string, args ...any)                   {}
func (l *NoOpLogger) Error(msg string, args ...any)                  {}
func (l *NoOpLogger) Fatal(msg string, args ...any)                  {}
func (l *NoOpLogger) SetLevel(level schemas.LogLevel)                {}
func (l *NoOpLogger) SetOutputType(t schemas.LoggerOutputType)       {}

Gateway y SDK: la misma máquina

Llegados aquí conviene cerrar el círculo. Todo lo que has hecho en este tutorial sobre el gateway HTTP (proveedores, fallbacks, caching semántico, governance, MCP, plugins, observabilidad) se ejecuta sobre este mismo paquete core. El gateway es, literalmente, un binario que crea una Account a partir de tu config.json, llama a bifrost.Init y expone los métodos por HTTP. Cuando embebes el SDK, tu main ocupa el lugar de ese binario.

Esto tiene una consecuencia práctica: las funcionalidades de resiliencia y enrutamiento son del motor, no del servidor. Reintentos, load balancing por Weight y selección de claves funcionan igual embebidos. Lo que es exclusivo del gateway es la capa de presentación: la Web UI, la API HTTP unificada y el drop-in replacement por base URL (capítulo 05). Y las capacidades marcadas como Enterprise a lo largo del tutorial (clustering, SSO, RBAC avanzado) siguen siendo Enterprise: el SDK te da el motor, no licencia funcionalidades de pago.

Resumen

En este capítulo cambiaste de perspectiva: dejaste de hablarle a Bifrost por HTTP y lo embebiste como librería Go dentro de tu proceso.

  • El Go SDK (go get github.com/maximhq/bifrost/core) es el motor sobre el que se construye el gateway. Conviene cuando buscas latencia mínima, un único servicio Go o control total del ciclo de vida y la configuración.
  • La configuración se hace en código implementando la interfaz Account con GetConfiguredProviders, GetKeysForProvider y GetConfigForProvider, lo que te permite leer claves de tu propio secret manager.
  • Inicializas con bifrost.Init pasando un BifrostConfig, y cierras con client.Shutdown().
  • ChatCompletionRequest devuelve la respuesta y un error tipado BifrostError con información estructurada del fallo.
  • ChatCompletionStreamRequest devuelve un canal que recorres con for chunk := range; el contenido llega en Delta.Content y el usage solo en el último chunk.
  • El tool calling declara herramientas como schemas.ChatTool y se controla con Params.ToolChoice; las llamadas se leen en Message.ChatAssistantMessage.ToolCalls.
  • Las context keys (bfCtx.SetValue) pasan metadata por petición (request id, virtual key, headers, herramientas MCP) y conectan tu código con plugins y governance.
  • El logger se configura con NewDefaultLogger o sustituyéndolo por una implementación propia de la interfaz Logger, y se inyecta en Init.

Con el SDK dominado, tienes las dos formas de operar Bifrost. Solo queda llevarlo a producción con garantías y saber qué abre la puerta Enterprise.

Siguiente: Despliegue en producción y camino a Enterprise