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
2 changes: 1 addition & 1 deletion .github/workflows/core-dependency-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@ jobs:
🔧 **Core Dependency Update Complete**
**Status**: ${{ job.status == 'success' && '✅ Success' || '❌ Failed' }}
**Branch**: `main`
**Commit**: `${{ github.sha }}`
**Commit**: ```${{ github.sha }}```
**Author**: ${{ github.actor }}
**[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})**
2 changes: 1 addition & 1 deletion .github/workflows/main-branch-notifications.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
args: |
📝 **Commit to Main Branch**
**Commit**: `${{ github.event.head_commit.message }}`
**Commit**: ```${{ github.event.head_commit.message }}```
**SHA**: `${{ github.sha }}`
**Author**: ${{ github.actor }}
**[View Commit](${{ github.event.head_commit.url }})**
86 changes: 86 additions & 0 deletions docs/usage/http-transport/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,92 @@ bifrost_provider_errors_total{provider="openai",error_type="rate_limit"} 23

---

## 🧾 Redis Cache Management

Bifrost provides built-in Redis caching capabilities to improve performance and reduce API costs.

### **Cache Control Headers**

Add these headers to your requests to control caching behavior:

| Header | Type | Description | Example |
|--------|------|-------------|---------|
| `x-bf-cache-key` | string | Unique cache identifier for the request | `"user-123-session"` |
| `x-bf-cache-ttl` | string | Time-to-live duration for cache entry. Supports Go duration format or plain numbers (treated as seconds) | `"30s"`, `"5m"`, `"1h"`, `"300"` |

**TTL Format Options:**

The `x-bf-cache-ttl` header accepts two formats:

1. **Go Duration Format**: `"30s"`, `"5m"`, `"1h"`, `"2h30m"`
2. **Plain Numbers**: `"30"`, `"300"`, `"3600"` (interpreted as seconds)

**cURL Examples with Caching:**

```bash
# Using Go duration format
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-bf-cache-key: user-session-abc123" \
-H "x-bf-cache-ttl: 10m" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [
{"role": "user", "content": "What is the capital of France?"}
]
}'

# Using plain number format (300 seconds = 5 minutes)
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-bf-cache-key: user-session-xyz789" \
-H "x-bf-cache-ttl: 300" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [
{"role": "user", "content": "What is the capital of Spain?"}
]
}'
```

**Cache Behavior:**

- **First request**: Goes to the AI provider, response is cached
- **Subsequent identical requests**: Served instantly from Redis cache
- **Cache hits**: Include `bifrost_cached: true` in response metadata
- **Streaming responses**: Cached and reconstructed chunk by chunk

### **DELETE /api/cache/{key}**

Delete a specific cache entry from Redis.

**URL Parameters:**

- `key` (required): The cache key to delete

**cURL Example:**

```bash
curl -X DELETE http://localhost:8080/api/cache/user-session-abc123
```

**Response:**

```json
{
"status": "success",
"message": "Redis cache deleted successfully"
}
```

**Notes:**

- For streaming responses, this deletes all chunks associated with the cache key
- Use the `bifrost_cache_key` from cached response metadata to get the exact key
- Returns success even if the key doesn't exist

---

## 🔧 Request Parameters

### **Common Parameters**
Expand Down
1 change: 1 addition & 0 deletions plugins/redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ config := redis.RedisPluginConfig{
| ----------------- | --------------- | -------- | ----------------- | ----------------------------------- |
| `Addr` | `string` | ✅ | - | Redis server address (host:port) |
| `CacheKey` | `string` | ✅ | - | Context key for cache identification|
| `CacheTTLKey` | `string` | ❌ | `""` | Context key for per-request TTL override |
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
| `Username` | `string` | ❌ | `""` | Username for Redis AUTH (Redis 6+) |
| `Password` | `string` | ❌ | `""` | Password for Redis AUTH |
| `DB` | `int` | ❌ | `0` | Redis database number |
Expand Down
153 changes: 153 additions & 0 deletions transports/bifrost-http/handlers/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package handlers

import (
"encoding/json"
"fmt"
"net/url"
"strings"

"github.com/fasthttp/router"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/plugins/redis"
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
"github.com/valyala/fasthttp"
)

// CacheHandler manages Cache plugin configuration for Bifrost.
// It provides endpoints to update and retrieve Cache caching settings.
type CacheHandler struct {
store *lib.ConfigStore
plugin *redis.Plugin
logger schemas.Logger
}

// NewCacheHandler creates a new handler for Cache configuration management.
func NewCacheHandler(store *lib.ConfigStore, plugin *redis.Plugin, logger schemas.Logger) *CacheHandler {
return &CacheHandler{
store: store,
plugin: plugin,
logger: logger,
}
}

// RegisterRoutes registers the Cache configuration-related routes.
func (h *CacheHandler) RegisterRoutes(r *router.Router) {
r.GET("/api/config/cache", h.GetCacheConfig)
r.PUT("/api/config/cache", h.UpdateCacheConfig)
r.DELETE("/api/cache/{key}", h.DeleteCacheCache)
}

// GetCacheConfig handles GET /api/config/cache - Get the current Cache configuration
func (h *CacheHandler) GetCacheConfig(ctx *fasthttp.RequestCtx) {
config, err := h.store.GetCacheConfigRedacted()
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to get Cache config: %v", err), h.logger)
return
}

SendJSON(ctx, config, h.logger)
}

