Files
bifrost/core/schemas/plugin.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

328 lines
14 KiB
Go

// Package schemas defines the core schemas and types used by the Bifrost system.
package schemas
import (
"context"
"strings"
"sync"
)
// PluginStatus constants
const (
PluginStatusActive = "active"
PluginStatusError = "error"
PluginStatusDisabled = "disabled"
PluginStatusLoading = "loading"
PluginStatusUninitialized = "uninitialized"
PluginStatusUnloaded = "unloaded"
PluginStatusLoaded = "loaded"
)
// PluginStatus represents the status of a plugin.
type PluginStatus struct {
Name string `json:"name"` // Display name of the plugin
Status string `json:"status"`
Logs []string `json:"logs"`
Types []PluginType `json:"types"` // Plugin types (LLM, MCP, HTTP)
}
// PluginType represents the type of plugin.
type PluginType string
const (
PluginTypeLLM PluginType = "llm"
PluginTypeMCP PluginType = "mcp"
PluginTypeHTTP PluginType = "http"
)
// HTTPRequest is a serializable representation of an HTTP request.
// Used for plugin HTTP transport interception (supports both native .so and WASM plugins).
// This type is pooled for allocation control - use AcquireHTTPRequest and ReleaseHTTPRequest.
type HTTPRequest struct {
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
Query map[string]string `json:"query"`
Body []byte `json:"body"`
PathParams map[string]string `json:"path_params"` // Path variables extracted from the URL pattern (e.g., {model})
}
// CaseInsensitiveHeaderLookup looks up a header key in a case-insensitive manner
func (req *HTTPRequest) CaseInsensitiveHeaderLookup(key string) string {
return caseInsensitiveLookup(req.Headers, key)
}
// CaseInsensitiveQueryLookup looks up a query key in a case-insensitive manner
func (req *HTTPRequest) CaseInsensitiveQueryLookup(key string) string {
return caseInsensitiveLookup(req.Query, key)
}
// CaseInsensitivePathParamLookup looks up a path parameter key in a case-insensitive manner
func (req *HTTPRequest) CaseInsensitivePathParamLookup(key string) string {
return caseInsensitiveLookup(req.PathParams, key)
}
// CaseInsensitiveLookup looks up a key in a case-insensitive manner for a map of strings
// Returns the value if found, otherwise an empty string
func caseInsensitiveLookup(data map[string]string, key string) string {
if data == nil || key == "" {
return ""
}
// exact match
if v, ok := data[key]; ok {
return v
}
// lower key checks
lowerKey := strings.ToLower(key)
if v, ok := data[lowerKey]; ok {
return v
}
// case-insensitive iteration
for k, v := range data {
if strings.EqualFold(k, key) {
return v
}
}
return ""
}
// HTTPResponse is a serializable representation of an HTTP response.
// Used for short-circuit responses in plugin HTTP transport interception.
// This type is pooled for allocation control - use AcquireHTTPResponse and ReleaseHTTPResponse.
type HTTPResponse struct {
StatusCode int `json:"status_code"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}
// httpRequestPool is the pool for HTTPRequest objects to reduce allocations.
var httpRequestPool = sync.Pool{
New: func() any {
return &HTTPRequest{
Headers: make(map[string]string, 16),
Query: make(map[string]string, 8),
PathParams: make(map[string]string, 4),
}
},
}
// AcquireHTTPRequest gets an HTTPRequest from the pool.
// The returned HTTPRequest is ready to use with pre-allocated maps.
// Call ReleaseHTTPRequest when done to return it to the pool.
func AcquireHTTPRequest() *HTTPRequest {
return httpRequestPool.Get().(*HTTPRequest)
}
// ReleaseHTTPRequest returns an HTTPRequest to the pool.
// The HTTPRequest is reset before being returned to the pool.
// Do not use the HTTPRequest after calling this function.
func ReleaseHTTPRequest(req *HTTPRequest) {
if req == nil {
return
}
// Clear the maps
clear(req.Headers)
clear(req.Query)
clear(req.PathParams)
// Reset fields
req.Method = ""
req.Path = ""
req.Body = nil
httpRequestPool.Put(req)
}
// httpResponsePool is the pool for HTTPResponse objects to reduce allocations.
var httpResponsePool = sync.Pool{
New: func() any {
return &HTTPResponse{
Headers: make(map[string]string, 8),
}
},
}
// AcquireHTTPResponse gets an HTTPResponse from the pool.
// The returned HTTPResponse is ready to use with a pre-allocated Headers map.
// Call ReleaseHTTPResponse when done to return it to the pool.
func AcquireHTTPResponse() *HTTPResponse {
return httpResponsePool.Get().(*HTTPResponse)
}
// ReleaseHTTPResponse returns an HTTPResponse to the pool.
// The HTTPResponse is reset before being returned to the pool.
// Do not use the HTTPResponse after calling this function.
func ReleaseHTTPResponse(resp *HTTPResponse) {
if resp == nil {
return
}
// Clear the map
clear(resp.Headers)
// Reset fields
resp.StatusCode = 0
resp.Body = nil
httpResponsePool.Put(resp)
}
// Plugin defines the interface for Bifrost plugins.
// Plugins can intercept and modify requests and responses at different stages
// of the processing pipeline.
// 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.
//
// Execution order:
// 1. HTTPTransportPreHook (HTTP transport only, executed in registration order)
// 2. PreLLMHook (executed in registration order)
// 3. Provider call
// 4. PostLLMHook (executed in reverse order of PreHooks)
// 5. HTTPTransportPostHook (HTTP transport only, executed in reverse order)
// 5a. HTTPTransportStreamChunkHook (for streaming responses, called per-chunk in reverse order)
//
// Common use cases: rate limiting, caching, logging, monitoring, request transformation, governance.
//
// Plugin error handling:
// - No Plugin errors are returned to the caller; they are logged as warnings by the Bifrost instance.
// - PreLLMHook and PostLLMHook 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).
// - PostLLMHook 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 PreLLMHook returns a LLMPluginShortCircuit, the provider call may be skipped and only the PostLLMHook methods of plugins that had their PreLLMHook executed are called in reverse order.
// - The plugin pipeline ensures symmetry: for every PreLLMHook executed, the corresponding PostLLMHook will be called in reverse order.
//
// IMPORTANT: When returning BifrostError from PreLLMHook or PostLLMHook:
// - 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 BasePlugin interface {
// GetName returns the name of the plugin.
GetName() string
// 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
}
type HTTPTransportPlugin interface {
BasePlugin
// HTTPTransportPreHook is called at the HTTP transport layer before requests enter Bifrost core.
// It receives a serializable HTTPRequest and allows plugins to modify it in-place.
// Only invoked when using HTTP transport (bifrost-http), not when using Bifrost as a Go SDK directly.
// Works with both native .so plugins and WASM plugins due to serializable types.
//
// Return values:
// - (nil, nil): Continue to next plugin/handler, request modifications are applied
// - (*HTTPResponse, nil): Short-circuit with this response, skip remaining plugins and provider call
// - (nil, error): Short-circuit with error response
//
// Return nil for both values if the plugin doesn't need HTTP transport interception.
HTTPTransportPreHook(ctx *BifrostContext, req *HTTPRequest) (*HTTPResponse, error)
// HTTPTransportPostHook is called at the HTTP transport layer after requests exit Bifrost core.
// It receives a serializable HTTPRequest and HTTPResponse and allows plugins to modify it in-place.
// Only invoked when using HTTP transport (bifrost-http), not when using Bifrost as a Go SDK directly.
// Works with both native .so plugins and WASM plugins due to serializable types.
// NOTE: This hook is NOT called for streaming responses. Use HTTPTransportStreamChunkHook instead.
// NOTE: For large streamed responses (non-streaming APIs that switch to body streaming for memory safety),
// resp.Body may be nil by design while StatusCode and Headers remain populated.
//
// Return values:
// - nil: Continue to next plugin/handler, response modifications are applied
// - error: Short-circuit with error response and skip remaining plugins
//
// Return nil if the plugin doesn't need HTTP transport interception.
HTTPTransportPostHook(ctx *BifrostContext, req *HTTPRequest, resp *HTTPResponse) error
// HTTPTransportStreamChunkHook is called for each chunk during streaming responses.
// It receives the BifrostStreamChunk BEFORE they are written to the client.
// Only invoked for streaming responses when using HTTP transport (bifrost-http).
// Works with both native .so plugins and WASM plugins due to serializable types.
//
// Plugins can modify the chunk by returning a different BifrostStreamChunk.
// Return the original chunk unchanged if no modification is needed.
//
// Return values:
// - (*BifrostStreamChunk, nil): Continue with the (potentially modified) BifrostStreamChunk
// - (nil, nil): Skip this BifrostStreamChunk entirely (don't send to client)
// - (*BifrostStreamChunk, error): Log warning and continue with the BifrostStreamChunk
// - (nil, error): Send back error to the client and stop the streaming
//
// Return (*BifrostStreamChunk, nil) unchanged if the plugin doesn't need streaming chunk interception.
HTTPTransportStreamChunkHook(ctx *BifrostContext, req *HTTPRequest, chunk *BifrostStreamChunk) (*BifrostStreamChunk, error)
}
type LLMPlugin interface {
BasePlugin
PreLLMHook(ctx *BifrostContext, req *BifrostRequest) (*BifrostRequest, *LLMPluginShortCircuit, error)
PostLLMHook(ctx *BifrostContext, resp *BifrostResponse, bifrostErr *BifrostError) (*BifrostResponse, *BifrostError, error)
}
type MCPPlugin interface {
BasePlugin
PreMCPHook(ctx *BifrostContext, req *BifrostMCPRequest) (*BifrostMCPRequest, *MCPPluginShortCircuit, error)
PostMCPHook(ctx *BifrostContext, resp *BifrostMCPResponse, bifrostErr *BifrostError) (*BifrostMCPResponse, *BifrostError, error)
}
// Plugin placement constants control where custom plugins execute relative to built-in plugins.
type PluginPlacement string
const (
PluginPlacementPostBuiltin PluginPlacement = "post_builtin"
PluginPlacementPreBuiltin PluginPlacement = "pre_builtin"
PluginPlacementBuiltin PluginPlacement = "builtin"
PluginPlacementDefault PluginPlacement = PluginPlacementPostBuiltin
)
// PluginConfig is the configuration for a plugin.
// It contains the name of the plugin, whether it is enabled, and the configuration for the plugin.
type PluginConfig struct {
Enabled bool `json:"enabled"`
Name string `json:"name"`
Path *string `json:"path,omitempty"`
Version *int16 `json:"version,omitempty"`
Config any `json:"config,omitempty"`
Placement *PluginPlacement `json:"placement,omitempty"` // "pre_builtin" or "post_builtin". Default: "post_builtin"
Order *int `json:"order,omitempty"` // Position within placement group. Lower = earlier. Default: 0
}
// ObservabilityPlugin is an interface for plugins that receive completed traces
// for forwarding to observability backends (e.g., OTEL collectors, Datadog, etc.)
//
// ObservabilityPlugins are called asynchronously after the HTTP response has been
// written to the wire, ensuring they don't add latency to the client response.
//
// Plugins implementing this interface will:
// 1. Continue to work as regular plugins via PreLLMHook/PostLLMHook
// 2. Additionally receive completed traces via the Inject method
//
// Example backends: OpenTelemetry collectors, Datadog, Jaeger, Maxim, etc.
//
// Note: Go type assertion (plugin.(ObservabilityPlugin)) is used to identify
// plugins implementing this interface - no marker method is needed.
type ObservabilityPlugin interface {
BasePlugin
// Inject receives a completed trace for forwarding to observability backends.
// This method is called asynchronously after the response has been written to the client.
// The trace contains all spans that were added during request processing.
//
// Implementations should:
// - Convert the trace to their backend's format
// - Send the trace to the backend (can be async, but see retention note below)
// - Handle errors gracefully (log and continue)
//
// The context passed is a fresh background context, not the request context.
//
// Retention: implementations MUST NOT retain the *Trace pointer after Inject
// returns. The caller releases the trace back to a sync.Pool immediately after
// Inject completes, so any background goroutine that still references it will
// race with pool reuse. If a plugin needs to forward the trace asynchronously,
// it must copy the data it needs before returning.
Inject(ctx context.Context, trace *Trace) error
}