Overview
This guide walks you through creating a native Go 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.26.1 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.26.1
Project Structure
A minimal plugin project should have the following structure:Step 1: Initialize Your Plugin Project
Create a new directory and initialize a Go module:go.mod should look like this:
Step 2: Implement the Plugin Interface
Createmain.go with the required plugin functions. Here’s the complete hello-world example:
- v1.5.x+
- v1.4.x
- v1.3.x
This skeleton only shows the LLM and HTTP transport hooks. Plugins that need to intercept the MCP gateway (per-tool-call governance, transport-setup mutation) can also export
PreMCPHook / PostMCPHook / PreMCPConnectionHook / PostMCPConnectionHook — see MCP plugin hooks overview below.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
GetName() string
Returns a unique identifier for your plugin. This name appears in logs and status reports.
ctx.Log(level, msg) v1.5.x+
Emits a structured log entry scoped to the current plugin. Use this instead of fmt.Println for production logging - entries are collected per-request and can be retrieved programmatically.
Key points:
- Thread-safe - safe to call from concurrent goroutines
- Scoped - each entry is automatically tagged with your plugin’s name
- Timestamped - entries include Unix millisecond timestamps
- No-op when unscoped - safe to call in any context; silently ignored outside plugin hooks
- Logs are retrievable via
ctx.GetPluginLogs()(read copy) orctx.DrainPluginLogs()(transfer ownership)
- v1.5.x+
- v1.4.x
- v1.3.x
HTTPTransportPreHook(ctx, req)
HTTP transport only. Intercepts requests BEFORE they enter Bifrost core. Use this to:- Modify request headers, body, or query params in-place
- Short-circuit with a custom response
- Store values in
BifrostContextfor use in other hooks - Works with both native .so and WASM plugins
- Receives serializable
*HTTPRequest(not raw fasthttp) - Modify
req.Headers,req.Body,req.Querydirectly - Return
(nil, nil)to continue to next plugin/handler - Return
(*HTTPResponse, nil)to short-circuit with response - Return
(nil, error)to short-circuit with error
HTTPTransportPostHook(ctx, req, resp)
HTTP transport only. Intercepts responses AFTER they exit Bifrost core. Use this to:- Modify response headers or body in-place
- Log or monitor response data
- Access context values set in pre-hook
- Called in reverse order of pre-hooks
- Receives both
*HTTPRequestand*HTTPResponse - Modify
resp.Headers,resp.Body,resp.StatusCodedirectly - Return
nilto continue to next plugin/handler - Return
errorto short-circuit with error and skip remaining post-hooks - NOT called for streaming responses - use
HTTPTransportStreamChunkHookinstead
HTTPTransportStreamChunkHook(ctx, req, chunk)
HTTP transport only. Intercepts streaming response chunks BEFORE they’re written to the client. Use this to:- Modify streaming chunks in real-time
- Filter/skip specific chunks
- Log or monitor streaming data
- Called in reverse order of pre-hooks
- Receives a
*schemas.BifrostStreamChunktyped struct (not raw bytes) - The struct contains one non-nil field based on the response type (chat, text completion, responses, speech, transcription, image generation, or error)
- Return
(chunk, nil)to pass through unchanged - Return
(nil, nil)to skip/filter the chunk entirely - Return
(modifiedChunk, nil)to return a modified chunk - Return
(nil, error)to send error to client and stop streaming
Header and Query Parameter Lookups: Use the case-insensitive helper methods for reading headers and query parameters:The helper methods (
CaseInsensitiveHeaderLookup and CaseInsensitiveQueryLookup) ensure your plugin works correctly regardless of how the client sends header/query parameter names.PreRequestHook(...) v1.6.x+
Called once per top-level request, before any provider call and before PreLLMHook. This is the routing phase: it’s where plugins decide which provider, model, and fallbacks the request should be sent to.
Use this for:
- Routing decisions: governance rules, virtual-key load balancing, geo/tier routing
- Provider resolution: filling in
req.Providerfor unprefixed model names (the built-inmodel-catalog-resolverdoes this as the last routing layer) - Fallback chain construction: populating
req.Fallbacksbased on policy
PreLLMHook:
Routing Example:
ctx.AppendRoutingEngineLog(engine, level, message)— emits a structured log entry visible in observability tools. Use it to explain why a routing decision was made.schemas.AppendToContextList(ctx, schemas.BifrostContextKeyRoutingEnginesUsed, engineName)— records which routing engine(s) participated. Surfaces in telemetry asrouting.engines_used.
Plugin order matters for routing. Built-in routing plugins run in this order within
PreRequestHook: governance routing rules → governance VK load balancing → enterprise load balancer → model-catalog-resolver (final fallback). Custom plugins can slot in via placement + order — see Plugin Sequencing.Errors are non-blocking. Returning a non-nil error from
PreRequestHook logs a warning but does NOT fail the request — the pipeline continues with the next plugin. The core validates req.Provider after all PreRequestHook plugins have run; an unresolved provider returns a 400 to the caller.PreLLMHook(...)
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
PostLLMHook(...)
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
MCP plugin hooks overview v1.5.x+
Plugins can also intercept traffic going through the MCP gateway (Bifrost’s Model Context Protocol surface) by exporting any of four optional symbols. These are separate from the LLM hooks above — the LLM hooks fire for/v1/chat/completions / /v1/responses style traffic; the MCP hooks fire for ping / list_tools / execute_tool / connect operations on configured MCP clients.
The MCP surface splits into two lifecycle stages with two hook pairs each:
Connect carries transport-level inputs (URL, headers, stdio args) that don’t apply post-connection, so it’s handled separately by a typed interface. A plugin can implement both pairs, just the envelope pair, or just the Connect pair — export whichever symbols you need. The
.so loader treats every MCP hook as optional.
Connect short-circuits use
*MCPConnectionShortCircuit (typed Connect response). Envelope short-circuits use *MCPPluginShortCircuit (generic envelope wrapper). They look similar but are not interchangeable — Connect doesn’t go through the envelope path.PreMCPConnectionHook(ctx, req) v1.5.x+
Runs when a configured MCP client’s transport is being brought up. Use this to:
- Inject transport-level headers (HTTP/SSE clients)
- Mutate
StdioCommand/StdioArgsfor stdio transports - Refuse a connection (short-circuit with an error) based on client name / auth type
- Mutable fields (changes are honored by Bifrost):
ConnectionString,Headers,StdioCommand,StdioArgs - Observe-only fields (mutations are ignored):
ClientName,ConnectionType,AuthType— changing them mid-flight would break the rest of the connect codepath Headersmutations are silently dropped onSTDIOandInProcesstransports — checkreq.ConnectionTypebefore injecting- Return
(req, nil, nil)to continue with the (possibly mutated) request - Return
(req, &MCPConnectionShortCircuit{Error: ...}, nil)to refuse the connection - Return
(req, &MCPConnectionShortCircuit{Response: ...}, nil)to synthesize a successful handshake (rare — mostly for testing)
PostMCPConnectionHook(ctx, resp, bifrostErr) v1.5.x+
Runs after the upstream initialize handshake. Use this to:
- Read
ServerInfo,ProtocolVersion, andServerCapabilitiesfrom the negotiated handshake - Gate downstream behavior on advertised capabilities (e.g. warn if a server doesn’t advertise
Tools) - Transform handshake errors before they surface to the caller
- On handshake failure,
respisnilandbifrostErris populated - Mutating
respis allowed but rarely useful — the handshake result is already cached by the connect pipeline - Return the values unchanged to pass through; return a modified
bifrostErrto transform errors
PreMCPHook(ctx, req) v1.5.x+
Runs before each MCP envelope request (ping / list_tools / execute_tool). Use this to:
- Apply per-tool allowlists or governance
- Cache tool-list responses (short-circuit ListTools with a cached value)
- Add request metadata to context for downstream PostMCPHook
- The envelope is request-type-discriminated via
req.RequestType— always early-out for non-tool-execute envelopes unless you specifically want to observe ping/list_tools. Thereq.RequestType.IsExecuteTool()helper exists for exactly this guard - For tool-execute requests, use
req.GetToolName()to read the tool name regardless of which deprecated sub-request shape is populated (chat-tool-call vs responses-tool-call vs the future unified execute-tool) - Return
(req, &MCPPluginShortCircuit{Error: ...}, nil)to block the call - Return
(req, &MCPPluginShortCircuit{Response: ...}, nil)to short-circuit with a synthetic result
PostMCPHook(ctx, resp, bifrostErr) v1.5.x+
Runs after each MCP envelope request completes (successfully or with error). Use this to:
- Record tool-call latency or usage metrics
- Transform error responses
- Update audit trails set by
PreMCPHookvia context
- Like Post LLM hook, runs in reverse order of pre-hooks across the plugin chain
- The originating request type is available on the response via
resp.ExtraFields.MCPRequestType(and on errors viabifrostErr.ExtraFields.MCPRequestType), so the sameIsExecuteTool()early-out pattern works here too - Latency in milliseconds is on
resp.ExtraFields.Latency
Cleanup() error
Called on Bifrost shutdown. Use this to:
- Close database connections
- Flush buffers
- Save state
- Release resources
Step 3: Create a Makefile
Create aMakefile to automate building your plugin:
Step 4: Build Your Plugin
Build the plugin using the Makefile:build/my-plugin.so in your project directory.
For production, you may need to build for specific platforms:
Step 5: Configure Bifrost to Load Your Plugin
Add your plugin to Bifrost’sconfig.json:
Plugin Configuration Options
enabled- Set totrueto load the pluginname- Plugin identifier (used in logs)path- Absolute or relative path to the.sofileconfig- Plugin-specific configuration passed toInit()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:- v1.5.x+ (non-streaming)
- v1.5.x+ (streaming)
- v1.4.x (non-streaming)
- v1.4.x (streaming)
- v1.3.x
Advanced Plugin Patterns
Stateful Plugins
For plugins that need to maintain state across requests:Error Handling with Fallbacks
Control whether Bifrost should try fallback providers:Caching Plugin Example
Troubleshooting
Plugin Fails to Load
Error:plugin: not a plugin file
Solution: Ensure you built with -buildmode=plugin:
Version Mismatch Errors
Error:plugin was built with a different version of package
Why this happens: Go’s plugin system requires exact version matching for:
- The Go compiler version
- All shared packages (especially
github.com/maximhq/bifrost/core) - Transitive dependencies (packages that your dependencies depend on)
go version -m to inspect the build info of both your plugin and the Bifrost binary:
go1.26.1), the package versions are different - this causes the error.
Step 2: Update your plugin dependencies
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. The loader treats every hook as optional except GetName and Cleanup — including the MCP hook quartet (PreMCPHook, PostMCPHook, PreMCPConnectionHook, PostMCPConnectionHook). A typo in a hook name silently disables that hook rather than failing the load.
Source Code Reference
The complete hello-world example is available in the Bifrost repository:- Full Example: examples/plugins/hello-world
- main.go: Plugin implementation
- Makefile: Build configuration
- go.mod: Dependencies
Real-World Plugin Examples
Explore production-ready plugins in the Bifrost repository:- Mocker Plugin - Mock responses for testing
- Logging Plugin - Advanced request/response logging
- Semantic Cache Plugin - Cache based on semantic similarity
- Governance Plugin - Rate limiting and budget controls
- JSON Parser Plugin - Parse and validate JSON responses
Frequently Asked Questions
Do I need to rebuild my plugin when upgrading Bifrost?
Yes, absolutely. Plugins must be compiled against the exact same version ofgithub.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.modto use the matching core version - Rebuild the plugin with the same Go version
- Redeploy the plugin alongside the new Bifrost version
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:- 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: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.What happens if I forget to rebuild a plugin?
You’ll see errors like:go version -m.
How do I test plugins before production deployment?
Multi-stage testing approach:-
Unit Tests - Test plugin logic in isolation
-
Integration Tests - Load plugin in test Bifrost instance
- 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:BIFROST_LOG_LEVEL=debug in the environment.
Check plugin symbols:
- 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?
- Discord Community: Join our Discord
- GitHub Issues: Report bugs or request features
- Documentation: Browse all docs

