---
title: "Writing Go Plugins"
description: "Step-by-step guide to creating native Go plugins for Bifrost using shared object (.so) files"
icon: "golang"
---

## Overview

This guide walks you through creating a native Go plugin for Bifrost using our [hello-world example](https://github.com/maximhq/bifrost/tree/main/examples/plugins/hello-world) 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

<Note>Make sure your go.mod has the go version pinned to 1.26.1</Note>

## 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:

```bash
mkdir my-plugin
cd my-plugin
go mod init github.com/yourusername/my-plugin
```

Add Bifrost as a dependency:

```bash
go get github.com/maximhq/bifrost/core@latest
```

Your `go.mod` should look like this:

```go
module github.com/yourusername/my-plugin

go 1.26.1

require github.com/maximhq/bifrost/core v1.2.38
```

## Step 2: Implement the Plugin Interface

Create `main.go` with the required plugin functions. Here's the complete hello-world example:

<Tabs>
  <Tab title="v1.5.x+">
```go
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"
}

// HTTPTransportPreHook intercepts requests BEFORE they enter Bifrost core
// Modify req in-place. Return (*HTTPResponse, nil) to short-circuit.
// Only called when using HTTP transport (bifrost-http)
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	// Use ctx.Log for structured, scoped logging (recommended)
	ctx.Log(schemas.LogLevelInfo, "HTTPTransportPreHook called")

	// Read headers using case-insensitive helper (recommended)
	contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
	fmt.Printf("Content-Type: %s\n", contentType)

	// Modify request in-place
	req.Headers["x-custom-header"] = "custom-value"

	// Store values in context for use in other hooks
	ctx.SetValue(schemas.BifrostContextKey("my-plugin-key"), "pre-hook-value")

	// Return nil to continue, or return &schemas.HTTPResponse{} to short-circuit
	return nil, nil
}

// HTTPTransportPostHook intercepts responses AFTER they exit Bifrost core
// Modify resp in-place. Called in reverse order of pre-hooks.
// Only called for NON-STREAMING responses when using HTTP transport (bifrost-http)
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	ctx.Log(schemas.LogLevelInfo, "HTTPTransportPostHook called")

	// Modify response headers
	resp.Headers["x-processed-by"] = "my-plugin"

	// Read context values set in pre-hook
	if val := ctx.Value(schemas.BifrostContextKey("my-plugin-key")); val != nil {
		fmt.Printf("Context value: %v\n", val)
	}

	// Return nil to continue, or return error to short-circuit
	return nil
}

// HTTPTransportStreamChunkHook intercepts streaming chunks BEFORE they're sent to the client
// Modify chunk or return nil to skip. Called in reverse order of pre-hooks.
// Only called for STREAMING responses when using HTTP transport (bifrost-http)
func HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
	ctx.Log(schemas.LogLevelInfo, "HTTPTransportStreamChunkHook called")

	// chunk is a typed struct containing one of:
	// - BifrostTextCompletionResponse (text completion streaming)
	// - BifrostChatResponse (chat completion streaming)
	// - BifrostResponsesStreamResponse (responses API streaming)
	// - BifrostSpeechStreamResponse (speech synthesis streaming)
	// - BifrostTranscriptionStreamResponse (transcription streaming)
	// - BifrostImageGenerationStreamResponse (image generation streaming)
	// - BifrostError (error during streaming)

	// Return chunk unchanged to pass through
	return chunk, nil

	// Or return nil to skip/filter this chunk:
	// return nil, nil

	// Or return modified chunk:
	// modifiedChunk := &schemas.BifrostStreamChunk{BifrostChatResponse: ...}
	// return modifiedChunk, nil
}


// PreLLMHook is called before the request is sent to the provider
// This is where you can modify requests or short-circuit the flow
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
	ctx.Log(schemas.LogLevelInfo, "PreLLMHook called")
	// Modify the request or return a short-circuit to skip provider call
	return req, nil, nil
}

// PostLLMHook is called after receiving a response from the provider
// This is where you can modify responses or handle errors
func PostLLMHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
	ctx.Log(schemas.LogLevelInfo, "PostLLMHook 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
}
```
  </Tab>
  <Tab title="v1.4.x">
```go
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"
}

// HTTPTransportPreHook intercepts requests BEFORE they enter Bifrost core
// Modify req in-place. Return (*HTTPResponse, nil) to short-circuit.
// Only called when using HTTP transport (bifrost-http)
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	fmt.Println("HTTPTransportPreHook called")

	// Read headers using case-insensitive helper (recommended)
	contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
	fmt.Printf("Content-Type: %s\n", contentType)

	// Modify request in-place
	req.Headers["x-custom-header"] = "custom-value"

	// Store values in context for use in other hooks
	ctx.SetValue(schemas.BifrostContextKey("my-plugin-key"), "pre-hook-value")

	// Return nil to continue, or return &schemas.HTTPResponse{} to short-circuit
	return nil, nil
}

// HTTPTransportPostHook intercepts responses AFTER they exit Bifrost core
// Modify resp in-place. Called in reverse order of pre-hooks.
// Only called for NON-STREAMING responses when using HTTP transport (bifrost-http)
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	fmt.Println("HTTPTransportPostHook called")

	// Modify response headers
	resp.Headers["x-processed-by"] = "my-plugin"

	// Read context values set in pre-hook
	if val := ctx.Value(schemas.BifrostContextKey("my-plugin-key")); val != nil {
		fmt.Printf("Context value: %v\n", val)
	}

	// Return nil to continue, or return error to short-circuit
	return nil
}

// HTTPTransportStreamChunkHook intercepts streaming chunks BEFORE they're sent to the client
// Modify chunk or return nil to skip. Called in reverse order of pre-hooks.
// Only called for STREAMING responses when using HTTP transport (bifrost-http)
func HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
	fmt.Println("HTTPTransportStreamChunkHook called")

	// chunk is a typed struct containing one of:
	// - BifrostTextCompletionResponse (text completion streaming)
	// - BifrostChatResponse (chat completion streaming)
	// - BifrostResponsesStreamResponse (responses API streaming)
	// - BifrostSpeechStreamResponse (speech synthesis streaming)
	// - BifrostTranscriptionStreamResponse (transcription streaming)
	// - BifrostImageGenerationStreamResponse (image generation streaming)
	// - BifrostError (error during streaming)

	// Return chunk unchanged to pass through
	return chunk, nil

	// Or return nil to skip/filter this chunk:
	// return nil, nil

	// Or return modified chunk:
	// modifiedChunk := &schemas.BifrostStreamChunk{BifrostChatResponse: ...}
	// return modifiedChunk, nil
}


// PreLLMHook is called before the request is sent to the provider
// This is where you can modify requests or short-circuit the flow
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
	fmt.Println("PreLLMHook called")
	// Modify the request or return a short-circuit to skip provider call
	return req, nil, nil
}

// PostLLMHook is called after receiving a response from the provider
// This is where you can modify responses or handle errors
func PostLLMHook(ctx *schemas.BifrostContext, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
	fmt.Println("PostLLMHook 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
}
```
  </Tab>
  <Tab title="v1.3.x">
```go
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
}
```
  </Tab>
</Tabs>

### 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

```go
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.

#### `ctx.Log(level, msg)` <sup>v1.5.x+</sup>

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.

```go
ctx.Log(schemas.LogLevelInfo, "processing request")
ctx.Log(schemas.LogLevelWarn, "cache miss, falling back to provider")
ctx.Log(schemas.LogLevelError, "failed to parse response body")
```

**Available log levels:**

| Constant | Value |
|---|---|
| `schemas.LogLevelDebug` | `"debug"` |
| `schemas.LogLevelInfo` | `"info"` |
| `schemas.LogLevelWarn` | `"warn"` |
| `schemas.LogLevelError` | `"error"` |

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) or `ctx.DrainPluginLogs()` (transfer ownership)

<Tabs>
  <Tab title="v1.5.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 `BifrostContext` for use in other hooks
- Works with both native .so and WASM plugins

Key points:
- Receives serializable `*HTTPRequest` (not raw fasthttp)
- Modify `req.Headers`, `req.Body`, `req.Query` directly
- 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

Key points:
- Receives both `*HTTPRequest` and `*HTTPResponse`
- Modify `resp.Headers`, `resp.Body`, `resp.StatusCode` directly
- Return `nil` to continue to next plugin/handler
- Return `error` to short-circuit with error and skip remaining post-hooks
- **NOT called for streaming responses** - use `HTTPTransportStreamChunkHook` instead

#### `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

Key points:
- Receives a `*schemas.BifrostStreamChunk` typed 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

<Warning>
`HTTPTransportPostHook` is **NOT called** for streaming responses. Use `HTTPTransportStreamChunkHook` to intercept streaming data.
</Warning>

<Note>
**Header and Query Parameter Lookups**: Use the case-insensitive helper methods for reading headers and query parameters:

```go
// ✅ Correct - use helper methods for case-insensitive lookup
contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
apiKey := req.CaseInsensitiveQueryLookup("api_key")

// Also works with any casing
contentType := req.CaseInsensitiveHeaderLookup("content-type")
contentType := req.CaseInsensitiveHeaderLookup("CONTENT-TYPE")

// For setting headers, use direct map access
req.Headers["X-Custom-Header"] = "value"
```

The helper methods (`CaseInsensitiveHeaderLookup` and `CaseInsensitiveQueryLookup`) ensure your plugin works correctly regardless of how the client sends header/query parameter names.
</Note>

<Warning>
These functions are **only called** when using `bifrost-http`. They are **not invoked** when using Bifrost as a Go SDK.
</Warning>
  </Tab>
  <Tab title="v1.4.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 `BifrostContext` for use in other hooks
- Works with both native .so and WASM plugins

Key points:
- Receives serializable `*HTTPRequest` (not raw fasthttp)
- Modify `req.Headers`, `req.Body`, `req.Query` directly
- 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

Key points:
- Receives both `*HTTPRequest` and `*HTTPResponse`
- Modify `resp.Headers`, `resp.Body`, `resp.StatusCode` directly
- Return `nil` to continue to next plugin/handler
- Return `error` to short-circuit with error and skip remaining post-hooks
- **NOT called for streaming responses** - use `HTTPTransportStreamChunkHook` instead

#### `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

Key points:
- Receives a `*schemas.BifrostStreamChunk` typed 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

<Warning>
`HTTPTransportPostHook` is **NOT called** for streaming responses. Use `HTTPTransportStreamChunkHook` to intercept streaming data.
</Warning>

<Warning>
These functions are **only called** when using `bifrost-http`. They are **not invoked** when using Bifrost as a Go SDK.
</Warning>
  </Tab>
  <Tab title="v1.3.x">
#### `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

<Warning>
This function is **only called** when using `bifrost-http`. It's **not invoked** when using Bifrost as a Go SDK.
</Warning>
  </Tab>
</Tabs>

#### `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

**Short-Circuiting Example:**

```go
func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
	// Return cached response without calling provider
	if cachedResponse := checkCache(req) {
		return req, &schemas.LLMPluginShortCircuit{
			Response: cachedResponse,
		}, nil
	}
	return req, nil, nil
}
```

#### `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

**Response Transformation Example:**

```go
func PostLLMHook(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:

```makefile
.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:

```bash
make build
```

This creates `build/my-plugin.so` in your project directory.

For production, you may need to build for specific platforms:

```bash
# 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
```

<Warning>
**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.
</Warning>

## Step 5: Configure Bifrost to Load Your Plugin

Add your plugin to Bifrost's `config.json`:

```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:

```bash
./bifrost-http
```

You should see output like:

```
Init called
[INFO] Plugin loaded: Hello World Plugin
```

Make a test request:

```bash
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:

<Tabs>
  <Tab title="v1.5.x+ (non-streaming)">
```
HTTPTransportPreHook called
PreLLMHook called
PostLLMHook called
HTTPTransportPostHook called
```
  </Tab>
  <Tab title="v1.5.x+ (streaming)">
```
HTTPTransportPreHook called
PreLLMHook called
PostLLMHook called
HTTPTransportStreamChunkHook called (per chunk)
```
  </Tab>
  <Tab title="v1.4.x (non-streaming)">
```
HTTPTransportPreHook called
PreLLMHook called
PostLLMHook called
HTTPTransportPostHook called
```
  </Tab>
  <Tab title="v1.4.x (streaming)">
```
HTTPTransportPreHook called
PreLLMHook called
PostLLMHook called
HTTPTransportStreamChunkHook called (per chunk)
```
  </Tab>
  <Tab title="v1.3.x">
```
TransportInterceptor called
PreHook called
PostHook called
```
  </Tab>
</Tabs>

## Advanced Plugin Patterns

### Stateful Plugins

For plugins that need to maintain state across requests:

```go
package main

import (
	"fmt"
	"sync"
	"github.com/maximhq/bifrost/core/schemas"
)

var (
	requestCount int64
	mu           sync.Mutex
)

func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
	mu.Lock()
	requestCount++
	count := requestCount
	mu.Unlock()
	
	ctx.Log(schemas.LogLevelInfo, fmt.Sprintf("request count: %d", count))
	// Use count for rate limiting, metrics, etc.
	return req, nil, nil
}
```

### Error Handling with Fallbacks

Control whether Bifrost should try fallback providers:

```go
func PostLLMHook(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" {
			ctx.Log(schemas.LogLevelWarn, "rate limit hit, allowing fallbacks")
			allowFallbacks := true
			bifrostErr.AllowFallbacks = &allowFallbacks
		} else {
			// Don't try fallbacks for auth errors
			ctx.Log(schemas.LogLevelError, "auth error, blocking fallbacks")
			allowFallbacks := false
			bifrostErr.AllowFallbacks = &allowFallbacks
		}
	}
	return resp, bifrostErr, nil
}
```

### Caching Plugin Example

```go
var cache sync.Map

func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
	// Generate cache key from request
	key := generateCacheKey(req)
	
	// Check cache
	if cached, ok := cache.Load(key); ok {
		ctx.Log(schemas.LogLevelDebug, "cache hit")
		return req, &schemas.LLMPluginShortCircuit{
			Response: cached.(*schemas.BifrostResponse),
		}, nil
	}
	
	ctx.Log(schemas.LogLevelDebug, "cache miss")
	return req, nil, nil
}

func PostLLMHook(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)
		ctx.Log(schemas.LogLevelDebug, "response cached")
	}
	return resp, bifrostErr, nil
}
```

## Troubleshooting

### Plugin Fails to Load

**Error**: `plugin: not a plugin file`

**Solution**: Ensure you built with `-buildmode=plugin`:
```bash
go build -buildmode=plugin -o plugin.so main.go
```

### 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)

This is more strict than typical Go builds. Even if only one transitive dependency differs by a patch version, the plugin will fail to load.

**Solution**: Ensure your plugin is built with the exact same versions as Bifrost.

**Step 1: Diagnose the mismatch**

Use `go version -m` to inspect the build info of both your plugin and the Bifrost binary:

```bash
# Check what versions your plugin was built with:
$ go version -m my-plugin.so
my-plugin.so: go1.26.1
  dep  github.com/maximhq/bifrost/core  v1.3.50
  dep  github.com/valyala/fasthttp      v1.51.0

# Check what versions Bifrost was built with:
$ go version -m bifrost-http
bifrost-http: go1.26.1
  dep  github.com/maximhq/bifrost/core  v1.3.54   # <-- MISMATCH!
  dep  github.com/valyala/fasthttp      v1.55.0   # <-- MISMATCH!
```

Notice that even though the Go version matches (`go1.26.1`), the **package versions** are different — this causes the error.

**Step 2: Update your plugin dependencies**

```bash
# Update to match Bifrost's core version
go get github.com/maximhq/bifrost/core@v1.3.54
go mod tidy

# Rebuild the plugin
go build -buildmode=plugin -o my-plugin.so main.go
```

**Step 3: Verify the fix**

```bash
# Confirm versions now match
$ go version -m my-plugin.so | grep bifrost
  dep  github.com/maximhq/bifrost/core  v1.3.54  # Now matches!
```

<Tip>
**Pro tip**: Pin exact versions in your `go.mod` and keep your plugin's dependencies in sync with the Bifrost version you're deploying. Consider building both Bifrost and your plugins in the same CI pipeline to guarantee version alignment.
</Tip>

### 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:

- **Full Example**: [examples/plugins/hello-world](https://github.com/maximhq/bifrost/tree/main/examples/plugins/hello-world)
- **main.go**: [Plugin implementation](https://github.com/maximhq/bifrost/blob/main/examples/plugins/hello-world/main.go)
- **Makefile**: [Build configuration](https://github.com/maximhq/bifrost/blob/main/examples/plugins/hello-world/Makefile)
- **go.mod**: [Dependencies](https://github.com/maximhq/bifrost/blob/main/examples/plugins/hello-world/go.mod)

## Real-World Plugin Examples

Explore production-ready plugins in the Bifrost repository:

- **[Mocker Plugin](https://github.com/maximhq/bifrost/tree/main/plugins/mocker)** - Mock responses for testing
- **[Logging Plugin](https://github.com/maximhq/bifrost/tree/main/plugins/logging)** - Advanced request/response logging
- **[Semantic Cache Plugin](https://github.com/maximhq/bifrost/tree/main/plugins/semanticcache)** - Cache based on semantic similarity
- **[Governance Plugin](https://github.com/maximhq/bifrost/tree/main/plugins/governance)** - Rate limiting and budget controls
- **[JSON Parser Plugin](https://github.com/maximhq/bifrost/tree/main/plugins/jsonparser)** - 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 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:

```bash
# 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
```

<Warning>
**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.
</Warning>

### 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:**

```yaml
# 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.26.1'
      
      # 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:

```bash
# 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. See the [Version Mismatch Errors](#version-mismatch-errors) troubleshooting section for detailed diagnosis steps using `go version -m`.

### How do I test plugins before production deployment?

**Multi-stage testing approach:**

1. **Unit Tests** - Test plugin logic in isolation
   ```go
   func TestPreHook(t *testing.T) {
       req := &schemas.BifrostRequest{...}
       modifiedReq, shortCircuit, err := PreLLMHook(&ctx, req)
       assert.NoError(t, err)
       assert.Nil(t, shortCircuit)
   }
   ```

2. **Integration Tests** - Load plugin in test Bifrost instance
   ```bash
   # 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:**

```json
{
  "client": {
    "enable_logging": true
  },
  "plugins": [
    {
      "enabled": true,
      "name": "my-plugin",
      "path": "./plugins/my-plugin.so",
      "config": {}
    }
  ]
}
```

For verbose plugin-loader logs, set `BIFROST_LOG_LEVEL=debug` in the environment.

**Check plugin symbols:**

```bash
# List symbols exported by plugin
go tool nm my-plugin.so | grep -E 'Init|GetName|PreLLMHook'
```

**Verify Go version:**

```bash
# 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?

- **Discord Community**: [Join our Discord](https://discord.gg/exN5KAydbU)
- **GitHub Issues**: [Report bugs or request features](https://github.com/maximhq/bifrost/issues)
- **Documentation**: [Browse all docs](/)
