diff --git a/core/bifrost.go b/core/bifrost.go index 395949a2b8..81346351de 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -50,13 +50,13 @@ type Bifrost struct { backgroundCtx context.Context // Shared background context for nil context handling } -// PluginPipeline encapsulates the execution of plugin PreHooks and PostHooks, tracks which plugins ran, and manages short-circuiting and error aggregation. +// PluginPipeline encapsulates the execution of plugin PreHooks and PostHooks, tracks how many plugins ran, and manages short-circuiting and error aggregation. type PluginPipeline struct { plugins []schemas.Plugin logger schemas.Logger - // Indices of plugins whose PreHook ran (for reverse PostHook) - preHookRan []int + // Number of PreHooks that were executed (used to determine which PostHooks to run in reverse order) + executedPreHooks int // Errors from PreHooks and PostHooks preHookErrors []error postHookErrors []error @@ -70,7 +70,7 @@ func NewPluginPipeline(plugins []schemas.Plugin, logger schemas.Logger) *PluginP } } -// RunPreHooks executes PreHooks in order, tracks which ran, and returns the final request, any short-circuit response, and error. +// RunPreHooks executes PreHooks in order, tracks how many ran, and returns the final request, any short-circuit response, and the count. func (p *PluginPipeline) RunPreHooks(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.BifrostResponse, int) { var resp *schemas.BifrostResponse var err error @@ -80,18 +80,25 @@ func (p *PluginPipeline) RunPreHooks(ctx *context.Context, req *schemas.BifrostR p.preHookErrors = append(p.preHookErrors, err) p.logger.Warn(fmt.Sprintf("Error in PreHook for plugin %s: %v", plugin.GetName(), err)) } - p.preHookRan = append(p.preHookRan, i) + p.executedPreHooks = i + 1 if resp != nil { - return req, resp, i + 1 // short-circuit: only plugins up to and including i ran + return req, resp, p.executedPreHooks // short-circuit: only plugins up to and including i ran } } - return req, nil, len(p.plugins) + return req, nil, p.executedPreHooks } // RunPostHooks executes PostHooks in reverse order for the plugins whose PreHook ran. // Accepts the response and error, and allows plugins to transform either (e.g., recover from error, or invalidate a response). -// Returns the final response and error after all hooks. If both are set, error takes precedence unless a plugin clears it. +// Returns the final response and error after all hooks. If both are set, error takes precedence unless error is nil. func (p *PluginPipeline) RunPostHooks(ctx *context.Context, resp *schemas.BifrostResponse, bifrostErr *schemas.BifrostError, count int) (*schemas.BifrostResponse, *schemas.BifrostError) { + // Defensive: ensure count is within valid bounds + if count < 0 { + count = 0 + } + if count > len(p.plugins) { + count = len(p.plugins) + } var err error for i := count - 1; i >= 0; i-- { plugin := p.plugins[i] @@ -105,7 +112,8 @@ func (p *PluginPipeline) RunPostHooks(ctx *context.Context, resp *schemas.Bifros } // Final logic: if both are set, error takes precedence, unless error is nil if bifrostErr != nil { - if resp != nil && bifrostErr.Error.Message == "" && bifrostErr.Error.Error == nil { + if resp != nil && bifrostErr.StatusCode == nil && bifrostErr.Error.Type == nil && + bifrostErr.Error.Message == "" && bifrostErr.Error.Error == nil { // Defensive: treat as recovery if error is empty return resp, nil } @@ -623,6 +631,7 @@ func (bifrost *Bifrost) tryTextCompletion(req *schemas.BifrostRequest, ctx conte } var result *schemas.BifrostResponse + var resp *schemas.BifrostResponse select { case result = <-msg.Response: resp, bifrostErr := pipeline.RunPostHooks(&ctx, result, nil, len(bifrost.plugins)) @@ -634,7 +643,7 @@ func (bifrost *Bifrost) tryTextCompletion(req *schemas.BifrostRequest, ctx conte return resp, nil case bifrostErrVal := <-msg.Err: bifrostErrPtr := &bifrostErrVal - resp, bifrostErrPtr := pipeline.RunPostHooks(&ctx, nil, bifrostErrPtr, len(bifrost.plugins)) + resp, bifrostErrPtr = pipeline.RunPostHooks(&ctx, nil, bifrostErrPtr, len(bifrost.plugins)) bifrost.releaseChannelMessage(msg) if bifrostErrPtr != nil { return nil, bifrostErrPtr @@ -742,6 +751,7 @@ func (bifrost *Bifrost) tryChatCompletion(req *schemas.BifrostRequest, ctx conte } var result *schemas.BifrostResponse + var resp *schemas.BifrostResponse select { case result = <-msg.Response: resp, bifrostErr := pipeline.RunPostHooks(&ctx, result, nil, len(bifrost.plugins)) @@ -753,7 +763,7 @@ func (bifrost *Bifrost) tryChatCompletion(req *schemas.BifrostRequest, ctx conte return resp, nil case bifrostErrVal := <-msg.Err: bifrostErrPtr := &bifrostErrVal - resp, bifrostErrPtr := pipeline.RunPostHooks(&ctx, nil, bifrostErrPtr, len(bifrost.plugins)) + resp, bifrostErrPtr = pipeline.RunPostHooks(&ctx, nil, bifrostErrPtr, len(bifrost.plugins)) bifrost.releaseChannelMessage(msg) if bifrostErrPtr != nil { return nil, bifrostErrPtr diff --git a/core/providers/anthropic.go b/core/providers/anthropic.go index eaa81cd07b..4afe105230 100644 --- a/core/providers/anthropic.go +++ b/core/providers/anthropic.go @@ -77,8 +77,10 @@ type AnthropicImageContent struct { // AnthropicProvider implements the Provider interface for Anthropic's Claude API. type AnthropicProvider struct { - logger schemas.Logger // Logger for provider operations - client *fasthttp.Client // HTTP client for API requests + logger schemas.Logger // Logger for provider operations + client *fasthttp.Client // HTTP client for API requests + baseURL string // Base URL for the provider + apiVersion string // API version for the provider } // anthropicChatResponsePool provides a pool for Anthropic chat response objects. @@ -145,9 +147,16 @@ func NewAnthropicProvider(config *schemas.ProviderConfig, logger schemas.Logger) // Configure proxy if provided client = configureProxy(client, config.ProxyConfig, logger) + baseURL := strings.TrimRight(config.NetworkConfig.BaseURL, "/") + if baseURL == "" { + baseURL = "https://api.anthropic.com" + } + return &AnthropicProvider{ - logger: logger, - client: client, + logger: logger, + client: client, + baseURL: baseURL, + apiVersion: "2023-06-01", } } @@ -198,7 +207,7 @@ func (provider *AnthropicProvider) completeRequest(ctx context.Context, requestB req.Header.SetMethod("POST") req.Header.SetContentType("application/json") req.Header.Set("x-api-key", key) - req.Header.Set("anthropic-version", "2023-06-01") + req.Header.Set("anthropic-version", provider.apiVersion) req.SetBody(jsonData) // Send the request @@ -238,7 +247,7 @@ func (provider *AnthropicProvider) TextCompletion(ctx context.Context, model, ke "prompt": fmt.Sprintf("\n\nHuman: %s\n\nAssistant:", text), }, preparedParams) - responseBody, err := provider.completeRequest(ctx, requestBody, "https://api.anthropic.com/v1/complete", key) + responseBody, err := provider.completeRequest(ctx, requestBody, provider.baseURL+"/v1/complete", key) if err != nil { return nil, err } @@ -294,7 +303,7 @@ func (provider *AnthropicProvider) ChatCompletion(ctx context.Context, model, ke "messages": formattedMessages, }, preparedParams) - responseBody, err := provider.completeRequest(ctx, requestBody, "https://api.anthropic.com/v1/messages", key) + responseBody, err := provider.completeRequest(ctx, requestBody, provider.baseURL+"/v1/messages", key) if err != nil { return nil, err } diff --git a/core/providers/cohere.go b/core/providers/cohere.go index 5d6445890d..6ea1414ab9 100644 --- a/core/providers/cohere.go +++ b/core/providers/cohere.go @@ -95,8 +95,9 @@ type CohereError struct { // CohereProvider implements the Provider interface for Cohere. type CohereProvider struct { - logger schemas.Logger // Logger for provider operations - client *fasthttp.Client // HTTP client for API requests + logger schemas.Logger // Logger for provider operations + client *fasthttp.Client // HTTP client for API requests + baseURL string // Base URL for the provider } // NewCohereProvider creates a new Cohere provider instance. @@ -117,9 +118,15 @@ func NewCohereProvider(config *schemas.ProviderConfig, logger schemas.Logger) *C bifrostResponsePool.Put(&schemas.BifrostResponse{}) } + baseURL := strings.TrimRight(config.NetworkConfig.BaseURL, "/") + if baseURL == "" { + baseURL = "https://api.cohere.ai" + } + return &CohereProvider{ - logger: logger, - client: client, + logger: logger, + client: client, + baseURL: baseURL, } } @@ -339,7 +346,7 @@ func (provider *CohereProvider) ChatCompletion(ctx context.Context, model, key s defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) - req.SetRequestURI("https://api.cohere.ai/v1/chat") + req.SetRequestURI(provider.baseURL + "/v1/chat") req.Header.SetMethod("POST") req.Header.SetContentType("application/json") req.Header.Set("Authorization", "Bearer "+key) diff --git a/core/providers/openai.go b/core/providers/openai.go index d146472236..c7601753c8 100644 --- a/core/providers/openai.go +++ b/core/providers/openai.go @@ -5,6 +5,7 @@ package providers import ( "context" "fmt" + "strings" "sync" "time" @@ -64,8 +65,9 @@ func releaseOpenAIResponse(resp *OpenAIResponse) { // OpenAIProvider implements the Provider interface for OpenAI's API. type OpenAIProvider struct { - logger schemas.Logger // Logger for provider operations - client *fasthttp.Client // HTTP client for API requests + logger schemas.Logger // Logger for provider operations + client *fasthttp.Client // HTTP client for API requests + baseURL string // Base URL for the provider } // NewOpenAIProvider creates a new OpenAI provider instance. @@ -89,9 +91,15 @@ func NewOpenAIProvider(config *schemas.ProviderConfig, logger schemas.Logger) *O // Configure proxy if provided client = configureProxy(client, config.ProxyConfig, logger) + baseURL := strings.TrimRight(config.NetworkConfig.BaseURL, "/") + if baseURL == "" { + baseURL = "https://api.openai.com" + } + return &OpenAIProvider{ - logger: logger, - client: client, + logger: logger, + client: client, + baseURL: baseURL, } } @@ -139,7 +147,7 @@ func (provider *OpenAIProvider) ChatCompletion(ctx context.Context, model, key s defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseResponse(resp) - req.SetRequestURI("https://api.openai.com/v1/chat/completions") + req.SetRequestURI(provider.baseURL + "/v1/chat/completions") req.Header.SetMethod("POST") req.Header.SetContentType("application/json") req.Header.Set("Authorization", "Bearer "+key) diff --git a/core/schemas/plugin.go b/core/schemas/plugin.go index 755427d0e9..207f3d290a 100644 --- a/core/schemas/plugin.go +++ b/core/schemas/plugin.go @@ -9,14 +9,22 @@ import "context" // User can provide multiple plugins in the BifrostConfig. // PreHooks are executed in the order they are registered. // PostHooks are executed in the reverse order of PreHooks. - +// // PreHooks and PostHooks can be used to implement custom logic, such as: // - Rate limiting // - Caching // - Logging // - Monitoring - -// No Plugin errors are returned to the caller, they are logged as warnings by the Bifrost instance. +// +// Plugin error handling: +// - No Plugin errors are returned to the caller; they are logged as warnings by the Bifrost instance. +// - PreHook and PostHook can both modify the request/response and the error. Plugins can recover from errors (set error to nil and provide a response), or invalidate a response (set response to nil and provide an error). +// - PostHook is always called with both the current response and error, and should handle either being nil. +// - Only truly empty errors (no message, no error, no status code, no type) are treated as recoveries by the pipeline. +// - If a PreHook returns a response, the provider call is skipped and only the PostHook methods of plugins that had their PreHook executed are called in reverse order. +// - The plugin pipeline ensures symmetry: for every PreHook executed, the corresponding PostHook will be called in reverse order. +// +// Plugin authors should ensure their hooks are robust to both response and error being nil, and should not assume either is always present. type Plugin interface { // GetName returns the name of the plugin. @@ -29,9 +37,10 @@ type Plugin interface { // If a response is returned, the provider call is skipped and only the PostHook methods of plugins that had their PreHook executed are called in reverse order. PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *BifrostResponse, error) - // PostHook is called after a response is received from a provider. - // It allows plugins to modify the response/error before it is returned to the caller. - // Returns the modified response, bifrost error and any error that occurred during processing. + // PostHook is called after a response is received from a provider or a PreHook short-circuit. + // It allows plugins to modify the response and/or error before it is returned to the caller. + // Plugins can recover from errors (set error to nil and provide a response), or invalidate a response (set response to nil and provide an error). + // Returns the modified response, bifrost error, and any error that occurred during processing. PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) // Cleanup is called on bifrost shutdown. diff --git a/core/schemas/provider.go b/core/schemas/provider.go index 405bdff7c4..646ea38d84 100644 --- a/core/schemas/provider.go +++ b/core/schemas/provider.go @@ -27,6 +27,8 @@ const ( // NetworkConfig represents the network configuration for provider connections. type NetworkConfig struct { + // BaseURL is only supported for OpenAI, Anthropic and Cohere providers + BaseURL string `json:"base_url,omitempty"` // Base URL for the provider (optional) DefaultRequestTimeoutInSeconds int `json:"default_request_timeout_in_seconds"` // Default timeout for requests MaxRetries int `json:"max_retries"` // Maximum number of retries RetryBackoffInitial time.Duration `json:"retry_backoff_initial"` // Initial backoff duration diff --git a/docs/plugins.md b/docs/plugins.md index 3e3bf285ac..1d7dc29635 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -4,7 +4,7 @@ Bifrost provides a powerful plugin system that allows you to extend and customiz ## 1. How Plugins Work -Plugins in Bifrost follow a simple but powerful interface that allows them to intercept and modify requests and responses at different stages of processing: +Plugins in Bifrost follow a flexible interface that allows them to intercept and modify requests and responses at different stages of processing: 1. **PreHook**: Executed before a request is sent to a provider @@ -12,56 +12,57 @@ Plugins in Bifrost follow a simple but powerful interface that allows them to in - Can add custom headers or parameters - Can implement rate limiting or validation - Executed in the order they are registered + - If a PreHook returns a response, the provider call is skipped and only the PostHook methods of plugins that had their PreHook executed are called in reverse order. -2. **PostHook**: Executed after receiving a response from a provider - - Can modify the response - - Can implement caching - - Can add monitoring or logging +2. **PostHook**: Executed after receiving a response from a provider or a PreHook short-circuit + - Can modify the response and/or error + - Can recover from errors (set error to nil and provide a response) + - Can invalidate a response (set response to nil and provide an error) + - Both response and error may be nil; plugins must handle both cases - Executed in reverse order of PreHooks + - Only truly empty errors (no message, no error, no status code, no type) are treated as recoveries by the pipeline -> **Note**: PostHooks maintain symmetry with PreHooks. If a plugin returns a response in its PreHook (short-circuiting the provider call), only the PostHook methods of plugins that had their PreHook executed are called, in reverse order. This ensures proper request/response pairing for each plugin. +> **Note**: The plugin pipeline ensures symmetry: for every PreHook executed, the corresponding PostHook will be called in reverse order. Plugin authors should ensure their hooks are robust to both response and error being nil, and should not assume either is always present. ## 2. Plugin Interface -```golang -type Plugin interface { - // PreHook is called before a request is processed by a provider. - // It allows plugins to modify the request before it is sent to the provider. - // The context parameter can be used to maintain state across plugin calls. - // Returns the modified request, an optional response (if the plugin wants to short-circuit the provider call), and any error that occurred during processing. - // If a response is returned, the provider call is skipped and only the PostHook methods of plugins that had their PreHook executed are called in reverse order. - PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *BifrostResponse, error) - - // PostHook is called after a response is received from a provider. - // It allows plugins to modify the response before it is returned to the caller. - // Returns the modified response and any error that occurred during processing. - PostHook(ctx *context.Context, result *BifrostResponse) (*BifrostResponse, error) - - // Cleanup is called on bifrost shutdown. - // It allows plugins to clean up any resources they have allocated. - // Returns any error that occurred during cleanup, which will be logged as a warning by the Bifrost instance. - Cleanup() error -} +```go +// Plugin interface for Bifrost plugins +// See core/schemas/plugin.go for the authoritative definition + +GetName() string +PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *BifrostResponse, error) +PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) +Cleanup() error ``` ## 3. Building Custom Plugins ### Basic Plugin Structure -```golang +```go +// Example plugin skeleton + type CustomPlugin struct { // Your plugin fields } +func (p *CustomPlugin) GetName() string { + return "CustomPlugin" +} + func (p *CustomPlugin) PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *BifrostResponse, error) { // Modify request or add custom logic // Return nil for response to continue with provider call return req, nil, nil } -func (p *CustomPlugin) PostHook(ctx *context.Context, result *BifrostResponse) (*BifrostResponse, error) { - // Modify response or add custom logic - return result, nil +func (p *CustomPlugin) PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) { + // Modify response or error, or recover from error + // Either result or err may be nil + // To recover from an error, set err to nil and return a valid result + // To invalidate a response, set the result to nil and return a non-nil err. + return result, err, nil } func (p *CustomPlugin) Cleanup() error { @@ -72,11 +73,15 @@ func (p *CustomPlugin) Cleanup() error { ### Example: Rate Limiting Plugin -```golang +```go type RateLimitPlugin struct { limiter *rate.Limiter } +func (p *RateLimitPlugin) GetName() string { + return "RateLimitPlugin" +} + func NewRateLimitPlugin(rps float64) *RateLimitPlugin { return &RateLimitPlugin{ limiter: rate.NewLimiter(rate.Limit(rps), 1), @@ -90,8 +95,9 @@ func (p *RateLimitPlugin) PreHook(ctx *context.Context, req *BifrostRequest) (*B return req, nil, nil } -func (p *RateLimitPlugin) PostHook(ctx *context.Context, result *BifrostResponse) (*BifrostResponse, error) { - return result, nil +func (p *RateLimitPlugin) PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) { + // No-op for rate limiting + return result, err, nil } func (p *RateLimitPlugin) Cleanup() error { @@ -102,11 +108,15 @@ func (p *RateLimitPlugin) Cleanup() error { ### Example: Logging Plugin -```golang +```go type LoggingPlugin struct { logger schemas.Logger } +func (p *LoggingPlugin) GetName() string { + return "LoggingPlugin" +} + func NewLoggingPlugin(logger schemas.Logger) *LoggingPlugin { return &LoggingPlugin{logger: logger} } @@ -116,9 +126,14 @@ func (p *LoggingPlugin) PreHook(ctx *context.Context, req *BifrostRequest) (*Bif return req, nil, nil } -func (p *LoggingPlugin) PostHook(ctx *context.Context, result *BifrostResponse) (*BifrostResponse, error) { - p.logger.Info(fmt.Sprintf("Response from %s with %d tokens", result.Model, result.Usage.TotalTokens)) - return result, nil +func (p *LoggingPlugin) PostHook(ctx *context.Context, result *BifrostResponse, err *BifrostError) (*BifrostResponse, *BifrostError, error) { + if result != nil { + p.logger.Info(fmt.Sprintf("Response from %s with %d tokens", result.Model, result.Usage.TotalTokens)) + } + if err != nil { + p.logger.Warn(fmt.Sprintf("Error: %v", err.Error.Message)) + } + return result, err, nil } func (p *LoggingPlugin) Cleanup() error { @@ -131,7 +146,7 @@ func (p *LoggingPlugin) Cleanup() error { ### Initializing Bifrost with Plugins -```golang +```go client, err := bifrost.Init(schemas.BifrostConfig{ Account: &yourAccount, Plugins: []schemas.Plugin{ @@ -142,67 +157,30 @@ client, err := bifrost.Init(schemas.BifrostConfig{ }) ``` -## 5. Available Plugins - -Bifrost comes with several built-in plugins that you can use out of the box. Each plugin has its own documentation in its respective folder: - -- **Rate Limiting**: `plugins/rate-limiting/` -- **Caching**: `plugins/caching/` -- **Monitoring**: `plugins/monitoring/` -- **Logging**: `plugins/logging/` - -To use these plugins, you can import them from their respective packages: - -```golang -import ( - "github.com/maximhq/bifrost/plugins/rate-limiting" - "github.com/maximhq/bifrost/plugins/caching" - // ... other plugin imports -) - -// Initialize with built-in plugins -client, err := bifrost.Init(schemas.BifrostConfig{ - Account: &yourAccount, - Plugins: []schemas.Plugin{ - rate_limiting.New(10.0), - caching.New(cacheConfig), - // ... other plugins - }, -}) -``` - -## 6. Best Practices - -1. **Plugin Order** - - - Consider the order of plugins carefully - - Rate limiting plugins should typically be first - - Logging plugins should be last to capture all modifications - -2. **Error Handling** +## 5. Plugin Pipeline Symmetry and Order - - Always handle errors in both PreHook and PostHook - - Return meaningful error messages - - Consider the impact of errors on the request pipeline +- PreHooks are executed in the order they are registered. +- PostHooks are executed in the reverse order of PreHooks. +- If a PreHook returns a response, the provider call is skipped and only the PostHook methods of plugins that had their PreHook executed are called in reverse order. +- The plugin pipeline ensures that for every PreHook executed, the corresponding PostHook will be called. -3. **Performance** +## 6. Best Practices for Plugin Authors - - Keep plugin logic lightweight - - Avoid blocking operations in hooks - - Use context for cancellation +- Always check for both response and error being nil in PostHook. +- Do not assume either is always present. +- To recover from an error, set err to nil and return a valid result. Only truly empty errors (no message, no error, no status code, no type) are treated as recoveries by the pipeline. +- To invalidate a response, set the result to nil and return a non-nil err. +- Keep plugin logic lightweight and avoid blocking operations in hooks. +- Use context for cancellation and state passing. +- Write unit tests for your plugins, including error recovery and response invalidation scenarios. -4. **State Management** +## 7. Available Plugins - - Be careful with shared state between hooks - - Use context for passing data between hooks - - Consider thread safety for concurrent requests +Bifrost provides a **[Plugin Store](https://github.com/maximhq/bifrost/tree/main/plugins)** with one-line integrations. For each plugin, refer to its specific documentation for configuration and usage details: -5. **Testing** - - Write unit tests for your plugins - - Test error scenarios - - Verify plugin order and execution +`https://github.com/maximhq/bifrost/tree/main/plugins/{plugin_name}` -## 7. Plugin Development Guidelines +## 8. Plugin Development Guidelines 1. **Documentation** @@ -242,7 +220,7 @@ client, err := bifrost.Init(schemas.BifrostConfig{ Example `main.go`: - ```golang + ```go package your_plugin_name import ( @@ -254,10 +232,8 @@ client, err := bifrost.Init(schemas.BifrostConfig{ // Plugin fields } - func New(config YourPluginConfig) *YourPlugin { - return &YourPlugin{ - // Initialize plugin - } + func (p *YourPlugin) GetName() string { + return "YourPlugin" } func (p *YourPlugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.BifrostResponse, error) { @@ -265,9 +241,9 @@ client, err := bifrost.Init(schemas.BifrostConfig{ return req, nil, nil } - func (p *YourPlugin) PostHook(ctx *context.Context, result *schemas.BifrostResponse) (*schemas.BifrostResponse, error) { + func (p *YourPlugin) PostHook(ctx *context.Context, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) { // Implementation - return result, nil + return result, err, nil } func (p *YourPlugin) Cleanup() error {