---
title: "Plugin Migration Guide"
description: "How to migrate your Bifrost plugins from v1.3.x to v1.4.x"
icon: "arrow-up-right-dots"
---

## Overview

Bifrost v1.4.x introduces a new plugin interface for HTTP transport layer interception. This guide helps you migrate existing plugins from the v1.3.x `TransportInterceptor` pattern to the v1.4.x `HTTPTransportPreHook` and `HTTPTransportPostHook` pattern.

<Note>
If your plugin doesn't use `TransportInterceptor`, no migration is needed. The `PreLLMHook`, `PostLLMHook`, `Init`, `GetName`, and `Cleanup` functions remain unchanged.
</Note>

## What Changed?

The HTTP transport interception mechanism changed from a simple function that receives and returns headers/body to a dual-hook pattern that works with both native `.so` plugins and WASM plugins.

### Key Differences

| Aspect | v1.3.x (TransportInterceptor) | v1.4.x+ (Pre/Post Hooks) |
|--------|-------------------------------|--------------------------|
| Signature | `TransportInterceptor(ctx, url, headers, body)` | `HTTPTransportPreHook(ctx, req)` + `HTTPTransportPostHook(ctx, req, resp)` |
| Return type | `(headers, body, error)` | Pre: `(*HTTPResponse, error)`, Post: `error` |
| Request type | Separate `headers map`, `body map` | Unified `*HTTPRequest` struct |
| Response access | Not available | Post-hook receives `*HTTPResponse` |
| Modification | Return modified maps | Modify `req`/`resp` in-place |
| Short-circuit | Return error | Return `*HTTPResponse` |
| WASM support | No | Yes |
| Context | Limited `BifrostContext` | Full `*BifrostContext` with `SetValue`/`Value` |

### Why the Change?

The new dual-hook pattern provides:

1. **WASM plugin support** - Serializable types work across WASM boundary
2. **Response interception** - Post-hook can modify responses before returning to client
3. **Simpler API** - No middleware wrapper, direct function call
4. **Better testability** - No fasthttp dependency in plugin tests
5. **Full context access** - BifrostContext available for sharing data between hooks
6. **Custom response short-circuits** - Return a full response to short-circuit

## Migration Steps

### Step 1: Update Imports

Remove the `fasthttp` import if present:

```go
import (
	"fmt"

	"github.com/maximhq/bifrost/core/schemas"
	// Remove: "github.com/valyala/fasthttp"
)
```

### Step 2: Replace the Function

**Before (v1.3.x):**

```go
// TransportInterceptor modifies raw HTTP headers and body
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
	// Add custom header
	headers["X-Custom-Header"] = "value"
	
	// Modify body
	body["custom_field"] = "custom_value"
	
	return headers, body, nil
}
```

**After (v1.4.x+):**

```go
// HTTPTransportPreHook intercepts requests BEFORE they enter Bifrost core
// Modify req in-place. Return (*HTTPResponse, nil) to short-circuit.
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	// Add custom header (in-place modification)
	req.Headers["x-custom-header"] = "value"
	
	// Modify body (in-place modification)
	var body map[string]any
	sonic.Unmarshal(req.Body, &body)
	body["custom_field"] = "custom_value"
	req.Body, _ = sonic.Marshal(body)
	
	// Store values in context for use in post-hook
	ctx.SetValue(schemas.BifrostContextKey("my-plugin-key"), "my-value")
	
	// Return nil to continue, or return &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.
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Add response header
	resp.Headers["x-processed-by"] = "my-plugin"
	
	// Read values set in pre-hook
	if val := ctx.Value(schemas.BifrostContextKey("my-plugin-key")); val != nil {
		fmt.Println("Context value:", val)
	}
	
	// Return nil to continue, or return error to short-circuit
	return nil
}
```

### Step 3: Update Body Modification Logic

In v1.3.x, you received the body as a `map[string]any`. In v1.4.x, you work with `req.Body` bytes:

**Before (v1.3.x):**

