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
| 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:
- WASM plugin support - Serializable types work across WASM boundary
- Response interception - Post-hook can modify responses before returning to client
- Simpler API - No middleware wrapper, direct function call
- Better testability - No fasthttp dependency in plugin tests
- Full context access - BifrostContext available for sharing data between hooks
- 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
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
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
-
Build your updated plugin:
go build -buildmode=plugin -o my-plugin.so main.go
-
Update Bifrost to v1.4.x:
-
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"}]}'
-
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:
- You’ve updated to
HTTPTransportPreHook and HTTPTransportPostHook
- 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
- 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
}
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:
| 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 |
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?