Skip to main content

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.
If your plugin doesn’t use TransportInterceptor, no migration is needed. The PreLLMHook, PostLLMHook, Init, GetName, and Cleanup functions remain unchanged.

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

Aspectv1.3.x (TransportInterceptor)v1.4.x+ (Pre/Post Hooks)
SignatureTransportInterceptor(ctx, url, headers, body)HTTPTransportPreHook(ctx, req) + HTTPTransportPostHook(ctx, req, resp)
Return type(headers, body, error)Pre: (*HTTPResponse, error), Post: error
Request typeSeparate headers map, body mapUnified *HTTPRequest struct
Response accessNot availablePost-hook receives *HTTPResponse
ModificationReturn modified mapsModify req/resp in-place
Short-circuitReturn errorReturn *HTTPResponse
WASM supportNoYes
ContextLimited BifrostContextFull *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:
import (
	"fmt"

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

Step 2: Replace the Function

Before (v1.3.x):
// 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+):
// 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):
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+):
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:
headers["authorization"] = "Bearer " + token
return headers, body, nil
v1.4.x+:
// 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:
apiKey := headers["X-API-Key"]
v1.4.x+:
// 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:
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+:
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:
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+:
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:
// 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+:
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:
    go build -buildmode=plugin -o my-plugin.so main.go
    
  2. Update Bifrost to v1.4.x:
    go get github.com/maximhq/bifrost/[email protected]
    
  3. Test with a simple 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"}]}'
    
  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:
// 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:
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:
// 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:
// 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.
// 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:
AspectHTTPTransportPostHookHTTPTransportStreamChunkHook
When calledAfter complete responsePer-chunk during streaming
InputFull HTTPResponse*BifrostStreamChunk (typed struct)
Can modifyFull responseIndividual chunk struct
Can skipN/AReturn nil to skip chunk
HTTPTransportPostHook is not called for streaming responses. Use HTTPTransportStreamChunkHook instead to intercept streaming data.

Migration for Existing Plugins

If your plugin implements HTTPTransportPostHook and you want to also handle streaming responses, add the new hook:
// 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?