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. + +![Maxim Logs](../media/maxim-logs.png) + +--- + +## 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 ? ( +
+ +
+ ) : ( +
+ + {/* Maxim API Key Input */} +
+
+
+ + debouncedUpdateMaximConfig({ api_key: e.target.value })} + /> +
+
+ + debouncedUpdateMaximConfig({ log_repo_id: e.target.value })} + /> +
+
+
+ +
+ +
    +
  • + You can override the default repository per request using the x-bf-maxim-log-repo-id header. +
  • +
  • If both x-bf-maxim-log-repo-id and default log repo id are absent, the request will not be logged.
  • +
  • + You can pass custom trace and generation IDs to the request context using the x-bf-maxim-trace-id and + x-bf-maxim-generation-id headers. +
  • +
  • + You can pass custom tags for the trace using the x-bf-maxim-[tag] headers. +
  • +
+
+
+ ))} +
); } diff --git a/ui/lib/types/config.ts b/ui/lib/types/config.ts index aa7851b07f..0dde297359 100644 --- a/ui/lib/types/config.ts +++ b/ui/lib/types/config.ts @@ -213,6 +213,7 @@ export interface CoreConfig { prometheus_labels: string[]; enable_logging: boolean; enable_governance: boolean; + enable_maxim: boolean; enforce_governance_header: boolean; allow_direct_keys: boolean; allowed_origins: string[]; @@ -235,6 +236,12 @@ export interface CacheConfig { updated_at?: string; } +// Maxim configuration types +export interface MaximConfig { + api_key: string; + log_repo_id: string; +} + // Form-specific custom provider config that allows any string for base_provider_type export interface FormCustomProviderConfig extends Omit { base_provider_type: string; diff --git a/ui/lib/types/plugins.ts b/ui/lib/types/plugins.ts index 5fb11a06c4..147747423b 100644 --- a/ui/lib/types/plugins.ts +++ b/ui/lib/types/plugins.ts @@ -1,6 +1,7 @@ // Plugins types that match the Go backend structures export const SEMANTIC_CACHE_PLUGIN = "semantic_cache"; +export const MAXIM_PLUGIN = "maxim"; export interface Plugin { name: string;