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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
**/__pycache__/**
private.*
.venv
bifrost-data
Comment thread
Pratham-Mishra04 marked this conversation as resolved.

# Temporary directories
**/temp/
Expand Down
40 changes: 38 additions & 2 deletions docs/quickstart/http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ docker run -p 8080:8080 \
npx @maximhq/bifrost -port 8080
```

> **🔄 Smart Configuration Loading**: Bifrost intelligently manages configuration sources:
> - **If `config.json` exists**: Checks if the file has changed. If unchanged, loads from database (fast path). If changed, uses file as source of truth and syncs to database.
> - **Without `config.json`**: Loads configuration from database only.
> - **Web UI changes**: Always update the database, making it the source of truth for subsequent loads.

Comment thread
Pratham-Mishra04 marked this conversation as resolved.
---

## 📁 Understanding App Directory & Docker Volumes
Expand All @@ -92,6 +97,7 @@ npx @maximhq/bifrost -port 8080

- `config.json` - Configuration file (if using file-based config)
- `logs/` - Database logs and request history
- Database files - Configuration data and hash tracking
- Any other persistent data

### **How Docker Volumes Work with App Directory**
Expand All @@ -114,15 +120,17 @@ docker run -p 8080:8080 maximhq/bifrost
| Scenario | Command | Result |
| ---------------------------- | ------------------------------------------------------------- | --------------------------------------- |
| **Ephemeral (testing)** | `docker run -p 8080:8080 maximhq/bifrost` | No persistence, configure via web UI |
| **Persistent (recommended)** | `docker run -p 8080:8080 -v $(pwd):/app/data maximhq/bifrost` | Saves config & logs to host directory |
| **Persistent (recommended)** | `docker run -p 8080:8080 -v $(pwd):/app/data maximhq/bifrost` | Saves config, logs & DB to host directory |
| **Pre-configured** | Create `config.json`, then run with volume | Starts with your existing configuration |
| **Web UI configured** | Configure via web UI, then restart | Database becomes source of truth |

### **Best Practices**

- **🔧 Development**: Use `-v $(pwd):/app/data` to persist config between restarts
- **🚀 Production**: Mount dedicated volume for data persistence
- **🧪 Testing**: Run without volume for clean ephemeral instances
- **👥 Teams**: Share `config.json` in version control, mount directory with volume
- **⚠️ Important**: After configuring via web UI, your `config.json` may become outdated. The database becomes the source of truth once you make changes through the UI.

### 3. Test the API

Expand All @@ -142,6 +150,34 @@ curl -X POST http://localhost:8080/v1/chat/completions \

---

## 🔄 Configuration Loading Behavior

Bifrost intelligently manages configuration sources to ensure your settings are always up-to-date:

### **When `config.json` exists:**
1. **File unchanged**: Loads from database (fast path)
2. **File modified**: Uses `config.json` as source of truth, syncs to database
3. **First time**: Uses `config.json` as source of truth, syncs to database

### **When no `config.json` exists:**

- Loads configuration from database only
- If database is empty, starts with default configuration

### **Web UI Configuration:**

- All changes made via web UI update the database
- Database becomes the source of truth for subsequent loads
- Your `config.json` may become outdated if you configure via web UI

### **Important Notes:**

- **Database is always the source of truth** after web UI changes
- **File changes take precedence** over database when file is modified
- **No data loss**: Configuration is always preserved in database

---

## 🔄 Drop-in Integrations (Zero Code Changes!)

**Already using OpenAI, Anthropic, or Google GenAI?** Get instant benefits with **zero code changes**:
Expand Down Expand Up @@ -319,7 +355,7 @@ response, err := http.Post(
| **Docker** | No Go installation needed, isolated environment | Production, CI/CD, quick testing |
| **Binary** | Direct execution, easier debugging | Development, custom builds |

**Note:** When using file-based config, Bifrost only looks for `config.json` in your specified app directory.
**Note:** When using file-based config, Bifrost only looks for `config.json` in your specified app directory. The database tracks file changes to optimize loading performance.

---

Expand Down
30 changes: 29 additions & 1 deletion docs/usage/http-transport/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,26 @@ Complete guide to configuring AI providers in Bifrost HTTP transport through `co

---

## 📋 Configuration Overview (File Based)
## 📋 Configuration Overview

> You can directly use the UI (`http://localhost:{port}/providers`) to configure the providers.

Provider configuration can be managed through:

