diff --git a/docs/docs.json b/docs/docs.json
index 784b907b7f..100be0c219 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -91,6 +91,7 @@
"features/mcp",
"features/tracing",
"features/telemetry",
+ "features/observability",
"features/governance",
"features/semantic-caching",
"features/custom-providers",
diff --git a/docs/features/mcp.mdx b/docs/features/mcp.mdx
index aaff715ace..fc2c744e31 100644
--- a/docs/features/mcp.mdx
+++ b/docs/features/mcp.mdx
@@ -103,7 +103,7 @@ func main() {
},
}
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: account,
MCPConfig: mcpConfig,
Logger: bifrost.NewDefaultLogger(schemas.LogLevelInfo),
@@ -282,7 +282,7 @@ import (
func main() {
// Initialize Bifrost with MCP
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: account,
MCPConfig: &schemas.MCPConfig{
ClientConfigs: []schemas.MCPClientConfig{
@@ -539,7 +539,7 @@ func calculatorHandler(args CalculatorArgs) (string, error) {
func main() {
// Initialize Bifrost (tool registry creates in-process MCP automatically)
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: account,
Logger: bifrost.NewDefaultLogger(schemas.LogLevelInfo),
})
diff --git a/docs/features/observability.mdx b/docs/features/observability.mdx
index a1224a05f9..1136caaddf 100644
--- a/docs/features/observability.mdx
+++ b/docs/features/observability.mdx
@@ -1,6 +1,229 @@
---
title: "Observability"
-description: "Comprehensive Prometheus-based monitoring for Bifrost Gateway with custom metrics and labels."
+description: "Integrate Maxim SDK for comprehensive LLM observability, tracing, and evaluation."
icon: "binoculars"
---
+## Overview
+
+Bifrost provides comprehensive LLM observability through the **Maxim plugin**, enabling seamless tracking, evaluation, and analysis of AI interactions. The plugin automatically forwards all LLM requests and responses to Maxim's platform for detailed monitoring and performance insights.
+
+
+
+---
+
+## Setup
+
+The Maxim plugin enables seamless observability and evaluation of LLM interactions by forwarding inputs/outputs to Maxim's platform:
+
+
+
+
+```go
+package main
+
+import (
+ "context"
+ bifrost "github.com/maximhq/bifrost/core"
+ "github.com/maximhq/bifrost/core/schemas"
+ maxim "github.com/maximhq/bifrost/plugins/maxim"
+)
+
+func main() {
+ // Initialize Maxim plugin
+ maximPlugin, err := maxim.Init(maxim.Config{
+ ApiKey: "your_maxim_api_key",
+ LogRepoId: "your_default_repo_id", // Optional: fallback repository
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // Initialize Bifrost with the plugin
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
+ Account: &yourAccount,
+ Plugins: []schemas.Plugin{maximPlugin},
+ })
+ if err != nil {
+ panic(err)
+ }
+ defer client.Shutdown()
+
+ // All requests will now be traced to Maxim
+}
+```
+
+
+
+
+For HTTP transport, configure via environment variables:
+
+```json
+{
+ "plugins": [
+ {
+ "enabled": true,
+ "name": "maxim",
+ "config": {
+ "api_key": "your_maxim_api_key",
+ "log_repo_id": "your_default_repo_id"
+ }
+ }
+ ]
+}
+```
+
+
+
+
+## Configuration
+
+### Plugin Configuration
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `ApiKey` | `string` | ✅ Yes | Your Maxim API key for authentication |
+| `LogRepoId` | `string` | ❌ No | Default log repository ID (can be overridden per request) |
+
+## Repository Selection
+
+The plugin uses repository selection with the following priority:
+
+1. **Header/Context Repository** - Highest priority
+2. **Default Repository** (from plugin config) - Fallback
+3. **Skip Logging** - If neither is available
+
+
+
+
+```go
+ctx := context.Background()
+
+// Use specific repository for this request
+ctx = context.WithValue(ctx, maxim.LogRepoIDKey, "project-specific-repo")
+```
+
+
+
+
+```bash
+# Use default repository (from config)
+curl -X POST http://localhost:8080/v1/chat/completions \
+ -d '{"model": "gpt-4", "messages": [...]}'
+
+# Override with specific repository
+curl -X POST http://localhost:8080/v1/chat/completions \
+ -H "x-bf-maxim-log-repo-id: project-specific-repo" \
+ -d '{"model": "gpt-4", "messages": [...]}'
+```
+
+
+
+
+
+## Custom Trace Management
+
+### Trace Propagation
+
+The plugin supports custom session, trace, and generation IDs for advanced tracing scenarios:
+
+
+
+```go
+ctx := context.Background()
+
+// Prefer typed keys from the Maxim plugin
+ctx = context.WithValue(ctx, maxim.TraceIDKey, "custom-trace-123")
+ctx = context.WithValue(ctx, maxim.GenerationIDKey, "custom-gen-456")
+ctx = context.WithValue(ctx, maxim.SessionIDKey, "user-session-789")
+
+// Optionally set human-friendly names
+ctx = context.WithValue(ctx, maxim.TraceNameKey, "checkout-flow")
+ctx = context.WithValue(ctx, maxim.GenerationNameKey, "rerank-step")
+```
+
+
+```bash
+curl -X POST http://localhost:8080/v1/chat/completions \
+ -H "x-bf-maxim-trace-id: custom-trace-123" \
+ -H "x-bf-maxim-generation-id: custom-gen-456" \
+ -H "x-bf-maxim-session-id: user-session-789" \
+ -H "x-bf-maxim-trace-name: checkout-flow" \
+ -H "x-bf-maxim-generation-name: rerank-step" \
+ -d '{"model": "gpt-4", "messages": [...]}'
+```
+
+
+
+### Custom Tags
+
+You can add custom tags to traces for enhanced filtering and analytics:
+
+
+
+
+```go
+ctx := context.Background()
+
+// Pass arbitrary tag key-values via context map
+tags := map[string]string{
+ "environment": "production",
+ "user-id": "user-123",
+ "feature-flag": "new-ui",
+}
+ctx = context.WithValue(ctx, maxim.TagsKey, tags)
+```
+
+
+
+
+```bash
+curl -X POST http://localhost:8080/v1/chat/completions \
+ -H "x-bf-maxim-environment: production" \
+ -H "x-bf-maxim-user-id: user-123" \
+ -H "x-bf-maxim-feature-flag: new-ui" \
+ -d '{"model": "gpt-4", "messages": [...]}'
+```
+
+Reserved keys are `session-id`, `trace-id`, `trace-name`, `generation-id`, `generation-name`, `log-repo-id`. All other `x-bf-maxim-*` headers are treated as tags.
+
+
+
+
+## Supported Request Types
+
+The plugin supports the following Bifrost request types:
+
+| Request Type | Support | Logged Data |
+|--------------|---------|-------------|
+| **Chat Completion** | ✅ Full | Messages, model parameters, responses |
+| **Text Completion** | ✅ Full | Input text, model parameters, responses |
+
+## Monitoring & Analytics
+
+### Maxim Dashboard
+
+Once configured, view your traces at [Maxim Dashboard](https://getmaxim.ai/):
+
+- **Request/Response Tracking**: Complete LLM interaction logs
+- **Performance Metrics**: Latency, token usage, and cost analytics
+- **Tool Usage Patterns**: Function calling and tool interaction insights
+- **Error Tracking**: Detailed error logs and failure analysis
+
+### Key Metrics
+
+- **Trace Coverage**: Percentage of requests being logged
+- **Response Times**: End-to-end latency tracking
+- **Token Usage**: Input/output token consumption
+- **Cost Analytics**: Per-request and aggregate cost tracking
+ - **Cost Analytics**: Per-request and aggregate cost tracking
+
+----
+
+## Next Steps
+
+Now that you have observability set up with the Maxim plugin, explore these related topics:
+
+- **[Tracing](./tracing)** - Deep-dive into request/response logging and correlation
+- **[Telemetry](./telemetry)** - Prometheus metrics, dashboards, and alerting
+- **[Governance](./governance)** - Virtual keys, per-team controls, and usage limits
diff --git a/docs/features/plugins/jsonparser.mdx b/docs/features/plugins/jsonparser.mdx
index 62cfa60b5b..379f09f9ad 100644
--- a/docs/features/plugins/jsonparser.mdx
+++ b/docs/features/plugins/jsonparser.mdx
@@ -46,7 +46,7 @@ func main() {
})
// Initialize Bifrost with the plugin
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &MyAccount{},
Plugins: []schemas.Plugin{
jsonPlugin,
@@ -84,7 +84,7 @@ func main() {
})
// Initialize Bifrost with the plugin
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &MyAccount{},
Plugins: []schemas.Plugin{
jsonPlugin,
@@ -201,7 +201,7 @@ func main() {
})
// Initialize Bifrost with the plugin
- client, err := bifrost.Init(schemas.BifrostConfig{
+ client, err := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &MyAccount{},
Plugins: []schemas.Plugin{jsonPlugin},
})
diff --git a/docs/features/plugins/mocker.mdx b/docs/features/plugins/mocker.mdx
index 9c830d04ad..7749b15d84 100644
--- a/docs/features/plugins/mocker.mdx
+++ b/docs/features/plugins/mocker.mdx
@@ -30,7 +30,7 @@ func main() {
}
// Initialize Bifrost with the plugin
- client, initErr := bifrost.Init(schemas.BifrostConfig{
+ client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &yourAccount,
Plugins: []schemas.Plugin{plugin},
})
@@ -124,7 +124,7 @@ if err != nil {
### Adding to Bifrost
```go
-client, initErr := bifrost.Init(schemas.BifrostConfig{
+client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &yourAccount,
Plugins: []schemas.Plugin{plugin},
Logger: bifrost.NewDefaultLogger(schemas.LogLevelInfo),
@@ -476,7 +476,7 @@ Response{
Enable debug logging to troubleshoot:
```go
-client, initErr := bifrost.Init(schemas.BifrostConfig{
+client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &account,
Plugins: []schemas.Plugin{plugin},
Logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
diff --git a/docs/media/maxim-logs.png b/docs/media/maxim-logs.png
new file mode 100644
index 0000000000..c738f80676
Binary files /dev/null and b/docs/media/maxim-logs.png differ
diff --git a/docs/media/ui-observability-config.png b/docs/media/ui-observability-config.png
new file mode 100644
index 0000000000..a929a1eff0
Binary files /dev/null and b/docs/media/ui-observability-config.png differ
diff --git a/docs/quickstart/go-sdk/setting-up.mdx b/docs/quickstart/go-sdk/setting-up.mdx
index 73819f6193..b6a92331ea 100644
--- a/docs/quickstart/go-sdk/setting-up.mdx
+++ b/docs/quickstart/go-sdk/setting-up.mdx
@@ -72,7 +72,7 @@ func (a *MyAccount) GetConfigForProvider(provider schemas.ModelProvider) (*schem
// Main function implement to initialize bifrost and make a request
func main() {
- client, initErr := bifrost.Init(schemas.BifrostConfig{
+ client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &MyAccount{},
})
if initErr != nil {
diff --git a/docs/quickstart/go-sdk/tool-calling.mdx b/docs/quickstart/go-sdk/tool-calling.mdx
index 3a2ab0795b..f58549981a 100644
--- a/docs/quickstart/go-sdk/tool-calling.mdx
+++ b/docs/quickstart/go-sdk/tool-calling.mdx
@@ -73,7 +73,7 @@ if toolCalls != nil {
Connect to Model Context Protocol (MCP) servers to give AI models access to external tools and services without manually defining each function.
```go
-client, initErr := bifrost.Init(schemas.BifrostConfig{
+client, initErr := bifrost.Init(context.Background(), schemas.BifrostConfig{
Account: &MyAccount{},
MCPConfig: &schemas.MCPConfig{
ClientConfigs: []schemas.MCPClientConfig{
diff --git a/plugins/maxim/README.md b/plugins/maxim/README.md
deleted file mode 100644
index c1279e2103..0000000000
--- a/plugins/maxim/README.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# Maxim-SDK Plugin for Bifrost
-
-This plugin integrates the Maxim SDK into Bifrost, enabling seamless observability and evaluation of LLM interactions. It captures and forwards inputs/outputs from Bifrost to the Maxim's observability platform. This facilitates end-to-end tracing, evaluation, and monitoring of your LLM-based application.
-
-## Usage for Bifrost Go Package
-
-1. Download the Plugin
-
- ```bash
- go get github.com/maximhq/bifrost/plugins/maxim
- ```
-
-2. Initialise the Plugin
-
- ```go
- maximPlugin, err := maxim.NewMaximLoggerPlugin("your_maxim_api_key", "your_maxim_log_repo_id")
- if err != nil {
- return nil, err
- }
- ```
-
-3. Pass the plugin to Bifrost
-
-```go
- client, initErr := bifrost.Init(schemas.BifrostConfig{
- Account: &yourAccount,
- Plugins: []schemas.Plugin{maximPlugin},
- })
-```
-
-## Usage for Bifrost HTTP Transport
-
-1. Set up the environment variables
-
- ```bash
- export MAXIM_API_KEY=your_maxim_api_key
- export MAXIM_LOG_REPO_ID=your_maxim_log_repo_id
- ```
-
-2. Set up flags to add the plugin
- Add `maxim` to the `--plugins` flag
-
- e.g., `npx -y @maximhq/bifrost -plugins maxim`
-
- For docker build
-
- ```bash
- docker build -t bifrost-transports .
- ```
-
- 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):/app/data \
- -e APP_PORT=8080 \
- -e MAXIM_API_KEY \
- -e MAXIM_LOG_REPO_ID \
- bifrost-transport
- ```
-
-## Viewing Your Traces
-
-1. Log in to your [Maxim Dashboard](https://getmaxim.ai/dashboard)
-2. Navigate to your repository
-3. View detailed llm traces, including:
- - LLM inputs/outputs
- - Tool usage patterns
- - Performance metrics
- - Cost analytics
-
-## Additional Features
-
-The plugin also supports custom `session-id`, `trace-id` and `generation-id` if the user wishes to log the generations to their custom logging implementation. To use it, pass your trace ID to the request context with the key `trace-id`, and similarly `generation-id` for generation ID. In these cases, no new trace/generation is created and the output is logged to your provided generation. Likewise, `session-id` can be used to add the traces to your generated session.
-
-e.g.
-
-```go
- ctx = context.WithValue(ctx, "generation-id", "123")
-
- result, err := bifrostClient.ChatCompletionRequest(schemas.OpenAI, &schemas.BifrostRequest{
- Model: "gpt-4o",
- Input: schemas.RequestInput{
- ChatCompletionInput: &messages,
- },
- Params: ¶ms,
- }, ctx)
-```
-
-HTTP transport offers out-of-the-box support for this feature (when the Maxim plugin is used). Pass `x-bf-maxim-session-id`, `x-bf-maxim-trace-id`, or `x-bf-maxim-generation-id` headers with your request to use this feature.
-
-## Testing Maxim Logger
-
-To test the Maxim Logger plugin, you'll need to set up the following environment variables:
-
-```bash
-# Required environment variables
-export MAXIM_API_KEY=your_maxim_api_key
-export MAXIM_LOGGER_ID=your_maxim_log_repo_id
-export OPENAI_API_KEY=your_openai_api_key
-```
-
-Then you can run the tests using:
-
-```bash
-go test -run TestMaximLoggerPlugin
-```
-
-The test suite includes:
-
-- Plugin initialization tests
-- Integration tests with Bifrost
-- Error handling for missing environment variables
-
-Note: The tests make actual API calls to both Maxim and OpenAI, so ensure you have valid API keys and sufficient quota before running the tests.
-
-After the test is complete, you can check your traces on [Maxim's Dashboard](https://www.getmaxim.ai)
diff --git a/plugins/maxim/go.mod b/plugins/maxim/go.mod
index 71c71efc6e..a32a413121 100644
--- a/plugins/maxim/go.mod
+++ b/plugins/maxim/go.mod
@@ -6,7 +6,7 @@ toolchain go1.24.3
require (
github.com/maximhq/bifrost/core v1.1.33
- github.com/maximhq/maxim-go v0.1.8
+ github.com/maximhq/maxim-go v0.1.10
)
require github.com/google/uuid v1.6.0
diff --git a/plugins/maxim/go.sum b/plugins/maxim/go.sum
index 3c915b2bbb..c22162e2a2 100644
--- a/plugins/maxim/go.sum
+++ b/plugins/maxim/go.sum
@@ -73,8 +73,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maximhq/bifrost/core v1.1.33 h1:FhWaoZPQJqgb4yH6dVlztpHVfHHUZ/+TK3Y7jqZNSUs=
github.com/maximhq/bifrost/core v1.1.33/go.mod h1:tf2pFTpoM53UGXXMFYxsaUjMqnCqYDOd9glFgMJvA0c=
-github.com/maximhq/maxim-go v0.1.8 h1:LXCYwg/WLNY5rPBScki9y4/wjH7h4VEz8vPUXbyoI4g=
-github.com/maximhq/maxim-go v0.1.8/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw=
+github.com/maximhq/maxim-go v0.1.10 h1:rGBYSY3qld2zfZeL4HBmropkyfrqNiJ4IYA49jbvYX8=
+github.com/maximhq/maxim-go v0.1.10/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
diff --git a/plugins/maxim/main.go b/plugins/maxim/main.go
index 8af0db1acf..8988ba7dca 100644
--- a/plugins/maxim/main.go
+++ b/plugins/maxim/main.go
@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
+ "sync"
"github.com/google/uuid"
"github.com/maximhq/bifrost/core/schemas"
@@ -14,36 +15,48 @@ import (
"github.com/maximhq/maxim-go/logging"
)
-// PluginName is the canonical name for the bifrost-maxim plugin.
-const PluginName = "bifrost-maxim"
+// PluginName is the canonical name for the maxim plugin.
+const PluginName = "maxim"
-// NewMaximLoggerPlugin initializes and returns a Plugin instance for Maxim's logger.
+// Config is the configuration for the maxim plugin.
+// - apiKey: API key for Maxim SDK authentication
+// - logRepoId: Optional default ID for the Maxim logger instance
+type Config struct {
+ LogRepoId string `json:"log_repo_id,omitempty"` // Optional - can be empty
+ ApiKey string `json:"api_key"`
+}
+
+// Init initializes and returns a Plugin instance for Maxim's logger.
//
// Parameters:
-// - apiKey: API key for Maxim SDK authentication
-// - logRepoId: ID for the Maxim logger instance
+// - config: Configuration for the maxim plugin
//
// Returns:
// - schemas.Plugin: A configured plugin instance for request/response tracing
// - error: Any error that occurred during plugin initialization
-func NewMaximLoggerPlugin(apiKey string, logRepoId string) (schemas.Plugin, error) {
+func Init(config Config) (schemas.Plugin, error) {
// check if Maxim Logger variables are set
- if apiKey == "" {
+ if config.ApiKey == "" {
return nil, fmt.Errorf("apiKey is not set")
}
- if logRepoId == "" {
- return nil, fmt.Errorf("log repo id is not set")
- }
-
- mx := maxim.Init(&maxim.MaximSDKConfig{ApiKey: apiKey})
+ mx := maxim.Init(&maxim.MaximSDKConfig{ApiKey: config.ApiKey})
- logger, err := mx.GetLogger(&logging.LoggerConfig{Id: logRepoId})
- if err != nil {
- return nil, err
+ plugin := &Plugin{
+ mx: mx,
+ defaultLogRepoId: config.LogRepoId,
+ loggers: make(map[string]*logging.Logger),
+ loggerMutex: &sync.RWMutex{},
}
- plugin := &Plugin{logger}
+ // Initialize default logger if LogRepoId is provided
+ if config.LogRepoId != "" {
+ logger, err := mx.GetLogger(&logging.LoggerConfig{Id: config.LogRepoId})
+ if err != nil {
+ return nil, fmt.Errorf("failed to initialize default logger: %w", err)
+ }
+ plugin.loggers[config.LogRepoId] = logger
+ }
return plugin, nil
}
@@ -63,6 +76,7 @@ const (
GenerationIDKey ContextKey = "generation-id"
GenerationNameKey ContextKey = "generation-name"
TagsKey ContextKey = "maxim-tags"
+ LogRepoIDKey ContextKey = "log-repo-id"
)
// The plugin provides request/response tracing functionality by integrating with Maxim's logging system.
@@ -79,13 +93,19 @@ const (
// These IDs can be propagated from external systems through HTTP headers (x-bf-maxim-trace-id and x-bf-maxim-generation-id).
// Plugin implements the schemas.Plugin interface for Maxim's logger.
-// It provides request and response tracing functionality using the Maxim logger,
-// allowing detailed tracking of requests and responses.
+// It provides request and response tracing functionality using Maxim logger,
+// allowing detailed tracking of requests and responses across different log repositories.
//
// Fields:
-// - logger: A Maxim logger instance used for tracing requests and responses
+// - mx: The Maxim SDK instance for creating new loggers
+// - defaultLogRepoId: Default log repository ID from config (optional)
+// - loggers: Map of log repo ID to logger instances
+// - loggerMutex: RW mutex for thread-safe access to loggers map
type Plugin struct {
- logger *logging.Logger
+ mx *maxim.Maxim
+ defaultLogRepoId string
+ loggers map[string]*logging.Logger
+ loggerMutex *sync.RWMutex
}
// GetName returns the name of the plugin.
@@ -93,6 +113,56 @@ func (plugin *Plugin) GetName() string {
return PluginName
}
+// getEffectiveLogRepoID determines which single log repo ID to use based on priority:
+// 1. Header log repo ID (if provided)
+// 2. Default log repo ID from config (if configured)
+// 3. Empty string (skip logging)
+func (plugin *Plugin) getEffectiveLogRepoID(ctx *context.Context) string {
+ // Check for header log repo ID first (highest priority)
+ if ctx != nil {
+ if headerRepoID, ok := (*ctx).Value(LogRepoIDKey).(string); ok && headerRepoID != "" {
+ return headerRepoID
+ }
+ }
+
+ // Fall back to default log repo ID from config
+ if plugin.defaultLogRepoId != "" {
+ return plugin.defaultLogRepoId
+ }
+
+ // Return empty string if neither header nor default is available
+ return ""
+}
+
+// getOrCreateLogger gets an existing logger or creates a new one for the given log repo ID
+func (plugin *Plugin) getOrCreateLogger(logRepoID string) (*logging.Logger, error) {
+ // First, try to get existing logger (read lock)
+ plugin.loggerMutex.RLock()
+ if logger, exists := plugin.loggers[logRepoID]; exists {
+ plugin.loggerMutex.RUnlock()
+ return logger, nil
+ }
+ plugin.loggerMutex.RUnlock()
+
+ // Logger doesn't exist, create it (write lock)
+ plugin.loggerMutex.Lock()
+ defer plugin.loggerMutex.Unlock()
+
+ // Double-check in case another goroutine created it while we were waiting
+ if logger, exists := plugin.loggers[logRepoID]; exists {
+ return logger, nil
+ }
+
+ // Create new logger
+ logger, err := plugin.mx.GetLogger(&logging.LoggerConfig{Id: logRepoID})
+ if err != nil {
+ return nil, fmt.Errorf("failed to create logger for repo ID %s: %w", logRepoID, err)
+ }
+
+ plugin.loggers[logRepoID] = logger
+ return logger, nil
+}
+
// PreHook is called before a request is processed by Bifrost.
// It manages trace and generation tracking for incoming requests by either:
// - Creating a new trace if none exists
@@ -121,6 +191,14 @@ func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest)
var generationName string
var tags map[string]string
+ // Get effective log repo ID (header > default > skip)
+ effectiveLogRepoID := plugin.getEffectiveLogRepoID(ctx)
+
+ // If no log repo ID available, skip logging
+ if effectiveLogRepoID == "" {
+ return req, nil, nil
+ }
+
// Check if context already has traceID and generationID
if ctx != nil {
if existingGenerationID, ok := (*ctx).Value(GenerationIDKey).(string); ok && existingGenerationID != "" {
@@ -206,7 +284,6 @@ func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest)
latestMessage = *req.Input.TextCompletionInput
}
- // Set action tag after determining request type
tags["action"] = requestType
if traceID == "" {
@@ -227,9 +304,12 @@ func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest)
traceConfig.SessionId = &sessionID
}
- trace := plugin.logger.Trace(&traceConfig)
-
- trace.SetInput(latestMessage)
+ // Create trace in the effective log repository
+ logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
+ if err == nil {
+ trace := logger.Trace(&traceConfig)
+ trace.SetInput(latestMessage)
+ }
}
// Convert ModelParameters to map[string]interface{}
@@ -257,7 +337,11 @@ func (plugin *Plugin) PreHook(ctx *context.Context, req *schemas.BifrostRequest)
generationConfig.Name = &generationName
}
- plugin.logger.AddGenerationToTrace(traceID, &generationConfig)
+ // Add generation to the effective log repository
+ logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
+ if err == nil {
+ logger.AddGenerationToTrace(traceID, &generationConfig)
+ }
if ctx != nil {
if _, ok := (*ctx).Value(TraceIDKey).(string); !ok {
@@ -293,34 +377,57 @@ func (plugin *Plugin) PostHook(ctxRef *context.Context, res *schemas.BifrostResp
if ctxRef != nil {
ctx := *ctxRef
+ // Get effective log repo ID for this request
+ effectiveLogRepoID := plugin.getEffectiveLogRepoID(ctxRef)
+
generationID, ok := ctx.Value(GenerationIDKey).(string)
- if ok {
- if bifrostErr != nil {
- genErr := logging.GenerationError{
- Message: bifrostErr.Error.Message,
- Code: bifrostErr.Error.Code,
- Type: bifrostErr.Error.Type,
+ if ok && effectiveLogRepoID != "" {
+ // Process generation completion in the effective log repository
+ logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
+ if err == nil {
+ if bifrostErr != nil {
+ genErr := logging.GenerationError{
+ Message: bifrostErr.Error.Message,
+ Code: bifrostErr.Error.Code,
+ Type: bifrostErr.Error.Type,
+ }
+ logger.SetGenerationError(generationID, &genErr)
+ } else if res != nil {
+ logger.AddResultToGeneration(generationID, res)
}
- plugin.logger.SetGenerationError(generationID, &genErr)
- } else if res != nil {
- plugin.logger.AddResultToGeneration(generationID, res)
- }
- plugin.logger.EndGeneration(generationID)
+ logger.EndGeneration(generationID)
+ }
}
traceID, ok := ctx.Value(TraceIDKey).(string)
- if ok {
- plugin.logger.EndTrace(traceID)
+ if ok && effectiveLogRepoID != "" {
+ // End trace in the effective log repository
+ logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
+ if err == nil {
+ logger.EndTrace(traceID)
+ }
+ }
+
+ // Flush only the effective logger that was used for this request
+ if effectiveLogRepoID != "" {
+ logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
+ if err == nil {
+ logger.Flush()
+ }
}
}
- plugin.logger.Flush()
return res, bifrostErr, nil
}
func (plugin *Plugin) Cleanup() error {
- plugin.logger.Flush()
+ // Flush all loggers
+ plugin.loggerMutex.RLock()
+ for _, logger := range plugin.loggers {
+ logger.Flush()
+ }
+ plugin.loggerMutex.RUnlock()
return nil
}
diff --git a/plugins/maxim/plugin_test.go b/plugins/maxim/plugin_test.go
index bfc2f169e0..9d69fe404b 100644
--- a/plugins/maxim/plugin_test.go
+++ b/plugins/maxim/plugin_test.go
@@ -18,7 +18,7 @@ import (
//
// Environment Variables:
// - MAXIM_API_KEY: API key for Maxim SDK authentication
-// - MAXIM_LOGGER_ID: ID for the Maxim logger instance
+// - MAXIM_LOG_REPO_ID: ID for the Maxim logger instance
//
// Returns:
// - schemas.Plugin: A configured plugin instance for request/response tracing
@@ -29,11 +29,10 @@ func getPlugin() (schemas.Plugin, error) {
return nil, fmt.Errorf("MAXIM_API_KEY is not set, please set it in your environment variables")
}
- if os.Getenv("MAXIM_LOGGER_ID") == "" {
- return nil, fmt.Errorf("MAXIM_LOGGER_ID is not set, please set it in your environment variables")
- }
-
- plugin, err := NewMaximLoggerPlugin(os.Getenv("MAXIM_API_KEY"), os.Getenv("MAXIM_LOGGER_ID"))
+ plugin, err := Init(Config{
+ ApiKey: os.Getenv("MAXIM_API_KEY"),
+ LogRepoId: os.Getenv("MAXIM_LOG_REPO_ID"),
+ })
if err != nil {
return nil, err
}
@@ -88,7 +87,7 @@ func TestMaximLoggerPlugin(t *testing.T) {
// Initialize the Maxim plugin
plugin, err := getPlugin()
if err != nil {
- log.Fatalf("Error setting up the plugin: %v", err)
+ t.Fatalf("Error setting up the plugin: %v", err)
}
account := BaseAccount{}
@@ -100,7 +99,7 @@ func TestMaximLoggerPlugin(t *testing.T) {
Logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
})
if err != nil {
- log.Fatalf("Error initializing Bifrost: %v", err)
+ t.Fatalf("Error initializing Bifrost: %v", err)
}
// Make a test chat completion request
@@ -127,3 +126,133 @@ func TestMaximLoggerPlugin(t *testing.T) {
client.Shutdown()
}
+
+// TestLogRepoIDSelection tests the single repository selection logic
+func TestLogRepoIDSelection(t *testing.T) {
+ tests := []struct {
+ name string
+ defaultRepo string
+ headerRepo string
+ expectedRepo string
+ shouldLog bool
+ }{
+ {
+ name: "Header repo takes priority",
+ defaultRepo: "default-repo",
+ headerRepo: "header-repo",
+ expectedRepo: "header-repo",
+ shouldLog: true,
+ },
+ {
+ name: "Fall back to default repo when no header",
+ defaultRepo: "default-repo",
+ headerRepo: "",
+ expectedRepo: "default-repo",
+ shouldLog: true,
+ },
+ {
+ name: "Use header repo when no default",
+ defaultRepo: "",
+ headerRepo: "header-repo",
+ expectedRepo: "header-repo",
+ shouldLog: true,
+ },
+ {
+ name: "Skip logging when neither available",
+ defaultRepo: "",
+ headerRepo: "",
+ expectedRepo: "",
+ shouldLog: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create plugin with default repo
+ plugin := &Plugin{
+ defaultLogRepoId: tt.defaultRepo,
+ }
+
+ // Create context with header repo if provided
+ ctx := context.Background()
+ if tt.headerRepo != "" {
+ ctx = context.WithValue(ctx, LogRepoIDKey, tt.headerRepo)
+ }
+
+ // Test the selection logic
+ result := plugin.getEffectiveLogRepoID(&ctx)
+
+ if result != tt.expectedRepo {
+ t.Errorf("Expected repo '%s', got '%s'", tt.expectedRepo, result)
+ }
+
+ shouldLog := result != ""
+ if shouldLog != tt.shouldLog {
+ t.Errorf("Expected shouldLog=%t, got shouldLog=%t", tt.shouldLog, shouldLog)
+ }
+ })
+ }
+}
+
+// TestPluginInitialization tests plugin initialization with different configs
+func TestPluginInitialization(t *testing.T) {
+ tests := []struct {
+ name string
+ config Config
+ expectError bool
+ }{
+ {
+ name: "Valid config with both fields",
+ config: Config{
+ ApiKey: "test-api-key",
+ LogRepoId: "test-repo-id",
+ },
+ expectError: false,
+ },
+ {
+ name: "Valid config with only API key",
+ config: Config{
+ ApiKey: "test-api-key",
+ LogRepoId: "",
+ },
+ expectError: false,
+ },
+ {
+ name: "Invalid config - missing API key",
+ config: Config{
+ ApiKey: "",
+ LogRepoId: "test-repo-id",
+ },
+ expectError: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Skip actual Maxim SDK initialization in tests
+ if tt.expectError {
+ _, err := Init(tt.config)
+ if err == nil {
+ t.Error("Expected error but got none")
+ }
+ } else {
+ // For valid configs, we can't test actual initialization without real API key
+ // Just test the validation logic
+ if tt.config.ApiKey == "" {
+ t.Skip("Skipping valid config test - would need real Maxim API key")
+ }
+ }
+ })
+ }
+}
+
+// TestPluginName tests the plugin name functionality
+func TestPluginName(t *testing.T) {
+ plugin := &Plugin{}
+ if plugin.GetName() != PluginName {
+ t.Errorf("Expected plugin name '%s', got '%s'", PluginName, plugin.GetName())
+ }
+ if PluginName != "maxim" {
+ t.Errorf("Expected PluginName constant to be 'maxim', got '%s'", PluginName)
+ }
+}
diff --git a/transports/bifrost-http/lib/ctx.go b/transports/bifrost-http/lib/ctx.go
index 5f92dabda6..4a908e81e3 100644
--- a/transports/bifrost-http/lib/ctx.go
+++ b/transports/bifrost-http/lib/ctx.go
@@ -112,9 +112,13 @@ func ConvertToBifrostContext(ctx *fasthttp.RequestCtx, allowDirectKeys bool) *co
bifrostCtx = context.WithValue(bifrostCtx, maxim.ContextKey(labelName), string(value))
}
+ if labelName == string(maxim.LogRepoIDKey) {
+ bifrostCtx = context.WithValue(bifrostCtx, maxim.ContextKey(labelName), string(value))
+ }
+
// apart from these all headers starting with x-bf-maxim- are keys for tags
// collect them in the maximTags map
- if labelName != string(maxim.GenerationIDKey) && labelName != string(maxim.TraceIDKey) && labelName != string(maxim.SessionIDKey) && labelName != string(maxim.TraceNameKey) && labelName != string(maxim.GenerationNameKey) {
+ if labelName != string(maxim.GenerationIDKey) && labelName != string(maxim.TraceIDKey) && labelName != string(maxim.SessionIDKey) && labelName != string(maxim.TraceNameKey) && labelName != string(maxim.GenerationNameKey) && labelName != string(maxim.LogRepoIDKey) {
maximTags[labelName] = string(value)
}
}
diff --git a/transports/bifrost-http/main.go b/transports/bifrost-http/main.go
index 03446c843c..a7947640e6 100644
--- a/transports/bifrost-http/main.go
+++ b/transports/bifrost-http/main.go
@@ -411,31 +411,30 @@ func main() {
// Eventually same flow will be used for third party plugins
for _, plugin := range config.Plugins {
if !plugin.Enabled {
+ logger.Debug("plugin %s is disabled, skipping initialization", plugin.Name)
continue
}
switch strings.ToLower(plugin.Name) {
case maxim.PluginName:
- if os.Getenv("MAXIM_LOG_REPO_ID") == "" {
- logger.Warn("maxim log repo id is required to initialize maxim plugin")
- continue
- }
- if os.Getenv("MAXIM_API_KEY") == "" {
- logger.Warn("maxim api key is required in environment variable MAXIM_API_KEY to initialize maxim plugin")
- continue
+
+ var maximConfig maxim.Config
+ if plugin.Config != nil {
+ configBytes, err := json.Marshal(plugin.Config)
+ if err != nil {
+ logger.Fatal("failed to marshal maxim config: %v", err)
+ }
+ if err := json.Unmarshal(configBytes, &maximConfig); err != nil {
+ logger.Fatal("failed to unmarshal maxim config: %v", err)
+ }
}
- maximPlugin, err := maxim.NewMaximLoggerPlugin(os.Getenv("MAXIM_API_KEY"), os.Getenv("MAXIM_LOG_REPO_ID"))
+ maximPlugin, err := maxim.Init(maximConfig)
if err != nil {
logger.Warn("failed to initialize maxim plugin: %v", err)
} else {
loadedPlugins = append(loadedPlugins, maximPlugin)
}
case semanticcache.PluginName:
- if !plugin.Enabled {
- logger.Debug("semantic cache plugin is disabled, skipping initialization")
- continue
- }
-
if config.VectorStore == nil {
logger.Error("vector store is required to initialize semantic cache plugin, skipping initialization")
continue
diff --git a/transports/go.mod b/transports/go.mod
index 74703eddc5..34f4abed1e 100644
--- a/transports/go.mod
+++ b/transports/go.mod
@@ -77,7 +77,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
- github.com/maximhq/maxim-go v0.1.8 // indirect
+ github.com/maximhq/maxim-go v0.1.10 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect
diff --git a/transports/go.sum b/transports/go.sum
index ca11ee4b5d..eac8467ac1 100644
--- a/transports/go.sum
+++ b/transports/go.sum
@@ -215,13 +215,12 @@ github.com/maximhq/bifrost/plugins/governance v1.2.9/go.mod h1:3KOk++F4jxS6gtndI
github.com/maximhq/bifrost/plugins/logging v1.2.9 h1:AyPSxR24VTyLD6dJ5JeYoxN1+maCA5d9FUCXMBwZHvE=
github.com/maximhq/bifrost/plugins/logging v1.2.9/go.mod h1:on7KopeErGqzjLRq2va81n6/n6WGD/gwbxFtpmX+O60=
github.com/maximhq/bifrost/plugins/maxim v1.2.7 h1:XelvIZVW0KaE8Q1m0zN4V02Pz9QoVtfxNf7vNyhl15c=
-github.com/maximhq/bifrost/plugins/maxim v1.2.7/go.mod h1:/AGvEY4yAXSQ58Tm7aPQmzCRkHibdzGlJ/qCW4U9uEE=
github.com/maximhq/bifrost/plugins/semanticcache v1.2.10 h1:1nzHBQjAYUBe5wpLPlevNtfW5wrcW+gDPHSjT1JUZUw=
github.com/maximhq/bifrost/plugins/semanticcache v1.2.10/go.mod h1:WLp3S/Zgn0dcWQ8truAvcvG4NROsshfsYlZnMvreYFs=
github.com/maximhq/bifrost/plugins/telemetry v1.2.8 h1:7I7VIgCiDocLqNLtjT8PJjpbl0pbpvaHtQfViaGGVC4=
github.com/maximhq/bifrost/plugins/telemetry v1.2.8/go.mod h1:qPBe99N0k7BQrekoYxmE7d8cwgh5ZgLs46z+cOj+wz4=
-github.com/maximhq/maxim-go v0.1.8 h1:LXCYwg/WLNY5rPBScki9y4/wjH7h4VEz8vPUXbyoI4g=
-github.com/maximhq/maxim-go v0.1.8/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw=
+github.com/maximhq/maxim-go v0.1.10 h1:rGBYSY3qld2zfZeL4HBmropkyfrqNiJ4IYA49jbvYX8=
+github.com/maximhq/maxim-go v0.1.10/go.mod h1:0+UTWM7UZwNNE5VnljLtr/vpRGtYP8r/2q9WDwlLWFw=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
diff --git a/ui/app/config/views/pluginsForm.tsx b/ui/app/config/views/pluginsForm.tsx
index 2ae4fc462f..39190a86f9 100644
--- a/ui/app/config/views/pluginsForm.tsx
+++ b/ui/app/config/views/pluginsForm.tsx
@@ -8,9 +8,10 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { getProviderLabel } from "@/lib/constants/logs";
import { getErrorMessage, useCreatePluginMutation, useGetPluginsQuery, useGetProvidersQuery, useUpdatePluginMutation } from "@/lib/store";
-import { CacheConfig, ModelProviderName } from "@/lib/types/config";
-import { SEMANTIC_CACHE_PLUGIN } from "@/lib/types/plugins";
+import { CacheConfig, MaximConfig, ModelProviderName } from "@/lib/types/config";
+import { MAXIM_PLUGIN, SEMANTIC_CACHE_PLUGIN } from "@/lib/types/plugins";
import { Loader2 } from "lucide-react";
+import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
@@ -27,12 +28,18 @@ const defaultCacheConfig: CacheConfig = {
cache_by_provider: true,
};
+const defaultMaximConfig: MaximConfig = {
+ api_key: "",
+ log_repo_id: "",
+};
+
interface PluginsFormProps {
isVectorStoreEnabled: boolean;
}
export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps) {
const [cacheConfig, setCacheConfig] = useState(defaultCacheConfig);
+ const [maximConfig, setMaximConfig] = useState(defaultMaximConfig);
const { data: providersData, error: providersError, isLoading: providersLoading } = useGetProvidersQuery();
@@ -51,8 +58,10 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
// Get semantic cache plugin and its config
const semanticCachePlugin = useMemo(() => plugins?.find((plugin) => plugin.name === SEMANTIC_CACHE_PLUGIN), [plugins]);
+ const maximPlugin = useMemo(() => plugins?.find((plugin) => plugin.name === MAXIM_PLUGIN), [plugins]);
const isSemanticCacheEnabled = Boolean(semanticCachePlugin?.enabled);
+ const isMaximEnabled = Boolean(maximPlugin?.enabled);
// Initialize cache config from plugin data
useEffect(() => {
@@ -61,15 +70,21 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
}
}, [semanticCachePlugin]);
+ useEffect(() => {
+ if (maximPlugin?.config) {
+ setMaximConfig({ ...defaultMaximConfig, ...maximPlugin.config });
+ }
+ }, [maximPlugin]);
+
// Update default provider when providers are loaded (only for new configs)
useEffect(() => {
- if (providers.length > 0 && !semanticCachePlugin?.config) {
+ if (providers.length > 0 && !semanticCachePlugin?.config && !maximPlugin?.config) {
setCacheConfig((prev) => ({
...prev,
provider: providers[0].name as ModelProviderName,
}));
}
- }, [providers, semanticCachePlugin?.config]);
+ }, [providers, semanticCachePlugin?.config, maximPlugin?.config]);
// Handle semantic cache toggle (create or update)
const handleSemanticCacheToggle = async (enabled: boolean) => {
@@ -95,6 +110,30 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
}
};
+ // Handle semantic cache toggle (create or update)
+ const handleMaximToggle = async (enabled: boolean) => {
+ try {
+ if (maximPlugin) {
+ // Update existing plugin
+ await updatePlugin({
+ name: MAXIM_PLUGIN,
+ data: { enabled, config: maximConfig },
+ }).unwrap();
+ } else {
+ // Create new plugin
+ await createPlugin({
+ name: MAXIM_PLUGIN,
+ enabled,
+ config: maximConfig,
+ }).unwrap();
+ }
+ toast.success(`Maxim ${enabled ? "enabled" : "disabled"} successfully`);
+ } catch (error) {
+ const errorMessage = getErrorMessage(error);
+ toast.error(`Failed to ${enabled ? "enable" : "disable"} Maxim: ${errorMessage}`);
+ }
+ };
+
// Update cache config
const updateCacheConfig = async (updates: Partial) => {
// Capture snapshot of previous config before updating
@@ -121,8 +160,9 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
}
};
- // Ref to store the timeout ID for debouncing
- const debounceTimeoutRef = useRef | null>(null);
+ // Refs to store the timeout IDs for debouncing (separate for cache and maxim)
+ const cacheDebounceTimeoutRef = useRef | null>(null);
+ const maximDebounceTimeoutRef = useRef | null>(null);
// Debounced version for text/number inputs
const debouncedUpdateCacheConfig = useCallback(
@@ -132,13 +172,13 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
setCacheConfig(newConfig);
// Clear previous timeout
- if (debounceTimeoutRef.current) {
- clearTimeout(debounceTimeoutRef.current);
+ if (cacheDebounceTimeoutRef.current) {
+ clearTimeout(cacheDebounceTimeoutRef.current);
}
// Only save to backend if plugin is enabled, with debouncing
if (semanticCachePlugin?.enabled) {
- debounceTimeoutRef.current = setTimeout(() => {
+ cacheDebounceTimeoutRef.current = setTimeout(() => {
updatePlugin({
name: SEMANTIC_CACHE_PLUGIN,
data: { enabled: true, config: newConfig },
@@ -158,11 +198,47 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
[cacheConfig, semanticCachePlugin?.enabled, updatePlugin],
);
- // Cleanup timeout on component unmount
+ const debouncedUpdateMaximConfig = useCallback(
+ (updates: Partial) => {
+ // Update local state immediately for responsive UI
+ const newConfig = { ...maximConfig, ...updates };
+ setMaximConfig(newConfig);
+
+ // Clear previous timeout
+ if (maximDebounceTimeoutRef.current) {
+ clearTimeout(maximDebounceTimeoutRef.current);
+ }
+
+ // Only save to backend if plugin is enabled, with debouncing
+ if (maximPlugin?.enabled) {
+ maximDebounceTimeoutRef.current = setTimeout(() => {
+ updatePlugin({
+ name: MAXIM_PLUGIN,
+ data: { enabled: true, config: newConfig },
+ })
+ .unwrap()
+ .then(() => {
+ toast.success("Maxim configuration updated successfully");
+ })
+ .catch((error) => {
+ toast.error("Failed to update Maxim configuration");
+ // Revert on error - use the newConfig that was captured in closure
+ setMaximConfig(maximConfig);
+ });
+ }, 500); // 500ms debounce
+ }
+ },
+ [maximConfig, maximPlugin?.enabled, updatePlugin],
+ );
+
+ // Cleanup timeouts on component unmount
useEffect(() => {
return () => {
- if (debounceTimeoutRef.current) {
- clearTimeout(debounceTimeoutRef.current);
+ if (cacheDebounceTimeoutRef.current) {
+ clearTimeout(cacheDebounceTimeoutRef.current);
+ }
+ if (maximDebounceTimeoutRef.current) {
+ clearTimeout(maximDebounceTimeoutRef.current);
}
};
}, []);
@@ -379,6 +455,88 @@ export default function PluginsForm({ isVectorStoreEnabled }: PluginsFormProps)
))}
+
+ {/* Maxim Logger Toggle */}
+
+
+
+
+
+ This will send traces of your requests and responses to the Maxim's Log repository. Read more about it{" "}
+
+ here
+
+ .
+ {!providersLoading && providers?.length === 0 && (
+ Requires at least one provider to be configured.
+ )}
+
+
+ {
+ handleMaximToggle(checked);
+ }}
+ />
+
+
+ {/* Maxim Configuration (only show when enabled) */}
+ {isMaximEnabled &&
+ (providersLoading ? (
+