Overview
This guide walks you through creating a custom plugin for Bifrost using our hello-world example as a reference. You’ll learn how to structure your plugin, implement required functions, build the shared object, and integrate it with Bifrost.
Prerequisites
Before you start, ensure you have:
- Go 1.24+ installed (must match Bifrost’s Go version)
- Linux or macOS (Go plugins are not supported on Windows)
- Bifrost installed and configured
- Basic understanding of Go programming
Make sure your go.mod has the go version pinned to 1.24.0
Project Structure
A minimal plugin project should have the following structure:
hello-world/
├── main.go # Plugin implementation
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── Makefile # Build automation
└── .gitignore # Git ignore patterns
Step 1: Initialize Your Plugin Project
Create a new directory and initialize a Go module:
mkdir my-plugin
cd my-plugin
go mod init github.com/yourusername/my-plugin
Add Bifrost as a dependency:
go get github.com/maximhq/bifrost/core@latest
Your go.mod should look like this:
module github.com/yourusername/my-plugin
go 1.24.0
require github.com/maximhq/bifrost/core v1.2.17
Step 2: Implement the Plugin Interface
Create main.go with the required plugin functions. Here’s the complete hello-world example:
package main
import (
"fmt"
"github.com/maximhq/bifrost/core/schemas"
)
// Init is called when the plugin is loaded
// config contains the plugin configuration from config.json
func Init(config any) error {
fmt.Println("Init called")
// Initialize your plugin here (database connections, API clients, etc.)
return nil
}
// GetName returns the plugin's unique identifier
func GetName() string {
return "Hello World Plugin"
}
// TransportInterceptor modifies raw HTTP headers and body
// Only called when using HTTP transport (bifrost-http)
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
fmt.Println("TransportInterceptor called")
// Modify headers or body before they enter Bifrost core
return headers, body, nil
}
// PreHook is called before the request is sent to the provider
// This is where you can modify requests or short-circuit the flow
func PreHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
fmt.Println("PreHook called")
// Modify the request or return a short-circuit to skip provider call
return req, nil, nil
}
// PostHook is called after receiving a response from the provider
// This is where you can modify responses or handle errors
func PostHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
fmt.Println("PostHook called")
// Modify the response or error before returning to caller
return resp, bifrostErr, nil
}
// Cleanup is called when Bifrost shuts down
func Cleanup() error {
fmt.Println("Cleanup called")
// Clean up resources (close connections, flush buffers, etc.)
return nil
}
Understanding Each Function
Init(config any) error
Called once when the plugin is loaded. Use this to:
- Parse plugin configuration
- Initialize database connections
- Set up API clients
- Validate required environment variables
func Init(config any) error {
// Parse configuration
cfg, ok := config.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid config format")
}
apiKey := cfg["api_key"].(string)
// Initialize your resources
return nil
}
GetName() string
Returns a unique identifier for your plugin. This name appears in logs and status reports.
TransportInterceptor(...)
HTTP transport only. Called before requests enter Bifrost core. Use this to:
- Add or modify HTTP headers
- Transform request body
- Implement authentication at the transport layer
This function is only called when using bifrost-http. It’s not invoked when using Bifrost as a Go SDK.
PreHook(...)
Called before each provider request. Use this to:
- Modify request parameters
- Add logging or monitoring
- Implement caching (check cache, return cached response)
- Apply governance rules (rate limiting, budget checks)
- Short-circuit to skip provider calls
Short-Circuiting Example:
func PreHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
// Return cached response without calling provider
if cachedResponse := checkCache(req) {
return req, &schemas.PluginShortCircuit{
Response: cachedResponse,
}, nil
}
return req, nil, nil
}
PostHook(...)
Called after provider responses (or short-circuits). Use this to:
- Transform responses
- Log response data
- Store responses in cache
- Handle errors or implement fallback logic
- Add custom metadata
Response Transformation Example:
func PostHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
if resp != nil && resp.ChatResponse != nil {
// Add custom metadata
resp.ChatResponse.ExtraFields.RawResponse = map[string]interface{}{
"plugin_processed": true,
"timestamp": time.Now().Unix(),
}
}
return resp, bifrostErr, nil
}
Cleanup() error
Called on Bifrost shutdown. Use this to:
- Close database connections
- Flush buffers
- Save state
- Release resources
Step 3: Create a Makefile
Create a Makefile to automate building your plugin:
.PHONY: all build clean install help
PLUGIN_NAME = my-plugin
OUTPUT_DIR = build
# Platform detection
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PLUGIN_EXT = .so
PLATFORM = linux
endif
ifeq ($(UNAME_S),Darwin)
PLUGIN_EXT = .so
PLATFORM = darwin
endif
# Architecture detection
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
ARCH = amd64
endif
ifeq ($(UNAME_M),arm64)
ARCH = arm64
endif
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME)$(PLUGIN_EXT)
build: ## Build the plugin for current platform
@echo "Building plugin for $(PLATFORM)/$(ARCH)..."
@mkdir -p $(OUTPUT_DIR)
go build -buildmode=plugin -o $(OUTPUT) main.go
@echo "Plugin built successfully: $(OUTPUT)"
clean: ## Remove build artifacts
@rm -rf $(OUTPUT_DIR)
install: build ## Build and install to Bifrost plugins directory
@mkdir -p ~/.bifrost/plugins
@cp $(OUTPUT) ~/.bifrost/plugins/
@echo "Plugin installed to ~/.bifrost/plugins/"
Step 4: Build Your Plugin
Build the plugin using the Makefile:
This creates build/my-plugin.so in your project directory.
For production, you may need to build for specific platforms:
# Build for Linux AMD64
GOOS=linux GOARCH=amd64 go build -buildmode=plugin -o my-plugin-linux-amd64.so main.go
# Build for Linux ARM64
GOOS=linux GOARCH=arm64 go build -buildmode=plugin -o my-plugin-linux-arm64.so main.go
# Build for macOS ARM64 (M1/M2)
GOOS=darwin GOARCH=arm64 go build -buildmode=plugin -o my-plugin-darwin-arm64.so main.go
Cross-compilation doesn’t work for plugins! You must build on the target platform. If you need a Linux plugin, build it on a Linux machine or use Docker.
Add your plugin to Bifrost’s config.json:
{
"plugins": [
{
"enabled": true,
"name": "my-plugin",
"path": "/path/to/my-plugin.so",
"version": 1,
"config": {
"api_key": "your-api-key",
"custom_setting": "value"
}
}
]
}
Plugin Configuration Options
enabled - Set to true to load the plugin
name - Plugin identifier (used in logs)
path - Absolute or relative path to the .so file
config - Plugin-specific configuration passed to Init()
version - (Optional) Plugin version number (default: 1). Increment this value to force a reload of the plugin and database update when Bifrost restarts. Useful when you want to ensure config changes take effect without manually clearing plugin state.
Step 6: Test Your Plugin
Start Bifrost and verify your plugin loads:
You should see output like:
Init called
[INFO] Plugin loaded: Hello World Plugin
Make a test request:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Check the logs for plugin hook calls:
TransportInterceptor called
PreHook called
PostHook called
Advanced Plugin Patterns
Stateful Plugins
For plugins that need to maintain state across requests:
package main
import (
"sync"
"github.com/maximhq/bifrost/core/schemas"
)
var (
requestCount int64
mu sync.Mutex
)
func PreHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
mu.Lock()
requestCount++
count := requestCount
mu.Unlock()
// Use count for rate limiting, metrics, etc.
return req, nil, nil
}
Error Handling with Fallbacks
Control whether Bifrost should try fallback providers:
func PostHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
if bifrostErr != nil {
// Allow fallbacks for rate limit errors
if bifrostErr.Error.Type != nil && *bifrostErr.Error.Type == "rate_limit" {
allowFallbacks := true
bifrostErr.AllowFallbacks = &allowFallbacks
} else {
// Don't try fallbacks for auth errors
allowFallbacks := false
bifrostErr.AllowFallbacks = &allowFallbacks
}
}
return resp, bifrostErr, nil
}
Caching Plugin Example
var cache sync.Map
func PreHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
// Generate cache key from request
key := generateCacheKey(req)
// Check cache
if cached, ok := cache.Load(key); ok {
return req, &schemas.PluginShortCircuit{
Response: cached.(*schemas.BifrostResponse),
}, nil
}
return req, nil, nil
}
func PostHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
if resp != nil && bifrostErr == nil {
// Store in cache
key := generateCacheKeyFromResponse(resp)
cache.Store(key, resp)
}
return resp, bifrostErr, nil
}
Troubleshooting
Plugin Fails to Load
Error: plugin: not a plugin file
Solution: Ensure you built with -buildmode=plugin:
go build -buildmode=plugin -o plugin.so main.go
Version Mismatch Errors
Error: plugin was built with a different version of package
Solution: Rebuild your plugin with the exact same Go version as Bifrost:
go version # Check your Go version
# Rebuild with matching version
Error: cannot load plugin built for GOOS=linux on darwin
Solution: Build on the target platform or use the correct GOOS/GOARCH for your system.
Function Not Found
Error: plugin: symbol Init not found
Solution: Ensure all required functions are exported (start with capital letter) and have the correct signature.
Source Code Reference
The complete hello-world example is available in the Bifrost repository:
Real-World Plugin Examples
Explore production-ready plugins in the Bifrost repository:
Frequently Asked Questions
Do I need to rebuild my plugin when upgrading Bifrost?
Yes, absolutely. Plugins must be compiled against the exact same version of github.com/maximhq/bifrost/core that Bifrost is using. This is a fundamental requirement of Go’s plugin system.
When you upgrade Bifrost, you must:
- Update your plugin’s
go.mod to use the matching core version
- Rebuild the plugin with the same Go version
- Redeploy the plugin alongside the new Bifrost version
Example:
If upgrading from Bifrost v1.2.17 to v1.3.0:
# Update your plugin dependency
go get github.com/maximhq/bifrost/core@v1.3.0
go mod tidy
# Rebuild the plugin
go build -buildmode=plugin -o my-plugin.so main.go
Version mismatch will cause runtime errors! If your plugin is compiled with v1.2.17 but Bifrost is running v1.3.0, the plugin will fail to load with cryptic errors about package versions.
Should plugin builds be part of my deployment pipeline?
Yes, strongly recommended. Your plugin build and deployment should be tightly coupled with your Bifrost deployment.
Recommended CI/CD Workflow:
# Example GitHub Actions workflow
name: Deploy Bifrost with Plugins
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 1. Checkout code
- uses: actions/checkout@v3
# 2. Setup Go
- uses: actions/setup-go@v4
with:
go-version: '1.24'
# 3. Build Bifrost
- name: Build Bifrost
run: |
cd transports/bifrost-http
go build -o bifrost-http
# 4. Build ALL plugins with matching version
- name: Build Plugins
run: |
cd plugins/my-plugin
# Ensure plugin uses same core version as Bifrost
go get github.com/maximhq/bifrost/core@${{ env.BIFROST_VERSION }}
go mod tidy
go build -buildmode=plugin -o my-plugin.so main.go
# 5. Bundle everything together
- name: Create deployment bundle
run: |
mkdir -p deploy/plugins
cp transports/bifrost-http/bifrost-http deploy/
cp plugins/my-plugin/my-plugin.so deploy/plugins/
cp config.json deploy/
# 6. Deploy bundle to your infrastructure
- name: Deploy to Production
run: |
# Upload to S3, copy to servers, deploy to K8s, etc.
./deploy.sh
Key Principles:
- Version Lock - Pin your plugin dependencies to specific Bifrost versions
- Atomic Deployment - Deploy Bifrost and plugins together as a single unit
- Build Verification - Test plugin loading as part of CI
- Rollback Strategy - Keep previous plugin versions for rollbacks
How do I handle plugin versioning in production?
Organize your plugin deployments by version:
/opt/bifrost/
├── v1.3.0/
│ ├── bifrost-http
│ └── plugins/
│ ├── my-plugin.so
│ └── cache-plugin.so
├── v1.2.17/
│ ├── bifrost-http
│ └── plugins/
│ ├── my-plugin.so
│ └── cache-plugin.so
└── current -> v1.3.0/ # Symlink to active version
This allows easy rollbacks:
# Rollback to previous version
ln -sfn /opt/bifrost/v1.2.17 /opt/bifrost/current
systemctl restart bifrost
Can I use different plugin versions for different Bifrost instances?
No. Each plugin must match the exact core version of the Bifrost instance loading it. If you’re running multiple Bifrost versions (e.g., staging vs production), you need separate plugin builds for each version.
staging/
bifrost-http (v1.3.0)
plugins/
my-plugin-v1.3.0.so
production/
bifrost-http (v1.2.17)
plugins/
my-plugin-v1.2.17.so
What happens if I forget to rebuild a plugin?
You’ll see errors like:
plugin: symbol Init not found in plugin github.com/you/plugin
plugin was built with a different version of package github.com/maximhq/bifrost/core
Solution: Rebuild the plugin with the correct core version.
How do I test plugins before production deployment?
Multi-stage testing approach:
-
Unit Tests - Test plugin logic in isolation
func TestPreHook(t *testing.T) {
req := &schemas.BifrostRequest{...}
modifiedReq, shortCircuit, err := PreHook(&ctx, req)
assert.NoError(t, err)
assert.Nil(t, shortCircuit)
}
-
Integration Tests - Load plugin in test Bifrost instance
# Start test Bifrost with plugin
./bifrost-http --config test-config.json
# Run test requests
curl -X POST http://localhost:8080/v1/chat/completions ...
-
Staging Environment - Deploy to staging with production-like load
-
Canary Deployment - Gradually roll out to production
Can I hot-reload plugins without restarting Bifrost?
Yes! Bifrost supports hot-reloading plugins at runtime. You can update plugin configurations or reload plugin code without restarting the entire Bifrost instance.
How do I debug plugin loading issues?
Enable verbose logging:
{
"log_level": "debug",
"plugins": [
{
"enabled": true,
"name": "my-plugin",
"path": "./plugins/my-plugin.so",
"config": {}
}
]
}
Check plugin symbols:
# List symbols exported by plugin
go tool nm my-plugin.so | grep -E 'Init|GetName|PreHook'
Verify Go version:
# Check Go version used to build plugin
go version -m my-plugin.so
Common debugging steps:
- Verify file exists and has correct permissions
- Check Go version matches Bifrost
- Confirm core package version matches
- Ensure all required symbols are exported
- Review Bifrost logs for detailed error messages
Need Help?