- **`config.json` file** - File-based configuration with intelligent loading
- **Web UI** - Visual configuration interface
- **Database** - Persistent storage with automatic synchronization

### **Configuration Loading Behavior**

Bifrost intelligently manages configuration sources:

- **If `config.json` exists**: Checks if the file has changed. If unchanged, loads from database (fast path). If changed, uses file as source of truth and syncs to database.
- **If no `config.json`**: Loads configuration from database only.
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
- **Web UI changes**: Always update the database, making it the source of truth for subsequent loads.

> **⚠️ Important**: After configuring via web UI, your `config.json` may become outdated. The database becomes the source of truth once you make changes through the UI.

Provider configuration in `config.json` defines:

- **API credentials** and key management
Expand Down Expand Up @@ -485,6 +501,16 @@ export MISTRAL_API_KEY="your-mistral-key"
### **Docker Environment**

```bash
# With persistent configuration
docker run -p 8080:8080 \
-v $(pwd):/app/data \
-e OPENAI_API_KEY \
-e ANTHROPIC_API_KEY \
-e BEDROCK_API_KEY \
-e AWS_SECRET_ACCESS_KEY \
maximhq/bifrost

# Legacy: Direct config.json mount
docker run -p 8080:8080 \
-v $(pwd)/config.json:/app/config/config.json \
-e OPENAI_API_KEY \
Expand All @@ -494,6 +520,8 @@ docker run -p 8080:8080 \
maximhq/bifrost
```

> **💡 Note**: The recommended approach uses `-v $(pwd):/app/data` to persist both the config file and database. This ensures configuration changes via web UI are preserved between container restarts.

---

## 🧪 Testing Configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/http-transport/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ services:
ports:
- "8080:8080"
volumes:
- ./config.json:/app/config/config.json
- ./data:/app/data # Recommended: persist both config and database
environment:
- OPENAI_API_KEY
- ANTHROPIC_API_KEY
Expand Down
4 changes: 3 additions & 1 deletion plugins/maxim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ This plugin integrates the Maxim SDK into Bifrost, enabling seamless observabili

Running the docker container

> **💡 Volume Mounting**: The entire working directory is mounted to `/app/data` to persist both the JSON configuration file and the database. This ensures that configuration changes made via the web UI are preserved between container restarts, and the new hash-based configuration loading system can properly track file changes.

