Skip to content
Open
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
149 changes: 145 additions & 4 deletions transports/bifrost-http/handlers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore"
"github.com/maximhq/bifrost/framework/logstore"
"github.com/maximhq/bifrost/framework/vectorstore"
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
"github.com/valyala/fasthttp"
)
Expand Down Expand Up @@ -37,6 +39,12 @@ func (h *ConfigHandler) RegisterRoutes(r *router.Router) {
r.GET("/api/config", h.getConfig)
r.PUT("/api/config", h.updateConfig)
r.GET("/api/version", h.getVersion)
// Vector store configuration endpoints
r.GET("/api/config/vector-store", h.getVectorStoreConfig)
r.PUT("/api/config/vector-store", h.updateVectorStoreConfig)
// Log store configuration endpoints
r.GET("/api/config/log-store", h.getLogStoreConfig)
r.PUT("/api/config/log-store", h.updateLogStoreConfig)
}

// getVersion handles GET /api/version - Get the current version
Expand Down Expand Up @@ -124,8 +132,141 @@ func (h *ConfigHandler) updateConfig(ctx *fasthttp.RequestCtx) {
}

ctx.SetStatusCode(fasthttp.StatusOK)
SendJSON(ctx, map[string]any{
"status": "success",
"message": "configuration updated successfully",
}, h.logger)
SendJSON(ctx, map[string]string{"status": "success"}, h.logger)
}

// getVectorStoreConfig handles GET /api/config/vector-store - Get the current vector store configuration
func (h *ConfigHandler) getVectorStoreConfig(ctx *fasthttp.RequestCtx) {
if h.store.ConfigStore == nil {
SendError(ctx, fasthttp.StatusServiceUnavailable, "config store not available", h.logger)
return
}

config, err := h.store.GetVectorStoreConfigRedacted()
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError,
fmt.Sprintf("failed to fetch vector store config: %v", err), h.logger)
return
}

SendJSON(ctx, config, h.logger)
}

// updateVectorStoreConfig handles PUT /api/config/vector-store - Update vector store configuration
func (h *ConfigHandler) updateVectorStoreConfig(ctx *fasthttp.RequestCtx) {
if h.store.ConfigStore == nil {
SendError(ctx, fasthttp.StatusInternalServerError, "Config store not initialized", h.logger)
return
}

var req vectorstore.Config

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

// Get the raw config to access actual values for merging with redacted request values
oldConfigRaw, err := h.store.ConfigStore.GetVectorStoreConfig()
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to get current vector store config: %v", err), h.logger)
return
}

if oldConfigRaw == nil {
oldConfigRaw = &vectorstore.Config{}
}

// Merge redacted values with actual values
mergedConfig, err := h.mergeVectorStoreConfig(oldConfigRaw, &req)
if err != nil {
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("failed to merge vector store config: %v", err), h.logger)
return
}

if err := h.store.ConfigStore.UpdateVectorStoreConfig(mergedConfig); err != nil {
h.logger.Warn(fmt.Sprintf("failed to save vector store configuration: %v", err))
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to save vector store configuration: %v", err), h.logger)
return
}

ctx.SetStatusCode(fasthttp.StatusOK)
SendJSON(ctx, map[string]string{"status": "success"}, h.logger)
}

// getLogStoreConfig handles GET /api/config/log-store - Get the current log store configuration
func (h *ConfigHandler) getLogStoreConfig(ctx *fasthttp.RequestCtx) {
if h.store.ConfigStore == nil {
SendError(ctx, fasthttp.StatusServiceUnavailable, "config store not available", h.logger)
return
}

config, err := h.store.ConfigStore.GetLogsStoreConfig()
if err != nil {
SendError(ctx, fasthttp.StatusInternalServerError,
fmt.Sprintf("failed to fetch log store config: %v", err), h.logger)
return
}

SendJSON(ctx, config, h.logger)
}

