Skip to main content

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:
make build
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.

Step 5: Configure Bifrost to Load Your Plugin

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:
./bifrost-http
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

Platform/Architecture Mismatch

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:
  1. Update your plugin’s go.mod to use the matching core version
  2. Rebuild the plugin with the same Go version
  3. 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:
  1. Version Lock - Pin your plugin dependencies to specific Bifrost versions
  2. Atomic Deployment - Deploy Bifrost and plugins together as a single unit
  3. Build Verification - Test plugin loading as part of CI
  4. 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:
  1. 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)
    }
    
  2. 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 ...
    
  3. Staging Environment - Deploy to staging with production-like load
  4. 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:
  1. Verify file exists and has correct permissions
  2. Check Go version matches Bifrost
  3. Confirm core package version matches
  4. Ensure all required symbols are exported
  5. Review Bifrost logs for detailed error messages

Need Help?