Status

active. Реализовано в cmd/bifrost-with-plugins/main.go. Open thread: re-enable через runtime API сломан как побочный эффект — описан ниже в “Следствия”.

Контекст

bifrost — наш LLM gateway. Стейдж крутит upstream maximhq/bifrost:latest. Чтобы добавить свой плагин (см. bifrost-mock-plugins) у bifrost есть один документированный путь — компилировать как .so shared object и указать path в config.json. Этот путь работает, но коробит:

  • .so плагин должен быть собран против точно той же версии github.com/maximhq/bifrost/core что и runtime. Любая разница даже в transitive dependency на patch-версию ломает loading. Цитата из upstream docs: «Even if only one transitive dependency differs by a patch version, the plugin will fail to load.»
  • Каждый bump bifrost-core требует пересборки всех .so. Это сцепление на rebuild-cadence upstream’а с нашим
  • Cgo, gccgo и Go shared-object plugins имеют известные ABI-проблемы (plugin: not implemented на некоторых runtime’ах)

Рассматривали

.so через PluginLoader. Стандартный путь. Brittle по причинам выше.

enabled: false в config + SyncLoadedPlugin enable вручную. Чтобы избежать ошибки unknown built-in plugin, помечаем плагин disabled в config.json. Bifrost тогда silently пропускает Bootstrap-инстанциацию, а мы вручную регистрируем после. Минус — переписывает intent юзера в config; человек, читающий config.json, видит «disabled» хотя по факту плагин активен. Plus runtime API будет рассогласован с config.

Custom PluginLoader до Bootstrap. Идея: реализовать свой PluginLoader который знает наши имена и возвращает наши плагины из in-memory registry. Минус — Config.PluginLoader ставится внутри Bootstrap(), заинжектить до — нельзя. Можно после, но Bootstrap уже отработал к этому моменту.

Fork upstream main.go + статический import + SyncLoadedPlugin. В transports/bifrost-http/server/server.go:974 есть exported метод SyncLoadedPlugin(ctx, name, plugin, placement, order). Он принимает уже инстанцированный schemas.BasePlugin и вкатывает его в plugin registry, обходя PluginLoader целиком. Метод используется в upstream’е для reload-flow. Не документирован для external custom plugins, но exported и стабилен.

Выбрали

Fork upstream main.go + SyncLoadedPlugin после Bootstrap.

Реализация (упрощённо):

// cmd/bifrost-with-plugins/main.go
func registerCustomPlugins(ctx context.Context) error {
    for _, cfg := range server.Config.PluginConfigs {
        switch cfg.Name {
        case schemamocker.PluginName:
            plugin, _ := schemamocker.New(decodePluginConfig[schemamocker.Config](cfg.Config))
            return server.SyncLoadedPlugin(ctx, cfg.Name, plugin, cfg.Placement, cfg.Order)
        case fhirpost.PluginName:
            // ...
        }
    }
}
 
func main() {
    server.Bootstrap(ctx)         // ← reads config, тщетно ищет наши плагины как built-in
    registerCustomPlugins(ctx)    // ← injects свои
    server.Start()
}

Почему

  • Нет ABI-coupling. Наш плагин compiled внутри того же binary что и core — версии сходятся by construction
  • Нет .so-дисциплины. go build собирает один статический бинарь. Распространяется как Docker image
  • Использует public API. SyncLoadedPlugin exported, не используем reflection или unsafe приёмы
  • Композируется с upstream UI/API. Bifrost после регистрации трактует наш плагин как обычный — он появляется в /api/plugins, в дашборде, отвечает на PUT /api/plugins/<name> для disable. См. open в “Следствия” про re-enable
  • Стабильность. Forking upstream main — это ~50 строк Go. Когда апстрим обновляет main (новые init-этапы) — сравниваем diff и переносим. Cheaper чем переcoborka .so

Следствия

Bootstrap логирует noisy unknown built-in plugin: <name> перед нашей регистрацией. Bifrost iterates PluginConfigs, для каждого enabled:true без Path вызывает loadBuiltinPlugin(name), который не знает наших имён → Error. Status потом перебивается на active нашим SyncLoadedPlugin, но log-line остаётся. Решено через filtering logger в cmd/bifrost-with-plugins/logger.go который дропает specific Error/Warn substrings. Лёгкий drift — при добавлении 3-го плагина нужно обновлять silenceFragments.

Re-enable через runtime API сломан (PUT /api/plugins/<name> {enabled:true} после disable). Bifrost reload-flow вызывает loadBuiltinPlugin, не находит, оставляет status: error. enabled: false через API работает (one-way). Workaround — kubectl rollout restart контейнера, наш registerCustomPlugins после Bootstrap ставит плагин обратно. Чистая починка — реализовать custom PluginLoader который знает наши имена и заменить server.Config.PluginLoader = ... после Bootstrap (там уже всё заинициализировано). ~50 строк Go, не реализовано.

UI bake требует Node-stage в Dockerfile. Bifrost UI это React/Vite app которая упаковывается в upstream-binary через //go:embed all:ui поверх transports/bifrost-http/ui/ build-output. Когда мы заменяем upstream-binary своим — UI исчезает. Multi-stage Dockerfile клонит bifrost@core/v1.5.8, делает npm run build, копирует output в cmd/bifrost-with-plugins/ui/, наш Go-stage embeds. Local dev — make build-ui (scripts/build-ui.sh).

Bifrost-core version coupling. В go.mod пинится github.com/maximhq/bifrost/core@v1.5.8. Bump core требует:

  • update go.mod
  • bump BIFROST_CORE_VERSION в Makefile / Dockerfile / scripts/build-ui.sh (UI должна совпадать с binary’ной версией)
  • audit core/schemas/plugin.go на breaking changes в LLMPlugin interface

Открытые вопросы

  • Реализовать custom PluginLoader для починки re-enable. Triggered by — operators начнут жаловаться на rollout-restart, или захотим A/B toggling без redeploy
  • Submit schema-mocker upstream. Плагин domain-agnostic (medical preset за blank import), LLMPlugin interface стандартный. Если merged — мы перестаём поддерживать форк, но возможно появится PR-coupling на upstream. Trade-off

Связано

  • bifrost-mock-plugins — entity-page плагинов, реализующих этот pattern
  • bifrost — vendor сам по себе
  • Source ref: _bifrost-upstream/transports/bifrost-http/server/server.go:974SyncLoadedPlugin definition

Источники

Источники: 1 2.

Сноски

  1. Bifrost docs, accessed 2026-05-17, https://docs.getbifrost.ai/plugins/writing-go-plugin.

  2. Bifrost issue про per-request context-keys, accessed 2026-05-17, https://github.com/maximhq/bifrost/issues/2029.