// updateLogStoreConfig handles PUT /api/config/log-store - Update log store configuration
func (h *ConfigHandler) updateLogStoreConfig(ctx *fasthttp.RequestCtx) {
if h.store.ConfigStore == nil {
SendError(ctx, fasthttp.StatusInternalServerError, "Config store not initialized", h.logger)
return
}

var req logstore.Config

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

if err := h.store.ConfigStore.UpdateLogsStoreConfig(&req); err != nil {
h.logger.Warn(fmt.Sprintf("failed to save log store configuration: %v", err))
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("failed to save log store configuration: %v", err), h.logger)
return
}

ctx.SetStatusCode(fasthttp.StatusOK)
SendJSON(ctx, map[string]string{"status": "success"}, h.logger)
}

// mergeVectorStoreConfig merges new config with old, preserving values that are redacted in the new config
func (h *ConfigHandler) mergeVectorStoreConfig(oldConfig *vectorstore.Config, newConfig *vectorstore.Config) (*vectorstore.Config, error) {
// Start with the new config
merged := *newConfig

// Handle different vector store types
if oldConfig.Type == newConfig.Type {
switch newConfig.Type {
case vectorstore.VectorStoreTypeWeaviate:
oldWeaviateConfig, oldOk := oldConfig.Config.(*vectorstore.WeaviateConfig)
newWeaviateConfig, newOk := newConfig.Config.(*vectorstore.WeaviateConfig)
if oldOk && newOk {
mergedWeaviateConfig := *newWeaviateConfig
// Preserve old API key if new one is redacted
if lib.IsRedacted(newWeaviateConfig.ApiKey) && oldWeaviateConfig.ApiKey != "" {
mergedWeaviateConfig.ApiKey = oldWeaviateConfig.ApiKey
}
merged.Config = &mergedWeaviateConfig
}
case vectorstore.VectorStoreTypeRedis:
oldRedisConfig, oldOk := oldConfig.Config.(*vectorstore.RedisConfig)
newRedisConfig, newOk := newConfig.Config.(*vectorstore.RedisConfig)
if oldOk && newOk {
mergedRedisConfig := *newRedisConfig
// Preserve old password if new one is redacted
if lib.IsRedacted(newRedisConfig.Addr) && oldRedisConfig.Addr != "" {
mergedRedisConfig.Addr = oldRedisConfig.Addr
}
merged.Config = &mergedRedisConfig
}
}
}

return &merged, nil
}
14 changes: 14 additions & 0 deletions transports/bifrost-http/lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,20 @@ func (s *Config) GetVectorStoreConfigRedacted() (*vectorstore.Config, error) {
redactedVectorStoreConfig := *vectorStoreConfig
redactedVectorStoreConfig.Config = &redactedWeaviateConfig
return &redactedVectorStoreConfig, nil
} else if vectorStoreConfig.Type == vectorstore.VectorStoreTypeRedis {
redisConfig, ok := vectorStoreConfig.Config.(*vectorstore.RedisConfig)
if !ok {
return nil, fmt.Errorf("failed to cast vector store config to redis config")
}
// Create a copy to avoid modifying the original
redactedRedisConfig := *redisConfig
// Redact details here
if redactedRedisConfig.Addr != "" {
redactedRedisConfig.Addr = RedactKey(redactedRedisConfig.Addr)
}
redactedVectorStoreConfig := *vectorStoreConfig
redactedVectorStoreConfig.Config = &redactedRedisConfig
return &redactedVectorStoreConfig, nil
}
return nil, nil
}
Expand Down
6 changes: 6 additions & 0 deletions ui/app/config/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";

import PluginsForm from "@/app/config/views/pluginsForm";
import VectorStoreForm from "@/app/config/views/vectorStoreForm";
import LogStoreForm from "@/app/config/views/logStoreForm";
import FullPageLoader from "@/components/fullPageLoader";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -364,6 +366,10 @@ export default function ConfigPage() {

<PluginsForm isVectorStoreEnabled={bifrostConfig?.is_cache_connected ?? false} />

<VectorStoreForm />

<LogStoreForm />

<div>
<div className="space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
Expand Down
149 changes: 149 additions & 0 deletions ui/app/config/views/logStoreForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useGetLogStoreConfigQuery, useUpdateLogStoreConfigMutation } from "@/lib/store";
import { LogStoreConfig, SQLiteConfig } from "@/lib/types/config";
import { getErrorMessage } from "@/lib/store";
import { AlertTriangle, Database, Save } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";