// UpdateCacheConfig handles PUT /api/config/cache - Update Cache configuration
func (h *CacheHandler) UpdateCacheConfig(ctx *fasthttp.RequestCtx) {
var req lib.DBCacheConfig

if err := json.Unmarshal(ctx.PostBody(), &req); err != nil {
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("invalid request format: %v", err), h.logger)
return
}

// Validate required fields
if req.Addr == "" {
SendError(ctx, fasthttp.StatusBadRequest, "cache address is required", h.logger)
return
}

// Validate address format (host:port)
if !strings.Contains(req.Addr, ":") {
SendError(ctx, fasthttp.StatusBadRequest, "cache address must be in format 'host:port'", h.logger)
return
}

hostPort := strings.SplitN(req.Addr, ":", 2)
if len(hostPort) != 2 || hostPort[0] == "" {
SendError(ctx, fasthttp.StatusBadRequest, "cache address must have a non-empty host part before the colon", h.logger)
return
}

// Validate TTL
if req.TTLSeconds <= 0 {
req.TTLSeconds = 300 // Default to 5 minutes
}

// Handle password redaction - if password is redacted, preserve existing password
if req.Password != "" && lib.IsRedacted(req.Password) {
// Get current config to preserve the existing password
currentConfig, err := h.store.GetCacheConfig()
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to get current Cache config: %v", err), h.logger)
return
}
// Preserve the existing password
req.Password = currentConfig.Password
}

// Update Cache configuration in database
if err := h.store.UpdateCacheConfig(&req); err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to update Cache config: %v", err), h.logger)
return
}

// Redact the password
req.Password = lib.RedactKey(req.Password)

h.logger.Info("Cache configuration updated successfully")

SendJSON(ctx, map[string]any{
"status": "success",
"message": "Cache configuration updated successfully",
"config": req,
}, h.logger)
}

// DeleteCacheCache handles DELETE /api/cache/{key} - Delete a specific cache key from Cache
Comment thread
akshaydeo marked this conversation as resolved.
func (h *CacheHandler) DeleteCacheCache(ctx *fasthttp.RequestCtx) {
// Safely extract and validate the key parameter
keyValue := ctx.UserValue("key")
if keyValue == nil {
SendError(ctx, fasthttp.StatusBadRequest, "cache key parameter is required", h.logger)
return
}

keyStr, ok := keyValue.(string)
if !ok {
SendError(ctx, fasthttp.StatusBadRequest, "cache key parameter must be a string", h.logger)
return
}

// URL unescape the key to handle percent-encoded path segments
unescapedKey, err := url.PathUnescape(keyStr)
if err != nil {
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("invalid URL-encoded key: %v", err), h.logger)
return
}

// Guard against nil plugin
if h.plugin == nil {
h.logger.Error(fmt.Errorf("redis plugin is not available for cache deletion"))
SendError(ctx, fasthttp.StatusInternalServerError, "cache plugin is not available", h.logger)
return
}

// Clear the cache key and handle errors
if err := h.plugin.ClearCacheForKey(unescapedKey); err != nil {
h.logger.Error(fmt.Errorf("failed to delete Cache cache for key '%s': %w", unescapedKey, err))
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to delete Cache cache: %v", err), h.logger)
return
}

