645 lines
25 KiB
Go
645 lines
25 KiB
Go
// Package lib provides core functionality for the Bifrost HTTP service,
|
|
// including context propagation, header management, and integration with monitoring systems.
|
|
//
|
|
// This package handles the conversion of FastHTTP request contexts to Bifrost contexts,
|
|
// ensuring that important metadata and tracking information is preserved across the system.
|
|
// It supports propagation of both Prometheus metrics and Maxim tracing data through HTTP headers.
|
|
package lib
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/plugins/governance"
|
|
"github.com/maximhq/bifrost/plugins/maxim"
|
|
"github.com/maximhq/bifrost/plugins/semanticcache"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
const (
|
|
// FastHTTPUserValueBifrostContext stores the active *schemas.BifrostContext on fasthttp.RequestCtx.
|
|
// This allows transport middleware and request handlers to share the same context instance.
|
|
FastHTTPUserValueBifrostContext = "__bifrost_context"
|
|
// FastHTTPUserValueBifrostCancel stores the cancel func for the active shared Bifrost context.
|
|
FastHTTPUserValueBifrostCancel = "__bifrost_context_cancel"
|
|
// FastHTTPUserValueLargeResponseMode marks requests that streamed a large response body.
|
|
// It is used by transport middleware to avoid re-buffering response bodies for post-hooks.
|
|
FastHTTPUserValueLargeResponseMode = "__bifrost_large_response_mode"
|
|
)
|
|
|
|
// ParseSessionIDFromBaggage extracts the session-id baggage member value.
|
|
// It supports simple W3C baggage parsing sufficient for log grouping.
|
|
func ParseSessionIDFromBaggage(header string) string {
|
|
for _, member := range strings.Split(header, ",") {
|
|
member = strings.TrimSpace(member)
|
|
if member == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(member, ";", 2)
|
|
kv := strings.SplitN(strings.TrimSpace(parts[0]), "=", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.ToLower(strings.TrimSpace(kv[0]))
|
|
value := strings.TrimSpace(kv[1])
|
|
if key != "session-id" || value == "" {
|
|
continue
|
|
}
|
|
if len(value) > 255 {
|
|
if logger != nil {
|
|
logger.Warn("session-id exceeds 255 chars, ignoring: length=%d, prefix=%s", len(value), value[:255])
|
|
}
|
|
continue
|
|
}
|
|
return value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ConvertToBifrostContext converts a FastHTTP RequestCtx to a Bifrost context,
|
|
// preserving important header values for monitoring and tracing purposes.
|
|
//
|
|
// The function processes several types of special headers:
|
|
// 1. Prometheus Headers (x-bf-prom-*):
|
|
// - All headers prefixed with 'x-bf-prom-' are copied to the context
|
|
// - The prefix is stripped and the remainder becomes the context key
|
|
// - Example: 'x-bf-prom-latency' becomes 'latency' in the context
|
|
//
|
|
// 2. Maxim Tracing Headers (x-bf-maxim-*):
|
|
// - Specifically handles 'x-bf-maxim-traceID' and 'x-bf-maxim-generationID'
|
|
// - These headers enable trace correlation across service boundaries
|
|
// - Values are stored using Maxim's context keys for consistency
|
|
//
|
|
// 3. MCP Headers (x-bf-mcp-*):
|
|
// - Specifically handles 'x-bf-mcp-include-clients' and 'x-bf-mcp-include-tools' (include-only filtering)
|
|
// - These headers enable MCP client and tool filtering
|
|
// - Values are stored using MCP context keys for consistency
|
|
//
|
|
// 4. Governance Headers:
|
|
// - x-bf-vk: Virtual key for governance (required for governance to work)
|
|
//
|
|
// 5. API Key Headers:
|
|
// - Authorization: Bearer token format only (e.g., "Bearer sk-...") - OpenAI style
|
|
// - x-api-key: Direct API key value - Anthropic style
|
|
// - x-goog-api-key: Direct API key value - Google Gemini style
|
|
// - x-bf-api-key references a stored API key name rather than the raw secret.
|
|
// - Keys are extracted and stored in the context using schemas.BifrostContextKey
|
|
// - This enables explicit key usage for requests via headers
|
|
//
|
|
// 6. Cancellable Context:
|
|
// - Creates a cancellable context that can be used to cancel upstream requests when clients disconnect
|
|
// - This is critical for streaming requests where write errors indicate client disconnects
|
|
// - Also useful for non-streaming requests to allow provider-level cancellation
|
|
//
|
|
// 7. Extra Headers (x-bf-eh-*):
|
|
// - Any header starting with 'x-bf-eh-' is collected and added to the map stored under schemas.BifrostContextKeyExtraHeaders
|
|
// - The prefix is stripped, the remainder is lower-cased, and duplicate names append values
|
|
// - This allows callers to send arbitrary context metadata without needing to extend the public schema
|
|
//
|
|
// 8. Session Stickiness Headers:
|
|
// - x-bf-session-id: Session identifier for key binding (reuse same key across requests)
|
|
// - x-bf-session-ttl: Per-request TTL override (duration string e.g. "30m" or seconds integer)
|
|
//
|
|
// 9. Raw Capture Headers (per-request override of provider config; accepts "true" or "false"):
|
|
// - x-bf-send-back-raw-request: include raw provider request in the BifrostResponse returned to the caller
|
|
// - x-bf-send-back-raw-response: include raw provider response in the BifrostResponse returned to the caller
|
|
// - x-bf-store-raw-request-response: capture raw request/response for logging only (stripped from client response)
|
|
|
|
// Parameters:
|
|
// - ctx: The FastHTTP request context containing the original headers
|
|
// - allowDirectKeys: Whether to allow direct API key usage from headers
|
|
//
|
|
// Returns:
|
|
// - *context.Context: A new cancellable context.Context containing the propagated values
|
|
// - context.CancelFunc: Function to cancel the context (should be called when request completes)
|
|
//
|
|
// Example Usage:
|
|
//
|
|
// fastCtx := &fasthttp.RequestCtx{...}
|
|
// bifrostCtx, cancel := ConvertToBifrostContext(fastCtx, true, nil)
|
|
// defer cancel() // Ensure cleanup
|
|
// // bifrostCtx now contains propagated header values including Prometheus metrics,
|
|
// // Maxim tracing data, MCP filters, governance keys, API keys, cache settings,
|
|
// // session stickiness, and extra headers
|
|
|
|
func ConvertToBifrostContext(ctx *fasthttp.RequestCtx, allowDirectKeys bool, matcher *HeaderMatcher, mcpHeaderCombinedAllowlist schemas.WhiteList) (*schemas.BifrostContext, context.CancelFunc) {
|
|
// Reuse a shared request-scoped context when available.
|
|
var bifrostCtx *schemas.BifrostContext
|
|
var cancel context.CancelFunc
|
|
if existing, ok := ctx.UserValue(FastHTTPUserValueBifrostContext).(*schemas.BifrostContext); ok && existing != nil {
|
|
if existingCancel, ok := ctx.UserValue(FastHTTPUserValueBifrostCancel).(context.CancelFunc); ok && existingCancel != nil {
|
|
bifrostCtx = existing
|
|
cancel = existingCancel
|
|
} else {
|
|
// Create one cancellable child context and promote it as the shared context.
|
|
bifrostCtx, cancel = schemas.NewBifrostContextWithCancel(existing)
|
|
ctx.SetUserValue(FastHTTPUserValueBifrostContext, bifrostCtx)
|
|
ctx.SetUserValue(FastHTTPUserValueBifrostCancel, cancel)
|
|
}
|
|
}
|
|
if bifrostCtx == nil {
|
|
// Create cancellable context for requests that don't have a shared context yet.
|
|
parent := context.Context(ctx)
|
|
func() {
|
|
// Zero-value fasthttp.RequestCtx can panic on Done(); fall back safely.
|
|
defer func() {
|
|
if recover() != nil {
|
|
parent = context.Background()
|
|
}
|
|
}()
|
|
_ = ctx.Done()
|
|
}()
|
|
bifrostCtx, cancel = schemas.NewBifrostContextWithCancel(parent)
|
|
ctx.SetUserValue(FastHTTPUserValueBifrostContext, bifrostCtx)
|
|
ctx.SetUserValue(FastHTTPUserValueBifrostCancel, cancel)
|
|
}
|
|
|
|
// Preserve existing request-id if already present on the shared context.
|
|
if existingRequestID, ok := bifrostCtx.Value(schemas.BifrostContextKeyRequestID).(string); !ok || existingRequestID == "" {
|
|
// First, check if x-request-id header exists
|
|
requestID := string(ctx.Request.Header.Peek("x-request-id"))
|
|
if requestID == "" {
|
|
requestID = uuid.New().String()
|
|
}
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyRequestID, requestID)
|
|
}
|
|
// Populating all user values from the request context
|
|
ctx.VisitUserValuesAll(func(key, value any) {
|
|
bifrostCtx.SetValue(key, value)
|
|
})
|
|
// Initialize tags map for collecting maxim tags
|
|
maximTags := make(map[string]string)
|
|
// Initialize extra headers map for headers prefixed with x-bf-eh-
|
|
extraHeaders := make(map[string][]string)
|
|
// Initialize extra headers map for headers in the mcp header combined allowlist
|
|
mcpExtraHeaders := make(map[string][]string)
|
|
// Security denylist of header names that should never be accepted (case-insensitive)
|
|
// This denylist is always enforced regardless of user configuration
|
|
securityDenylist := map[string]bool{
|
|
"proxy-authorization": true,
|
|
"cookie": true,
|
|
"host": true,
|
|
"content-length": true,
|
|
"connection": true,
|
|
"transfer-encoding": true,
|
|
|
|
// prevent auth/key overrides via x-bf-eh-*
|
|
"x-api-key": true,
|
|
"x-goog-api-key": true,
|
|
"x-bf-api-key": true,
|
|
"x-bf-api-key-id": true,
|
|
"x-bf-vk": true,
|
|
}
|
|
|
|
// Debug: Log header matcher state
|
|
if logger != nil {
|
|
if matcher != nil {
|
|
logger.Debug("headerMatcher hasAllowlist=%v, hasDenylist=%v", matcher.HasAllowlist(), matcher.hasDenylist)
|
|
} else {
|
|
logger.Debug("headerMatcher is nil (allow all)")
|
|
}
|
|
}
|
|
|
|
// Then process other headers
|
|
ctx.Request.Header.All()(func(key, value []byte) bool {
|
|
keyStr := strings.ToLower(string(key))
|
|
if keyStr == "baggage" {
|
|
if sessionID := ParseSessionIDFromBaggage(string(value)); sessionID != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyParentRequestID, sessionID)
|
|
}
|
|
return true
|
|
}
|
|
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-prom-"); ok {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
return true
|
|
}
|
|
// Checking for maxim headers
|
|
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-maxim-"); ok {
|
|
switch labelName {
|
|
case string(maxim.GenerationIDKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
case string(maxim.TraceIDKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
case string(maxim.SessionIDKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
case string(maxim.TraceNameKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
case string(maxim.GenerationNameKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
case string(maxim.LogRepoIDKey):
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(labelName), string(value))
|
|
default:
|
|
// apart from these all headers starting with x-bf-maxim- are keys for tags
|
|
// collect them in the maximTags map
|
|
maximTags[labelName] = string(value)
|
|
}
|
|
return true
|
|
}
|
|
// MCP control headers (include-only filtering)
|
|
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-mcp-"); ok {
|
|
switch labelName {
|
|
case "include-clients":
|
|
fallthrough
|
|
case "include-tools":
|
|
// Parse comma-separated values into []string
|
|
valueStr := string(value)
|
|
var parsedValues []string
|
|
if valueStr != "" {
|
|
// Split by comma and trim whitespace
|
|
for _, v := range strings.Split(valueStr, ",") {
|
|
if trimmed := strings.TrimSpace(v); trimmed != "" {
|
|
parsedValues = append(parsedValues, trimmed)
|
|
}
|
|
}
|
|
} else {
|
|
parsedValues = []string{""}
|
|
}
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey("mcp-"+labelName), parsedValues)
|
|
return true
|
|
}
|
|
}
|
|
// Handle virtual key header (x-bf-vk, authorization, x-api-key, x-goog-api-key headers)
|
|
if keyStr == string(schemas.BifrostContextKeyVirtualKey) {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
|
|
return true
|
|
}
|
|
if keyStr == "authorization" {
|
|
valueStr := string(value)
|
|
// Only accept Bearer token format: "Bearer ..."
|
|
if strings.HasPrefix(strings.ToLower(valueStr), "bearer ") {
|
|
authHeaderValue := strings.TrimSpace(valueStr[7:]) // Remove "Bearer " prefix
|
|
if authHeaderValue != "" && strings.HasPrefix(strings.ToLower(authHeaderValue), governance.VirtualKeyPrefix) {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, authHeaderValue)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
if keyStr == "x-api-key" && strings.HasPrefix(strings.ToLower(string(value)), governance.VirtualKeyPrefix) {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
|
|
return true
|
|
}
|
|
if keyStr == "x-goog-api-key" && strings.HasPrefix(strings.ToLower(string(value)), governance.VirtualKeyPrefix) {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyVirtualKey, string(value))
|
|
return true
|
|
}
|
|
if keyStr == "x-bf-api-key" {
|
|
if keyName := strings.TrimSpace(string(value)); keyName != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyAPIKeyName, keyName)
|
|
}
|
|
return true
|
|
}
|
|
if keyStr == "x-bf-api-key-id" {
|
|
if keyID := strings.TrimSpace(string(value)); keyID != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyAPIKeyID, keyID)
|
|
}
|
|
return true
|
|
}
|
|
// Handle cache key header (x-bf-cache-key)
|
|
if keyStr == "x-bf-cache-key" {
|
|
bifrostCtx.SetValue(semanticcache.CacheKey, string(value))
|
|
return true
|
|
}
|
|
// Handle cache TTL header (x-bf-cache-ttl)
|
|
if keyStr == "x-bf-cache-ttl" {
|
|
valueStr := string(value)
|
|
var ttlDuration time.Duration
|
|
var err error
|
|
|
|
// First try to parse as duration (e.g., "30s", "5m", "1h")
|
|
if ttlDuration, err = time.ParseDuration(valueStr); err != nil {
|
|
// If that fails, try to parse as plain number and treat as seconds
|
|
if seconds, parseErr := strconv.Atoi(valueStr); parseErr == nil && seconds > 0 {
|
|
ttlDuration = time.Duration(seconds) * time.Second
|
|
err = nil // Reset error since we successfully parsed as seconds
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
bifrostCtx.SetValue(semanticcache.CacheTTLKey, ttlDuration)
|
|
}
|
|
// If both parsing attempts fail, we silently ignore the header and use default TTL
|
|
return true
|
|
}
|
|
// Cache threshold header
|
|
if keyStr == "x-bf-cache-threshold" {
|
|
threshold, err := strconv.ParseFloat(string(value), 64)
|
|
if err == nil {
|
|
// Clamp threshold to the inclusive range [0.0, 1.0]
|
|
if threshold < 0.0 {
|
|
threshold = 0.0
|
|
} else if threshold > 1.0 {
|
|
threshold = 1.0
|
|
}
|
|
bifrostCtx.SetValue(semanticcache.CacheThresholdKey, threshold)
|
|
}
|
|
// If parsing fails, silently ignore the header (no context value set)
|
|
return true
|
|
}
|
|
// Cache type header
|
|
if keyStr == "x-bf-cache-type" {
|
|
bifrostCtx.SetValue(semanticcache.CacheTypeKey, semanticcache.CacheType(string(value)))
|
|
return true
|
|
}
|
|
// Cache no store header
|
|
if keyStr == "x-bf-cache-no-store" {
|
|
if valueStr := string(value); valueStr == "true" {
|
|
bifrostCtx.SetValue(semanticcache.CacheNoStoreKey, true)
|
|
}
|
|
return true
|
|
}
|
|
// Session stickiness: session ID for key binding
|
|
if keyStr == "x-bf-session-id" {
|
|
if valueStr := strings.TrimSpace(string(value)); valueStr != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySessionID, valueStr)
|
|
}
|
|
return true
|
|
}
|
|
// Session stickiness: per-request TTL override (duration string or seconds integer)
|
|
if keyStr == "x-bf-session-ttl" {
|
|
valueStr := strings.TrimSpace(string(value))
|
|
var ttlDuration time.Duration
|
|
var err error
|
|
if ttlDuration, err = time.ParseDuration(valueStr); err != nil {
|
|
if seconds, parseErr := strconv.Atoi(valueStr); parseErr == nil && seconds > 0 {
|
|
ttlDuration = time.Duration(seconds) * time.Second
|
|
err = nil
|
|
}
|
|
}
|
|
if err == nil && ttlDuration > 0 {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySessionTTL, ttlDuration)
|
|
}
|
|
return true
|
|
}
|
|
if labelName, ok := strings.CutPrefix(keyStr, "x-bf-eh-"); ok {
|
|
// Skip empty header names after prefix removal
|
|
if labelName == "" {
|
|
return true
|
|
}
|
|
// Normalize header name to lowercase
|
|
labelName = strings.ToLower(labelName)
|
|
// Validate against security denylist (always enforced)
|
|
if securityDenylist[labelName] {
|
|
return true
|
|
}
|
|
// Apply configurable header filter
|
|
if !matcher.ShouldAllow(labelName) {
|
|
return true
|
|
}
|
|
// Append header value (allow multiple values for the same header)
|
|
extraHeaders[labelName] = append(extraHeaders[labelName], string(value))
|
|
return true
|
|
}
|
|
// Direct header forwarding: when allowlist is configured, any header explicitly
|
|
// in the allowlist can be forwarded directly without the x-bf-eh- prefix.
|
|
// This enables forwarding arbitrary headers like "anthropic-beta" directly.
|
|
// Only applies when allowlist is non-empty (backward compatible).
|
|
if matcher.HasAllowlist() {
|
|
if matcher.MatchesAllow(keyStr) {
|
|
// Skip reserved x-bf-* headers (handled separately)
|
|
if strings.HasPrefix(keyStr, "x-bf-") {
|
|
return true
|
|
}
|
|
// Validate against security denylist (always enforced)
|
|
if securityDenylist[keyStr] {
|
|
return true
|
|
}
|
|
// Check denylist
|
|
if matcher.MatchesDeny(keyStr) {
|
|
return true
|
|
}
|
|
// Forward the header directly with its original name
|
|
if logger != nil {
|
|
logger.Debug("forwarding header via allowlist: %s", keyStr)
|
|
}
|
|
extraHeaders[keyStr] = append(extraHeaders[keyStr], string(value))
|
|
return true
|
|
}
|
|
}
|
|
// Handle MCP extra headers
|
|
if mcpHeaderCombinedAllowlist.IsAllowed(keyStr) {
|
|
mcpExtraHeaders[keyStr] = append(mcpExtraHeaders[keyStr], string(value))
|
|
return true
|
|
}
|
|
// Raw capture headers — all three support "true"/"false" to fully override the
|
|
// provider-level config for this request.
|
|
if keyStr == "x-bf-send-back-raw-request" {
|
|
if b, err := strconv.ParseBool(string(value)); err == nil {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySendBackRawRequest, b)
|
|
}
|
|
return true
|
|
}
|
|
if keyStr == "x-bf-send-back-raw-response" {
|
|
if b, err := strconv.ParseBool(string(value)); err == nil {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySendBackRawResponse, b)
|
|
}
|
|
return true
|
|
}
|
|
if keyStr == "x-bf-store-raw-request-response" {
|
|
if b, err := strconv.ParseBool(string(value)); err == nil {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyStoreRawRequestResponse, b)
|
|
}
|
|
return true
|
|
}
|
|
// Parent request ID header (for linking MCP tool calls to parent LLM requests)
|
|
if keyStr == "x-bf-parent-request-id" {
|
|
if valueStr := strings.TrimSpace(string(value)); valueStr != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostMCPAgentOriginalRequestID, valueStr)
|
|
}
|
|
return true
|
|
}
|
|
// Add passthrough extra params header support
|
|
if keyStr == "x-bf-passthrough-extra-params" {
|
|
if valueStr := string(value); valueStr == "true" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Compat header: per-request override of compat plugin settings.
|
|
// Accepts: "true" (enable all), JSON array of feature names, or ["*"] (enable all).
|
|
// An empty array [] or absent header means no overrides.
|
|
if keyStr == "x-bf-compat" {
|
|
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatConvertTextToChat)
|
|
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatConvertChatToResponses)
|
|
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatShouldDropParams)
|
|
bifrostCtx.ClearValue(schemas.BifrostContextKeyCompatShouldConvertParams)
|
|
valueStr := strings.TrimSpace(string(value))
|
|
if valueStr == "true" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
|
|
} else if strings.HasPrefix(valueStr, "[") {
|
|
var features []string
|
|
if err := json.Unmarshal([]byte(valueStr), &features); err == nil {
|
|
if len(features) == 1 && features[0] == "*" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
|
|
} else {
|
|
for _, f := range features {
|
|
switch f {
|
|
case "convert_text_to_chat":
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertTextToChat, true)
|
|
case "convert_chat_to_responses":
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatConvertChatToResponses, true)
|
|
case "should_drop_params":
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldDropParams, true)
|
|
case "should_convert_params":
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyCompatShouldConvertParams, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Store the collected maxim tags in the context
|
|
if len(maximTags) > 0 {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKey(maxim.TagsKey), maximTags)
|
|
}
|
|
|
|
// Store collected extra headers in the context if any were found
|
|
if len(extraHeaders) > 0 {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, extraHeaders)
|
|
}
|
|
|
|
// Store collected MCP extra headers in the context if any were found
|
|
if len(mcpExtraHeaders) > 0 {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyMCPExtraHeaders, mcpExtraHeaders)
|
|
}
|
|
|
|
// Collect all request headers for downstream use (e.g., governance required headers check)
|
|
// Keys are lowercased for case-insensitive lookup
|
|
allHeaders := make(map[string]string)
|
|
ctx.Request.Header.All()(func(key, value []byte) bool {
|
|
allHeaders[strings.ToLower(string(key))] = string(value)
|
|
return true
|
|
})
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyRequestHeaders, allHeaders)
|
|
|
|
// Extract per-user MCP OAuth user identifier from X-Bf-User-Id header
|
|
if mcpUserID := string(ctx.Request.Header.Peek("X-Bf-User-Id")); mcpUserID != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyMCPUserID, mcpUserID)
|
|
}
|
|
|
|
// Build and set OAuth redirect URI for per-user OAuth flows
|
|
scheme := "http"
|
|
if ctx.IsTLS() || string(ctx.Request.Header.Peek("X-Forwarded-Proto")) == "https" {
|
|
scheme = "https"
|
|
}
|
|
host := string(ctx.Host())
|
|
if host != "" {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyOAuthRedirectURI, fmt.Sprintf("%s://%s/api/oauth/callback", scheme, host))
|
|
}
|
|
|
|
if allowDirectKeys {
|
|
// Extract API key from Authorization header (Bearer format), x-api-key, or x-goog-api-key header
|
|
var apiKey string
|
|
|
|
// TODO: fix plugin data leak
|
|
// Check Authorization header (Bearer format only - OpenAI style)
|
|
authHeader := string(ctx.Request.Header.Peek("Authorization"))
|
|
if authHeader != "" {
|
|
// Only accept Bearer token format: "Bearer ..."
|
|
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
|
authHeaderValue := strings.TrimSpace(authHeader[7:]) // Remove "Bearer " prefix
|
|
if authHeaderValue != "" && !strings.HasPrefix(strings.ToLower(authHeaderValue), governance.VirtualKeyPrefix) {
|
|
apiKey = authHeaderValue
|
|
}
|
|
} else {
|
|
apiKey = authHeader
|
|
}
|
|
}
|
|
|
|
if apiKey == "" {
|
|
// Check x-api-key (Anthropic style) header if no valid Authorization header found
|
|
xAPIKey := string(ctx.Request.Header.Peek("x-api-key"))
|
|
if xAPIKey != "" && !strings.HasPrefix(strings.ToLower(xAPIKey), governance.VirtualKeyPrefix) {
|
|
apiKey = strings.TrimSpace(xAPIKey)
|
|
} else {
|
|
// Check x-goog-api-key (Google Gemini style) header if no valid Authorization header found
|
|
xGoogleAPIKey := string(ctx.Request.Header.Peek("x-goog-api-key"))
|
|
if xGoogleAPIKey != "" && !strings.HasPrefix(strings.ToLower(xGoogleAPIKey), governance.VirtualKeyPrefix) {
|
|
apiKey = strings.TrimSpace(xGoogleAPIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found an API key, create a Key object and store it in context
|
|
if apiKey != "" {
|
|
key := schemas.Key{
|
|
ID: "header-provided", // Identifier for header-provided keys
|
|
Value: *schemas.NewEnvVar(apiKey),
|
|
Models: schemas.WhiteList{"*"}, // Allow all models
|
|
Weight: 1.0, // Default weight
|
|
}
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyDirectKey, key)
|
|
}
|
|
}
|
|
return bifrostCtx, cancel
|
|
}
|
|
|
|
// BuildHTTPRequestFromFastHTTP creates an HTTPRequest from fasthttp context for streaming handlers.
|
|
// The returned request should be released with schemas.ReleaseHTTPRequest when done.
|
|
// Note: Body is not copied for streaming (body was already consumed for the request).
|
|
func BuildHTTPRequestFromFastHTTP(ctx *fasthttp.RequestCtx) *schemas.HTTPRequest {
|
|
req := schemas.AcquireHTTPRequest()
|
|
req.Method = string(ctx.Method())
|
|
req.Path = string(ctx.Path())
|
|
|
|
// Copy headers
|
|
for key, value := range ctx.Request.Header.All() {
|
|
req.Headers[string(key)] = string(value)
|
|
}
|
|
|
|
// Copy query params
|
|
for key, value := range ctx.Request.URI().QueryArgs().All() {
|
|
req.Query[string(key)] = string(value)
|
|
}
|
|
|
|
// Copy path parameters from user values
|
|
ctx.VisitUserValuesAll(func(key, value any) {
|
|
keyStr, keyIsString := key.(string)
|
|
valueStr, valueIsString := value.(string)
|
|
if !keyIsString || !valueIsString {
|
|
return
|
|
}
|
|
if strings.HasPrefix(keyStr, "bifrost-") ||
|
|
keyStr == "BifrostContextKeyRequestID" ||
|
|
keyStr == "trace_id" ||
|
|
keyStr == "span_id" {
|
|
return
|
|
}
|
|
req.PathParams[keyStr] = valueStr
|
|
})
|
|
|
|
// Note: Body not copied - for streaming, body was already consumed
|
|
return req
|
|
}
|
|
|
|
// BuildHTTPResponseFromFastHTTP creates an HTTPResponse snapshot from fasthttp context.
|
|
// Only captures status code and headers — body is skipped because for streaming
|
|
// responses it is an active io.Reader that cannot be materialized.
|
|
// The returned response should be released with schemas.ReleaseHTTPResponse when done.
|
|
func BuildHTTPResponseFromFastHTTP(ctx *fasthttp.RequestCtx) *schemas.HTTPResponse {
|
|
resp := schemas.AcquireHTTPResponse()
|
|
resp.StatusCode = ctx.Response.StatusCode()
|
|
for key, value := range ctx.Response.Header.All() {
|
|
resp.Headers[string(key)] = string(value)
|
|
}
|
|
return resp
|
|
}
|