const defaultSQLiteConfig: SQLiteConfig = {
path: "./bifrost.db",
};

export default function LogStoreForm() {
const { data: logStoreConfig, isLoading } = useGetLogStoreConfigQuery();
const [updateLogStoreConfig] = useUpdateLogStoreConfigMutation();

const [localConfig, setLocalConfig] = useState<LogStoreConfig>({
enabled: false,
type: "sqlite",
config: defaultSQLiteConfig,
});

const [needsRestart, setNeedsRestart] = useState(false);
const [hasChanges, setHasChanges] = useState(false);

// Update local config when data is loaded
useEffect(() => {
if (logStoreConfig) {
setLocalConfig(logStoreConfig);
setHasChanges(false);
}
}, [logStoreConfig]);

// Track changes
useEffect(() => {
if (logStoreConfig) {
const hasConfigChanges = JSON.stringify(localConfig) !== JSON.stringify(logStoreConfig);
setHasChanges(hasConfigChanges);
setNeedsRestart(hasConfigChanges);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this logic not show the alert when we make some change and the just reload the page without restart bifrost? as we are directly making update in the DB via the handlers

}
}, [localConfig, logStoreConfig]);

const handleEnabledChange = useCallback((enabled: boolean) => {
setLocalConfig(prev => ({ ...prev, enabled }));
}, []);

const handleSQLiteConfigChange = useCallback((field: keyof SQLiteConfig, value: string) => {
setLocalConfig(prev => ({
...prev,
config: {
...(prev.config as SQLiteConfig),
[field]: value,
},
}));
}, []);

const handleSave = useCallback(async () => {
try {
await updateLogStoreConfig(localConfig).unwrap();
toast.success("Log store configuration updated successfully.");
setHasChanges(false);
setNeedsRestart(false);
} catch (error) {
toast.error(getErrorMessage(error));
}
}, [localConfig, updateLogStoreConfig]);

if (isLoading) {
return <div>Loading log store configuration...</div>;
}

return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Log Store Configuration
</CardTitle>
<CardDescription>
Configure log store for request and response logging to a SQLite database.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between space-x-2 rounded-lg border p-4">
<div className="space-y-0.5">
<Label htmlFor="log-store-enabled" className="text-sm font-medium">
Enable Log Store
</Label>
<p className="text-muted-foreground text-sm">
Enable logging of requests and responses to a SQLite database. This can add 40-60mb of overhead to the system memory.
</p>
</div>
<Switch
id="log-store-enabled"
size="md"
checked={localConfig.enabled}
onCheckedChange={handleEnabledChange}
/>
</div>

{localConfig.enabled && (
<>
<div className="space-y-4">
<h4 className="font-medium">SQLite Configuration</h4>
<div className="space-y-2">
<Label htmlFor="sqlite-path">Database Path</Label>
<Input
id="sqlite-path"
value={(localConfig.config as SQLiteConfig).path}
onChange={(e) => handleSQLiteConfigChange("path", e.target.value)}
placeholder="./bifrost.db"
/>
<p className="text-muted-foreground text-xs">
Path to the SQLite database file. Use relative path for current directory or absolute path.
</p>
</div>
</div>

{needsRestart && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Log store configuration changes require a Bifrost service restart to take effect.
</AlertDescription>
</Alert>
)}

{hasChanges && (
<div className="flex justify-end">
<Button onClick={handleSave} className="flex items-center gap-2">
<Save className="h-4 w-4" />
Save Configuration
</Button>
</div>
)}
</>
)}
</CardContent>
</Card>
);
}

Loading