Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 54 additions & 19 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,19 @@ func NewPluginPipeline(plugins []schemas.Plugin, logger schemas.Logger) *PluginP
}
}

// 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
// RunPreHooks executes PreHooks in order, tracks how many ran, and returns the final request, any short-circuit decision, and the count.
func (p *PluginPipeline) RunPreHooks(ctx *context.Context, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, int) {
var shortCircuit *schemas.PluginShortCircuit
var err error
for i, plugin := range p.plugins {
req, resp, err = plugin.PreHook(ctx, req)
req, shortCircuit, err = plugin.PreHook(ctx, req)
if err != nil {
p.preHookErrors = append(p.preHookErrors, err)
p.logger.Warn(fmt.Sprintf("Error in PreHook for plugin %s: %v", plugin.GetName(), err))
}
p.executedPreHooks = i + 1
if resp != nil {
return req, resp, p.executedPreHooks // short-circuit: only plugins up to and including i ran
if shortCircuit != nil {
return req, shortCircuit, p.executedPreHooks // short-circuit: only plugins up to and including i ran
}
}
return req, nil, p.executedPreHooks
Expand Down Expand Up @@ -571,7 +571,14 @@ func (bifrost *Bifrost) TextCompletionRequest(ctx context.Context, req *schemas.
return nil, primaryErr
}

// Check if this is a short-circuit error that doesn't allow fallbacks
// Note: AllowFallbacks = nil is treated as true (allow fallbacks by default)
if primaryErr.AllowFallbacks != nil && !*primaryErr.AllowFallbacks {
return nil, primaryErr
}

// If primary provider failed and we have fallbacks, try them in order
// This includes both regular provider errors and plugin short-circuit errors with AllowFallbacks=true/nil
if len(req.Fallbacks) > 0 {
for _, fallback := range req.Fallbacks {
// Check if we have config for this fallback provider
Expand Down Expand Up @@ -618,14 +625,24 @@ func (bifrost *Bifrost) tryTextCompletion(req *schemas.BifrostRequest, ctx conte
}

pipeline := NewPluginPipeline(bifrost.plugins, bifrost.logger)
preReq, preResp, preCount := pipeline.RunPreHooks(&ctx, req)
if preResp != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, preResp, nil, preCount)
// If PostHooks recovered from error, return resp; if not, return error
if bifrostErr != nil {
return nil, bifrostErr
preReq, shortCircuit, preCount := pipeline.RunPreHooks(&ctx, req)
if shortCircuit != nil {
// Handle short-circuit with response (success case)
if shortCircuit.Response != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, shortCircuit.Response, nil, preCount)
if bifrostErr != nil {
return nil, bifrostErr
}
return resp, nil
}
// Handle short-circuit with error
if shortCircuit.Error != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, nil, shortCircuit.Error, preCount)
if bifrostErr != nil {
return nil, bifrostErr
}
return resp, nil
}
return resp, nil
}
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
if preReq == nil {
return nil, newBifrostErrorFromMsg("bifrost request after plugin hooks cannot be nil")
Expand Down Expand Up @@ -702,7 +719,14 @@ func (bifrost *Bifrost) ChatCompletionRequest(ctx context.Context, req *schemas.
return primaryResult, nil
}

// Check if this is a short-circuit error that doesn't allow fallbacks
// Note: AllowFallbacks = nil is treated as true (allow fallbacks by default)
if primaryErr.AllowFallbacks != nil && !*primaryErr.AllowFallbacks {
return nil, primaryErr
}

// If primary provider failed and we have fallbacks, try them in order
// This includes both regular provider errors and plugin short-circuit errors with AllowFallbacks=true/nil
if len(req.Fallbacks) > 0 {
for _, fallback := range req.Fallbacks {
// Check if we have config for this fallback provider
Expand Down Expand Up @@ -749,13 +773,24 @@ func (bifrost *Bifrost) tryChatCompletion(req *schemas.BifrostRequest, ctx conte
}

pipeline := NewPluginPipeline(bifrost.plugins, bifrost.logger)
preReq, preResp, preCount := pipeline.RunPreHooks(&ctx, req)
if preResp != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, preResp, nil, preCount)
if bifrostErr != nil {
return nil, bifrostErr
preReq, shortCircuit, preCount := pipeline.RunPreHooks(&ctx, req)
if shortCircuit != nil {
// Handle short-circuit with response (success case)
if shortCircuit.Response != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, shortCircuit.Response, nil, preCount)
if bifrostErr != nil {
return nil, bifrostErr
}
return resp, nil
}
// Handle short-circuit with error
if shortCircuit.Error != nil {
resp, bifrostErr := pipeline.RunPostHooks(&ctx, nil, shortCircuit.Error, preCount)
if bifrostErr != nil {
return nil, bifrostErr
}
return resp, nil
}
return resp, nil
}
if preReq == nil {
return nil, newBifrostErrorFromMsg("bifrost request after plugin hooks cannot be nil")
Expand Down
6 changes: 6 additions & 0 deletions core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,12 +415,18 @@ const (
)

// BifrostError represents an error from the Bifrost system.
//
// PLUGIN DEVELOPERS: When creating BifrostError in PreHook or PostHook, you can set AllowFallbacks:
// - AllowFallbacks = &true: Bifrost will try fallback providers if available
// - AllowFallbacks = &false: Bifrost will return this error immediately, no fallbacks
// - AllowFallbacks = nil: Treated as true by default (fallbacks allowed for resilience)
type BifrostError struct {
EventID *string `json:"event_id,omitempty"`
Type *string `json:"type,omitempty"`
IsBifrostError bool `json:"is_bifrost_error"`
StatusCode *int `json:"status_code,omitempty"`
Error ErrorField `json:"error"`
AllowFallbacks *bool `json:"allow_fallbacks,omitempty"` // Optional: Controls fallback behavior (nil = true by default)
}

// ErrorField represents detailed error information.
Expand Down
20 changes: 16 additions & 4 deletions core/schemas/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ package schemas

import "context"

// PluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
// It can contain either a response (success short-circuit) or an error (error short-circuit).
type PluginShortCircuit struct {
Response *BifrostResponse // If set, short-circuit with this response (skips provider call)
Error *BifrostError // If set, short-circuit with this error (can set AllowFallbacks field)
}
Comment thread
Pratham-Mishra04 marked this conversation as resolved.

// Plugin defines the interface for Bifrost plugins.
// Plugins can intercept and modify requests and responses at different stages
// of the processing pipeline.
Expand All @@ -21,9 +28,15 @@ import "context"
// - 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.
// - If a PreHook returns a PluginShortCircuit, the provider call may be 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.
//
// IMPORTANT: When returning BifrostError from PreHook or PostHook:
// - You can set the AllowFallbacks field to control fallback behavior
// - AllowFallbacks = &true: Allow Bifrost to try fallback providers
// - AllowFallbacks = &false: Do not try fallbacks, return error immediately
// - AllowFallbacks = nil: Treated as true by default (allow fallbacks for resilience)
//
// 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 {
Expand All @@ -33,9 +46,8 @@ 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)
// Returns the modified request, an optional short-circuit decision, and any error that occurred during processing.
PreHook(ctx *context.Context, req *BifrostRequest) (*BifrostRequest, *PluginShortCircuit, error)
Comment thread
Pratham-Mishra04 marked this conversation as resolved.

// 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.
Expand Down
Loading