```go
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
	// Direct map access
	body["model"] = "gpt-4"
	return headers, body, nil
}
```

**After (v1.4.x+):**

```go
import "github.com/bytedance/sonic"

func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	// Parse body
	var body map[string]any
	if err := sonic.Unmarshal(req.Body, &body); err == nil {
		// Modify body
		body["model"] = "gpt-4"
		// Update req.Body in-place
		req.Body, _ = sonic.Marshal(body)
	}
	return nil, nil
}

func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Modify response body if needed
	var respBody map[string]any
	if err := sonic.Unmarshal(resp.Body, &respBody); err == nil {
		respBody["plugin_processed"] = true
		resp.Body, _ = sonic.Marshal(respBody)
	}
	return nil
}
```

## Common Migration Patterns

### Adding Headers

**v1.3.x:**
```go
headers["authorization"] = "Bearer " + token
return headers, body, nil
```

**v1.4.x+:**
```go
// In HTTPTransportPreHook - modify request headers
req.Headers["authorization"] = "Bearer " + token
return nil, nil

// In HTTPTransportPostHook - modify response headers
resp.Headers["x-request-id"] = requestID
return nil
```

### Reading Headers

**v1.3.x:**
```go
apiKey := headers["X-API-Key"]
```

**v1.4.x+:**
```go
// Use case-insensitive helper for reading (recommended)
apiKey := req.CaseInsensitiveHeaderLookup("X-API-Key")

// Or direct map access (case-sensitive)
apiKey := req.Headers["x-api-key"]
```

### Conditional Processing

**v1.3.x:**
```go
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
	if headers["x-skip-processing"] == "true" {
		return headers, body, nil
	}
	// Process...
	return headers, body, nil
}
```

**v1.4.x+:**
```go
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	if req.CaseInsensitiveHeaderLookup("x-skip-processing") == "true" {
		return nil, nil // Continue without modification
	}
	// Process...
	return nil, nil
}

func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Post-hook always runs unless pre-hook short-circuited
	return nil
}
```

### Error Handling / Short-Circuit

**v1.3.x:**
```go
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
	if headers["x-api-key"] == "" {
		return nil, nil, fmt.Errorf("missing API key")
	}
	return headers, body, nil
}
```

**v1.4.x+:**
```go
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	if req.CaseInsensitiveHeaderLookup("x-api-key") == "" {
		// Return a custom response to short-circuit
		return &schemas.HTTPResponse{
			StatusCode: 401,
			Headers:    map[string]string{"Content-Type": "application/json"},
			Body:       []byte(`{"error": "missing API key"}`),
		}, nil
	}
	return nil, nil
}

func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Not called if pre-hook short-circuited
	return nil
}
```

### Accessing Request Method and Path

**v1.3.x:**
```go
// url parameter contained the full URL
func TransportInterceptor(ctx *schemas.BifrostContext, url string, headers map[string]string, body map[string]any) (map[string]string, map[string]any, error) {
	// Limited access to URL
	return headers, body, nil
}
```

**v1.4.x+:**
```go
func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
	// Full access to request properties
	method := req.Method        // "GET", "POST", etc.
	path := req.Path            // "/v1/chat/completions"
	query := req.Query          // map[string]string of query params
	pathParams := req.PathParams // map[string]string of path variables (e.g., {model})
	return nil, nil
}

func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Access both request and response
	statusCode := resp.StatusCode
	responseHeaders := resp.Headers
	responseBody := resp.Body
	_ = statusCode      // Use variables...
	_ = responseHeaders
	_ = responseBody
	return nil
}
```

## Testing Your Migration

1. **Build your updated plugin:**
   ```bash
   go build -buildmode=plugin -o my-plugin.so main.go
   ```

2. **Update Bifrost to v1.4.x:**
   ```bash
   go get github.com/maximhq/bifrost/core@v1.4.0
   ```