SendJSON(ctx, map[string]any{
"status": "success",
"message": "cache cache deleted successfully",
}, h.logger)
}
1 change: 1 addition & 0 deletions transports/bifrost-http/handlers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (h *ConfigHandler) handleUpdateConfig(ctx *fasthttp.RequestCtx) {
updatedConfig.EnableLogging = req.EnableLogging
updatedConfig.EnableGovernance = req.EnableGovernance
updatedConfig.EnforceGovernanceHeader = req.EnforceGovernanceHeader
updatedConfig.EnableCaching = req.EnableCaching
Comment thread
Pratham-Mishra04 marked this conversation as resolved.

Comment thread
Pratham-Mishra04 marked this conversation as resolved.
// Update the store with the new config
h.store.ClientConfig = updatedConfig
Expand Down
1 change: 1 addition & 0 deletions transports/bifrost-http/lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ClientConfig struct {
EnableLogging bool `json:"enable_logging"` // Enable logging of requests and responses
EnableGovernance bool `json:"enable_governance"` // Enable governance on all requests
EnforceGovernanceHeader bool `json:"enforce_governance_header"` // Enforce governance on all requests
EnableCaching bool `json:"enable_caching"` // Enable Redis caching plugin
AllowedOrigins []string `json:"allowed_origins,omitempty"` // Additional allowed origins for CORS and WebSocket (localhost is always allowed)
}

Expand Down
29 changes: 29 additions & 0 deletions transports/bifrost-http/lib/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ package lib

import (
"context"
"strconv"
"strings"
"time"

"github.com/google/uuid"
"github.com/maximhq/bifrost/plugins/maxim"
"github.com/maximhq/bifrost/plugins/redis"
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
"github.com/maximhq/bifrost/transports/bifrost-http/plugins/logging"
"github.com/maximhq/bifrost/transports/bifrost-http/plugins/telemetry"
"github.com/valyala/fasthttp"
Expand Down Expand Up @@ -110,6 +113,32 @@ func ConvertToBifrostContext(ctx *fasthttp.RequestCtx) *context.Context {
if keyStr == "x-bf-vk" {
bifrostCtx = context.WithValue(bifrostCtx, ContextKey(keyStr), string(value))
}

// Handle cache key header (x-bf-cache-key)
if keyStr == "x-bf-cache-key" {
bifrostCtx = context.WithValue(bifrostCtx, redis.ContextKey("request-cache-key"), string(value))
}

// 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 = context.WithValue(bifrostCtx, redis.ContextKey("request-cache-ttl"), ttlDuration)
}
// If both parsing attempts fail, we silently ignore the header and use default TTL
}
})

return &bifrostCtx
Expand Down
17 changes: 17 additions & 0 deletions transports/bifrost-http/lib/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type DBClientConfig struct {
EnableLogging bool `gorm:"" json:"enable_logging"`
EnableGovernance bool `gorm:"" json:"enable_governance"`
EnforceGovernanceHeader bool `gorm:"" json:"enforce_governance_header"`
EnableCaching bool `gorm:"" json:"enable_caching"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`

Expand All @@ -129,13 +130,29 @@ type DBEnvKey struct {
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
}

// DBCacheConfig represents Cache plugin configuration in the database
type DBCacheConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Addr string `gorm:"type:varchar(255);not null" json:"addr"` // Cache server address (host:port)
Username string `gorm:"type:varchar(255)" json:"username,omitempty"` // Username for Cache AUTH
Password string `gorm:"type:text" json:"password,omitempty"` // Password for Cache AUTH
DB int `gorm:"default:0" json:"db"` // Cache database number
TTLSeconds int `gorm:"default:300" json:"ttl_seconds"` // TTL in seconds (default: 5 minutes)
Prefix string `gorm:"type:varchar(100)" json:"prefix,omitempty"` // Cache key prefix
CacheByModel bool `gorm:"" json:"cache_by_model"` // Include model in cache key
CacheByProvider bool `gorm:"" json:"cache_by_provider"` // Include provider in cache key
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}

// TableName sets the table name for each model
func (DBConfigHash) TableName() string { return "config_hashes" }
func (DBProvider) TableName() string { return "config_providers" }
func (DBKey) TableName() string { return "config_keys" }
func (DBMCPClient) TableName() string { return "config_mcp_clients" }
func (DBClientConfig) TableName() string { return "config_client" }
func (DBEnvKey) TableName() string { return "config_env_keys" }
func (DBCacheConfig) TableName() string { return "config_redis" }

// GORM Hooks for JSON serialization/deserialization

Expand Down
Loading
Loading