328 lines
14 KiB
Go
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
|
|
}
|