3. **Test with a simple 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"}]}'
   ```

4. **Verify logs show both hooks being called:**
   ```
   HTTPTransportPreHook called
   PreLLMHook called
   PostLLMHook called
   HTTPTransportPostHook called
   ```

## Troubleshooting

### Plugin fails to load after migration

**Error:** `plugin: symbol TransportInterceptor not found`

This error occurs if Bifrost v1.4.x is looking for the old function. Make sure:
1. You've updated to `HTTPTransportPreHook` and `HTTPTransportPostHook`
2. The function signatures match exactly:
   - `func HTTPTransportPreHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error)`
   - `func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error`
3. You've rebuilt the plugin with the correct core version

### Body modification not working

Make sure you're assigning back to `req.Body` in the pre-hook:

```go
// Wrong - body changes lost
var body map[string]any
sonic.Unmarshal(req.Body, &body)
body["model"] = "gpt-4"
// Missing: req.Body = ...

// Correct - body changes applied
var body map[string]any
sonic.Unmarshal(req.Body, &body)
body["model"] = "gpt-4"
req.Body, _ = sonic.Marshal(body)  // Assign back!
```

### Response modification not working

Make sure you're modifying `resp` in the post-hook:

```go
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
	// Modify response headers
	resp.Headers["x-custom-header"] = "value"
	
	// Modify response body
	var body map[string]any
	sonic.Unmarshal(resp.Body, &body)
	body["extra_field"] = "value"
	resp.Body, _ = sonic.Marshal(body)
	
	return nil
}
```

### Headers not being set

Make sure you're modifying `req.Headers` or `resp.Headers` directly:

```go
// Set request header in pre-hook
req.Headers["x-custom-header"] = "value"

// Set response header in post-hook
resp.Headers["x-custom-header"] = "value"

// Read headers using case-insensitive helper
value := req.CaseInsensitiveHeaderLookup("X-Custom-Header")
```

### Context values not available in post-hook

Make sure you're using the correct context key type:

```go
// In pre-hook - set value
ctx.SetValue(schemas.BifrostContextKey("my-key"), "my-value")

// In post-hook - read value
if val := ctx.Value(schemas.BifrostContextKey("my-key")); val != nil {
	// Use val
}
```

## Streaming Chunk Hook (v1.4.x)

Bifrost v1.4.x introduces a new hook for intercepting streaming response chunks:

### HTTPTransportStreamChunkHook

This hook is called for each chunk during streaming responses, allowing plugins to modify or filter chunks before they're sent to the client.

```go
// HTTPTransportStreamChunkHook intercepts streaming chunks BEFORE they're written to the client.
// Modify chunk data or return nil to skip the chunk entirely.
// 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) {
    // 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

    // Return nil to skip/filter this chunk
    // return nil, nil

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

**Key differences from `HTTPTransportPostHook`:**

| Aspect | HTTPTransportPostHook | HTTPTransportStreamChunkHook |
|--------|----------------------|------------------------------|
| When called | After complete response | Per-chunk during streaming |
| Input | Full HTTPResponse | `*BifrostStreamChunk` (typed struct) |
| Can modify | Full response | Individual chunk struct |
| Can skip | N/A | Return nil to skip chunk |

<Note>
`HTTPTransportPostHook` is **not called** for streaming responses. Use `HTTPTransportStreamChunkHook` instead to intercept streaming data.
</Note>

### Migration for Existing Plugins

If your plugin implements `HTTPTransportPostHook` and you want to also handle streaming responses, add the new hook:

```go
// Existing hook for non-streaming responses
func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
    // Handle complete responses
    return nil
}

// NEW: Add this for streaming responses
func HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, chunk *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
    // Handle streaming chunks (typed struct, not raw bytes)
    // Return chunk unchanged if no modification needed
    return chunk, nil
}
```

## Need Help?

- **Discord Community**: [Join our Discord](https://discord.gg/exN5KAydbU)
- **GitHub Issues**: [Report bugs or request features](https://github.com/maximhq/bifrost/issues)
- **Writing Plugins Guide**: [Full plugin documentation](./writing-plugin)