```bash
docker run -d \
-p 8080:8080 \
-v $(pwd)/config.json:/app/config/config.json \
-v $(pwd):/app/data \
Comment thread
Pratham-Mishra04 marked this conversation as resolved.
-e APP_PORT=8080 \
Comment thread
coderabbitai[bot] marked this conversation as resolved.
-e MAXIM_API_KEY \
-e MAXIM_LOG_REPO_ID \
Expand Down
20 changes: 9 additions & 11 deletions transports/bifrost-http/handlers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@ import (
)

// ConfigHandler manages runtime configuration updates for Bifrost.
// It provides an endpoint to hot-reload settings from the configuration file.
// It provides endpoints to update and retrieve settings persisted via the ConfigStore backed by sql database.
type ConfigHandler struct {
client *bifrost.Bifrost
logger schemas.Logger
store *lib.ConfigStore
configPath string
client *bifrost.Bifrost
logger schemas.Logger
store *lib.ConfigStore
}

// NewConfigHandler creates a new handler for configuration management.
// It requires the Bifrost client, a logger, and the path to the config file to be reloaded.
func NewConfigHandler(client *bifrost.Bifrost, logger schemas.Logger, store *lib.ConfigStore, configPath string) *ConfigHandler {
// It requires the Bifrost client, a logger, and the config store.
func NewConfigHandler(client *bifrost.Bifrost, logger schemas.Logger, store *lib.ConfigStore) *ConfigHandler {
return &ConfigHandler{
client: client,
logger: logger,
store: store,
configPath: configPath,
client: client,
logger: logger,
store: store,
}
}

Expand Down
80 changes: 65 additions & 15 deletions transports/bifrost-http/handlers/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ import (
"github.com/valyala/fasthttp"
)

// WebSocketClient represents a connected WebSocket client with its own mutex
type WebSocketClient struct {
conn *websocket.Conn
mu sync.Mutex // Per-connection mutex for thread-safe writes
}

// WebSocketHandler manages WebSocket connections for real-time updates
type WebSocketHandler struct {
logManager logging.LogManager
logger schemas.Logger
clients map[*websocket.Conn]bool
clients map[*websocket.Conn]*WebSocketClient
mu sync.RWMutex
stopChan chan struct{} // Channel to signal heartbeat goroutine to stop
done chan struct{} // Channel to signal when heartbeat goroutine has stopped
Expand All @@ -32,7 +38,7 @@ func NewWebSocketHandler(logManager logging.LogManager, logger schemas.Logger) *
return &WebSocketHandler{
logManager: logManager,
logger: logger,
clients: make(map[*websocket.Conn]bool),
clients: make(map[*websocket.Conn]*WebSocketClient),
stopChan: make(chan struct{}),
done: make(chan struct{}),
}
Expand Down Expand Up @@ -83,9 +89,14 @@ func isLocalhost(host string) bool {
// HandleLogStream handles WebSocket connections for real-time log streaming
func (h *WebSocketHandler) HandleLogStream(ctx *fasthttp.RequestCtx) {
err := upgrader.Upgrade(ctx, func(ws *websocket.Conn) {
// Create a new client with its own mutex
client := &WebSocketClient{
conn: ws,
}

// Register new client
h.mu.Lock()
h.clients[ws] = true
h.clients[ws] = client
h.mu.Unlock()

// Clean up on disconnect
Expand Down Expand Up @@ -123,8 +134,37 @@ func (h *WebSocketHandler) HandleLogStream(ctx *fasthttp.RequestCtx) {
}
}

// sendMessageSafely sends a message to a client with proper locking and error handling
func (h *WebSocketHandler) sendMessageSafely(client *WebSocketClient, messageType int, data []byte) error {
client.mu.Lock()
defer client.mu.Unlock()

// Set a write deadline to prevent hanging connections
client.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
defer client.conn.SetWriteDeadline(time.Time{}) // Clear the deadline

err := client.conn.WriteMessage(messageType, data)
if err != nil {
// Remove the client from the map if write fails
go func() {
h.mu.Lock()
delete(h.clients, client.conn)
h.mu.Unlock()
client.conn.Close()
}()
}
return err
}

// BroadcastLogUpdate sends a log update to all connected WebSocket clients
func (h *WebSocketHandler) BroadcastLogUpdate(logEntry *logging.LogEntry) {
// Add panic recovery to prevent server crashes
defer func() {
if r := recover(); r != nil {
h.logger.Error(fmt.Errorf("panic in BroadcastLogUpdate: %v", r))
}
}()

// Determine operation type based on log status and timestamp
operationType := "update"
if logEntry.Status == "processing" && logEntry.CreatedAt.Equal(logEntry.Timestamp) {
Expand All @@ -147,14 +187,18 @@ func (h *WebSocketHandler) BroadcastLogUpdate(logEntry *logging.LogEntry) {
return
}

// Get a snapshot of clients to avoid holding the lock during writes
h.mu.RLock()
defer h.mu.RUnlock()
clients := make([]*WebSocketClient, 0, len(h.clients))
for _, client := range h.clients {
clients = append(clients, client)
}
h.mu.RUnlock()

for client := range h.clients {
err := client.WriteMessage(websocket.TextMessage, data)
if err != nil {
// Send message to each client safely
for _, client := range clients {
if err := h.sendMessageSafely(client, websocket.TextMessage, data); err != nil {
h.logger.Error(fmt.Errorf("failed to send message to client: %v", err))
continue
}
}
}
Expand All @@ -171,14 +215,20 @@ func (h *WebSocketHandler) StartHeartbeat() {
for {
select {
case <-ticker.C:
// Get a snapshot of clients to avoid holding the lock during writes
h.mu.RLock()
for client := range h.clients {
err := client.WriteMessage(websocket.PingMessage, nil)
if err != nil {
clients := make([]*WebSocketClient, 0, len(h.clients))
for _, client := range h.clients {
clients = append(clients, client)
}
h.mu.RUnlock()

// Send heartbeat to each client safely
for _, client := range clients {
if err := h.sendMessageSafely(client, websocket.PingMessage, nil); err != nil {
h.logger.Error(fmt.Errorf("failed to send heartbeat: %v", err))
}
}
h.mu.RUnlock()
case <-h.stopChan:
return
}
Expand All @@ -193,9 +243,9 @@ func (h *WebSocketHandler) Stop() {

// Close all client connections
h.mu.Lock()
for client := range h.clients {
client.Close()
for _, client := range h.clients {
client.conn.Close()
}
h.clients = make(map[*websocket.Conn]bool)
h.clients = make(map[*websocket.Conn]*WebSocketClient)
h.mu.Unlock()
}
Loading